【问题标题】:How to lose margin/padding in UITextView如何在 UITextView 中丢失边距/填充
【发布时间】:2010-10-19 07:13:45
【问题描述】:

我的 iOS 应用程序中有一个UITextView,它显示大量文本。

然后我使用UITextView 的偏移边距参数来分页此文本。

我的问题是UITextView 的填充使我的计算变得混乱,因为它似乎因我使用的字体大小和字体而异。

是否可以删除UITextView 内容周围的填充?

【问题讨论】:

  • 请注意,这个 QA 已经快十年了!拥有超过 100,000 次查看,因为它是 iOS 中最愚蠢的问题之一。只是 FTR 我在下面输入了当前的 2017 年相当简单/通常/接受的解决方案作为答案。
  • 我仍然得到这方面的更新,我在 2009 年 IOS 3.0 刚刚发布时编写了一个硬编码的解决方法。我刚刚编辑了答案,以明确说明它已经过时并忽略了已接受的状态。
  • 令人惊讶的是,十多年了(这个 QA 有 200,000 次浏览!),Apple仍然没有修复 U​​ITextView 中普通错误、软错误和奇怪行为的组合.这确实是所有移动计算开发中最奇怪的事情之一!

标签: ios iphone cocoa-touch uikit uitextview


【解决方案1】:

此解决方法是在 2009 年 iOS 3.0 发布时编写的。它不再适用。

我遇到了完全相同的问题,最后我不得不使用

nameField.contentInset = UIEdgeInsetsMake(-4, -8, 0, 0);

其中 nameField 是 UITextView。我碰巧使用的字体是 Helvetica 16 点。这只是针对我正在绘制的特定字段大小的自定义解决方案。这使得左侧偏移量与左侧齐平,并且顶部偏移量在我想要的位置用于绘制它的框。

此外,这似乎仅适用于您使用默认对齐方式的UITextViews,即

nameField.textAlignment = NSTextAlignmentLeft;

例如向右对齐,UIEdgeInsetsMake 似乎对右边缘完全没有影响。

至少,使用 .contentInset 属性可以让您将字段放置在“正确”的位置,并在不抵消UITextViews 的情况下适应偏差。

【讨论】:

  • 是的,它在 iOS 7 中确实有效。确保 UIEdgeInset 设置为正确的值。它可能与 UIEdgeInsetsMake(-4,-8,0,0) 不同。
  • 在 IOS 7 中,我发现 UIEdgeInsetsMake(0,-4,0,-4) 效果最好。
  • 是的,这些是示例数字。我说“它只是我正在绘制的特定字段大小的自定义解决方案”。主要是我试图说明您必须根据自己的独特布局情况使用数字。
  • UITextAlignmentLeft 在 iOS 7 中已弃用。使用 NSTextAlignmentLeft。
  • 我不明白为什么人们会赞成使用硬编码值的答案。它很可能会在未来的 iOS 版本中出现问题,这只是一个糟糕的主意。
【解决方案2】:

在进行插入解决方案时,我仍然在右侧和底部有填充。文本对齐也引起了问题。我发现唯一可靠的方法是将文本视图放在另一个裁剪到边界的视图中。

【讨论】:

  • 什么是“插入解决方案”?你能详细说明和/或添加参考吗?请通过editing (changing) your answer 回复,而不是在 cmets 中(without "Edit:"、"Update:" 或类似的 - 答案应该看起来像是今天写的)。
【解决方案3】:
[firstNameTextField setContentVerticalAlignment:UIControlContentVerticalAlignmentCenter];

【讨论】:

  • 解释一下。例如,想法/要点是什么?请通过editing (changing) your answer 回复,而不是在 cmets 中(without "Edit:"、"Update:" 或类似的 - 答案应该看起来像是今天写的)。
【解决方案4】:

在 iOS 5 上 UIEdgeInsetsMake(-8,-8,-8,-8); 似乎工作得很好。

【讨论】:

    【解决方案5】:

    我发现了另一种方法。从 UITextView 的子视图中获取带有文本的视图并在子类的 layoutSubview 方法中进行设置:

    - (void)layoutSubviews {
        [super layoutSubviews];
    
        const int textViewIndex = 1;
        UIView *textView = [self.subviews objectAtIndex:textViewIndex];
        textView.frame = CGRectMake(
                                     kStatusViewContentOffset,
                                     0.0f,
                                     self.bounds.size.width - (2.0f * kStatusViewContentOffset),
                                     self.bounds.size.height - kStatusViewContentOffset);
    }
    

    【讨论】:

      【解决方案6】:

      Storyboard 或 Interface Builder 解决方案使用用户定义的运行时属性

      屏幕截图来自 iOS 7.1 和 iOS 6.1,contentInset = {{-10, -5}, {0, 0}}

      【讨论】:

      • 在 iOS 7 中为我工作。
      • 只针对用户定义的运行时属性 - 太棒了!
      【解决方案7】:

      对于 iOS 7.0,我发现 contentInset 技巧不再有效。这是我用来摆脱 iOS 7 中的边距/填充的代码。

      这会将文本的左边缘带到容器的左边缘:

      textView.textContainer.lineFragmentPadding = 0
      

      这会使文本的顶部与容器的顶部对齐:

      textView.textContainerInset = .zero
      

      这两行都需要完全去除边距/填充。

      【讨论】:

      • 这对我有用两行,但是当我达到 5 行时,文本被截断了。
      • 注意第二行也可以写成:self.descriptionTextView.textContainerInset = UIEdgeInsetsZero;
      • lineFragmentPadding 设置为 0 是我一直在寻找的魔法。我不知道为什么 Apple 很难将 UITextView 内容与其他控件对齐。
      • 这是正确答案。此解决方案不会发生水平滚动。
      • lineFragmentPadding 不打算修改边距。从docs 行片段填充不是为了表达文本边距而设计的。相反,您应该在文本视图上使用插图,调整段落边距属性,或更改文本视图在其父视图中的位置。
      【解决方案8】:

      在已经给出的一些好的答案的基础上,这里是一个纯粹的基于 Storyboard / Interface Builder 的解决方案,适用于 iOS 7.0+

      为以下键设置 UITextView 的 用户定义的运行时属性

      textContainer.lineFragmentPadding
      textContainerInset
      

      【讨论】:

      • 这显然是最好的答案,特别是如果您需要仅 XIB 的解决方案
      • 复制/粘贴:textContainer.lineFragmentPadding | textContainerInset
      • 这是一个漂亮的答案 - 简单且效果很好,即使在 3 年后 :)
      【解决方案9】:

      您可以使用UITextViewtextContainerInset 属性:

      textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);
      

      (上、左、下、右)

      【讨论】:

      • 我想知道为什么这里没有将其指定为最佳答案。 textContainerInset 确实满足了我的需求。
      • 当我想将textContainerInset 属性的底部值更改为新值时,它对我不起作用—请参阅stackoverflow.com/questions/19422578/…
      • @MaggiePhillips 这不是最好的答案,因为硬编码值不可能是正确的方法,并且在某些情况下肯定会失败。您需要考虑 lineFragmentPadding。
      【解决方案10】:

      所有这些答案都解决了标题问题,但我想为 OP 问题正文中提出的问题提出一些解决方案。

      文本内容的大小

      计算UITextView 中文本大小的一种快速方法是使用NSLayoutManager

      UITextView *textView;
      CGSize textSize = [textView usedRectForTextContainer:textView.textContainer].size;
      

      这给出了总的可滚动内容,它可能大于UITextView 的框架。我发现这比textView.contentSize 准确得多,因为它实际上计算了文本占用了多少空间。例如,给定一个空的UITextView

      textView.frame.size = (width=246, height=50)
      textSize = (width=10, height=16.701999999999998)
      textView.contentSize = (width=246, height=33)
      textView.textContainerInset = (top=8, left=0, bottom=8, right=0)
      

      行高

      UIFont 有一个属性,可以让您快速获取给定字体的行高。因此,您可以通过以下方式快速找到 UITextView 中文本的行高:

      UITextView *textView;
      CGFloat lineHeight = textView.font.lineHeight;
      

      计算可见文本大小

      确定实际可见的文本数量对于处理“分页”效果很重要。 UITextView 有一个名为 textContainerInset 的属性,它实际上是实际 UITextView.frame 和文本本身之间的边距。要计算可见框架的实际高度,您可以执行以下计算:

      UITextView *textView;
      CGFloat textViewHeight = textView.frame.size.height;
      UIEdgeInsets textInsets = textView.textContainerInset;
      CGFloat textHeight = textViewHeight - textInsets.top - textInsets.bottom;
      

      确定分页大小

      最后,既然您已经有了可见的文本大小和内容,您可以通过从textSize 中减去textHeight 来快速确定您的偏移量:

      // where n is the page number you want
      CGFloat pageOffsetY = textSize - textHeight * (n - 1);
      textView.contentOffset = CGPointMake(textView.contentOffset.x, pageOffsetY);
      
      // examples
      CGFloat page1Offset = 0;
      CGFloat page2Offset = textSize - textHeight
      CGFloat page3Offset = textSize - textHeight * 2
      

      使用所有这些方法,我没有触摸我的插入,我能够转到插入符号或文本中我想要的任何位置。

      【讨论】:

        【解决方案11】:

        我肯定会避免任何涉及硬编码值的答案,因为实际边距可能会随着用户字体大小设置等而变化。

        这里是user1687195's answer,编写时没有修改textContainer.lineFragmentPadding(因为documentation 状态不是预期的用法)。

        这适用于 iOS 7 及更高版本。

        self.textView.textContainerInset = UIEdgeInsetsMake(
                                              0,
                                              -self.textView.textContainer.lineFragmentPadding,
                                              0,
                                              -self.textView.textContainer.lineFragmentPadding);
        

        这实际上是相同的结果,只是更简洁一点,因为它不会滥用 lineFragmentPadding 属性。

        【讨论】:

        • 我根据这个答案尝试了另一种解决方案,效果很好。解决方案是:self.textView.textContainer.lineFragmentPadding = 0;
        • @BakytAbdrasulov 请阅读我的回答。虽然您的解决方案有效,但它不符合文档(请参阅我的答案中的链接)。 lineFragmentPadding 并不是为了控制边距。这就是为什么你应该使用textContainerInset
        【解决方案12】:

        对于 iOS 10,以下行适用于移除顶部和底部填充。

        captionTextView.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0)
        

        【讨论】:

        • Xcode 8.2.1。仍然与此答案中提到的问题相同。我的解决方案是将值编辑为 UIEdgeInsets(top: 0, left: -4.0, bottom: 0, right: -4.0)。
        • UIEdgeInset.zero 使用 XCode 8.3 和 iOS 10.3 模拟器工作
        【解决方案13】:

        textView 的滚动也会影响文本的位置,使其看起来不像垂直居中。我设法通过禁用滚动并将顶部插图设置为 0 来使视图中的文本居中:

            textView.scrollEnabled = NO;
            textView.textContainerInset = UIEdgeInsetsMake(0, textView.textContainerInset.left, textView.textContainerInset.bottom, textView.textContainerInset.right);
        

        由于某种原因我还没有弄清楚,在输入开始之前光标仍未居中,但在我开始输入时文本立即居中。

        【讨论】:

          【解决方案14】:

          2021 年最新版本

          这是 iOS 中最愚蠢的错误之一。

          这里给出的类 UITextViewFixed 被广泛使用,通常是总体上最合理的解决方案

          这是课程:

          @IBDesignable class UITextViewFixed: UITextView {
              override func layoutSubviews() {
                  super.layoutSubviews()
                  setup()
              }
              func setup() {
                  textContainerInset = UIEdgeInsets.zero
                  textContainer.lineFragmentPadding = 0
              }
          }
          

          不要忘记在 Inspector 中关闭 scrollEnabled!

          1. 该解决方案在情节提要中正常工作

          2. 解决方案在运行时正常工作

          就是这样,你完成了。

          一般而言,在大多数情况下应该就是您所需要的全部

          即使您动态地更改文本视图的高度UITextViewFixed 通常也能满足您的所有需求。

          (动态更改高度的一个常见示例是在用户键入时更改它。)

          这是 Apple 损坏的 UITextView...

          这里是UITextViewFixed

          请注意,您当然必须...

          ...在 Inspector 中关闭 scrollEnabled!

          (开启scrollEnabled的意思是“通过尽可能扩大下边距,让这个视图在垂直方向尽可能扩大。”)


          一些进一步的问题

          (1) 在一些非常不寻常的情况下动态改变高度,Apple 做了一件奇怪的事情:他们在底部添加了额外的空间

          不,真的!这一定是 iOS 中最令人愤怒的事情之一。

          如果您遇到问题,这里有一个“快速修复”,通常会有所帮助:

          ...
                  textContainerInset = UIEdgeInsets.zero
                  textContainer.lineFragmentPadding = 0
          
                  // this is not ideal, but sometimes this "quick fix"
                  // will solve the "extra space at the bottom" insanity:
                  var b = bounds
                  let h = sizeThatFits(CGSize(
                     width: bounds.size.width,
                     height: CGFloat.greatestFiniteMagnitude)
                 ).height
                 b.size.height = h
                 bounds = b
           ...
          

          (2) 在极少数情况下,要修复 Apple 的另一个微妙问题,您必须添加:

          override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
              super.setContentOffset(contentOffset, animated: false) // sic
          }
          

          (3) 可以说,我们应该添加:

          contentInset = UIEdgeInsets.zero
          

          就在.lineFragmentPadding = 0 之后UITextViewFixed

          但是……不管你信不信……在当前的 iOS 中根本行不通! (2021 年检查。)将来可能需要添加该行。

          UITextView 在 iOS 中被破坏的事实是所有移动计算中最奇怪的事情之一。这个问题十周年了,还没有解决!

          最后,这里有一个与 TextField 有点相似的提示:Set the maximum character length of a UITextField in Swift

          完全随机提示:如何在末尾添加“...”

          您经常使用“像 UILabel”一样的 UITextView。因此,您希望它使用省略号“...”来截断文本

          如果有,请添加:

           textContainer.lineBreakMode = .byTruncatingTail
          

          如果你想要 0 高度的小贴士,什么时候根本没有 文字

          您经常使用文本视图来仅显示文本。因此,您使用行“0”表示文本视图将根据文本行数自动更改高度。

          那太好了。但是如果根本没有文字,那么不幸的是你得到的高度就好像只有一行文字!!!!文本视图永远不会“消失”。

          如果你想让它“消失”,只需添加这个

          override var intrinsicContentSize: CGSize {
              var i = super.intrinsicContentSize
              print("for \(text) size will be \(i)")
              if text == "" { i.height = 1.0 }
              print("   but we changed it to \(i)")
              return i
          }
          

          (我将它设为“1”高度,所以演示中发生了什么很清楚,“0”很好。)

          UILabel 呢?

          当只是显示文本时,UILabel 比 UITextView 有很多优势。 UILabel 没有遇到此问答页面中描述的问题。

          确实,我们通常“放弃”而只使用 UITextView 的原因是 UILabel 难以使用。特别是要正确地向 UILabel 添加 padding 是非常困难的。

          事实上,这里是关于如何“最终”正确地向 UILabel 添加填充的完整讨论:Adding space/padding to a UILabel。在某些情况下,如果您正在使用动态高度单元格进行困难的布局,有时最好使用 UILabel 进行困难的布局。

          【讨论】:

          • 为什么说inspector中必须关闭scrollEnabled呢?当我这样做时,我的文本视图中的空间似乎更少了。
          • 滚动视图的框架高度与内容大小无关。 OP提出的问题没有提到身高。只是想了解您的反应。
          • 我已经发布了对@Fattie 答案的更新答案,它帮助我使用启用/禁用translatesAutoresizingMaskIntoConstraints 的特殊技巧真正摆脱了所有插入。仅凭这个答案,我无法使用自动布局删除视图层次结构中的所有边距(一些奇怪的底部边距拒绝消失)。它还使用 systemLayoutSizeFitting 处理视图大小的计算,由于 UITextView 的错误,它之前返回了无效的大小@
          • 我在表格中有文本视图,只有单行文本的那些没有正确计算其高度(自动布局)。要修复它,我必须覆盖 didMoveToSuperview 并在那里调用 setup 。
          • 我在我的应用程序中使用了它,它导致 layoutSubviews 和 setup 之间的递归调用导致堆栈溢出错误!这里出了点问题
          【解决方案15】:

          对于 Swift 4、Xcode 9

          使用以下函数。它可以改变 UITextView 中文本的边距/填充:

          public func UIEdgeInsetsMake(_ top: CGFloat, _ left: CGFloat, _ bottom: CGFloat, _ right: CGFloat) -> UIEdgeInsets
          

          所以在这种情况下是:

           self.textView?.textContainerInset = UIEdgeInsetsMake(0, 0, 0, 0)
          

          【讨论】:

            【解决方案16】:

            这是Fattie's very helpful answer 的更新版本。 它添加了两条重要的线,帮助我让布局在 iOS 10 和 11 上运行(可能也适用于较低的版本):

            @IBDesignable class UITextViewFixed: UITextView {
                override func layoutSubviews() {
                    super.layoutSubviews()
                    setup()
                }
                func setup() {
                    translatesAutoresizingMaskIntoConstraints = true
                    textContainerInset = UIEdgeInsets.zero
                    textContainer.lineFragmentPadding = 0
                    translatesAutoresizingMaskIntoConstraints = false
                }
            }
            

            重要的几行是两个translatesAutoresizingMaskIntoConstraints = <true/false> 语句!

            这令人惊讶地在我的所有情况下都删除了所有边距

            虽然textView 不是第一响应者,但可能会出现一些奇怪的底部边距,使用接受的答案中提到的sizeThatFits 方法无法解决。

            当点击 textView 时,奇怪的底部边距突然消失了,一切看起来都应该如此,但只有在 textView 获得 firstResponder 之后。

            所以我读到here on Stack Overflow,在调用之间手动设置框架/边界时,启用和禁用translatesAutoresizingMaskIntoConstraints 确实有帮助。

            幸运的是,这不仅适用于框架设置,还适用于夹在两个 translatesAutoresizingMaskIntoConstraints 调用之间的两行 setup()

            这在计算视图框架时非常有用UIView 上使用systemLayoutSizeFitting。它返回正确的大小(以前没有)!

            如原答案所述:

            不要忘记在 Inspector 中关闭 scrollEnabled!该解决方案在情节提要和运行时都能正常工作。

            就是这样,现在您真的完成了!

            【讨论】:

              【解决方案17】:

              这是一个简单的小扩展,可以从应用中的每个文本视图中删除 Apple 的默认边距。

              注意:Interface Builder 仍会显示旧边距,但您的应用将按预期运行。

              extension UITextView {
              
                 open override func awakeFromNib() {
                    super.awakeFromNib();
                    removeMargins();
                 }
              
                 /** Removes the Apple textview margins. */
                 public func removeMargins() {
                    self.contentInset = UIEdgeInsetsMake(
                       0, -textContainer.lineFragmentPadding,
                       0, -textContainer.lineFragmentPadding);
                 }
              }
              

              【讨论】:

                【解决方案18】:

                最新的斯威夫特:

                self.textView.textContainerInset = .init(top: -2, left: 0, bottom: 0, right: 0)
                self.textView.textContainer.lineFragmentPadding = 0
                

                【讨论】:

                • 很好的答案。这个答案删除了额外的顶部插图。 textView.textContainerInset = UIEdgeInsets.zero 不会从顶部插图中移除 2 个像素。
                【解决方案19】:

                对我(iOS 11 和 Xcode 9.4.1)来说,神奇的是将 textView.font 属性设置为 UIFont.preferred(forTextStyle:UIFontTextStyle) 样式,并将第一个答案设置为 mentioned by Fattie。但是在我设置 textView.font 属性之前,Fattie 的答案不起作用。否则 UITextView 一直表现不正常。

                【讨论】:

                  【解决方案20】:

                  如果有人正在寻找最新的 Swift 版本,那么下面的代码可以在 Xcode 10.2Swift 4.2

                  上正常工作
                  yourTextView.textContainerInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
                  

                  【讨论】:

                    【解决方案21】:

                    如果您想设置 HTML 字符串并避免底部填充,请确保您没有使用块标签,即 divp

                    就我而言,这就是原因。您可以通过将出现的块标记替换为 span 标记来轻松对其进行测试。

                    【讨论】:

                      【解决方案22】:

                      SwiftUI

                      如果您正在使用 UIViewRepresentable 制作自己的 TextView 并想要控制填充,请在您的 makeUIView 函数中执行以下操作:

                      uiTextView.textContainerInset = UIEdgeInsets(top: 10, left: 18, bottom: 0, right: 18)

                      或任何你想要的。

                      【讨论】:

                        【解决方案23】:

                        你需要在 didMoveToSuperView 上设置 inset 和 lineFragmentPadding

                        @IBDesignable class UITextViewFixed: UITextView {
                            override func didMoveToSuperview() {
                                super.didMoveToSuperview()
                                setup()
                            }
                            func setup() {
                                textContainerInset = UIEdgeInsets.zero
                                textContainer.lineFragmentPadding = 0
                            }
                        }
                        

                        【讨论】:

                        • textContainerInset吗?不是textContainer.Inset
                        【解决方案24】:

                        对于由于将 HTML 解析为 UITextView 而遇到此问题的任何人,只需删除由结束 p 标记“/n”引入的新行。我假设您使用 NSMutableAttributedString

                        if let lastCharacter = attributedString.string.last, lastCharacter == "\n" {
                            attributedString.deleteCharacters(in: NSRange(location:(attributedString.length) - 1, length:1))
                        }
                        

                        【讨论】: