如何在UITextView中去除边距/填充。

569

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

我通过使用UITextView的偏移边距参数来对这些文本进行分页。

问题是UITextView的填充似乎因所使用的字体大小和字体类型而异,这使得我的计算变得复杂难懂。

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


11
请注意,这个问答已经有将近十年的历史了!虽然它是iOS中最愚蠢的问题之一,但已经有超过100,000次的浏览量。仅供参考,我在下面作为答案提供了当前2017年的合理、简单、常见解决方案。 - Fattie
1
我仍然会收到关于这个问题的更新,因为在2009年IOS 3.0刚发布时,我编写了一个硬编码解决方法。我刚刚编辑了答案,明确说明它已经过时多年,并忽略了被接受的状态。 - Michael
1
令人惊讶的是,现在已经超过十年了(这个QA已经有200,000次浏览!),但苹果仍然没有修复UITextView中普通错误、软件错误和奇怪行为的组合。这确实是移动计算开发中最奇怪的事情之一! - Fattie
24个回答

844

针对iOS 7.0,我发现contentInset技巧不再起作用。以下是我用于消除iOS 7中边距/填充的代码。

这将把文本的左边缘移至容器的左边缘:

textView.textContainer.lineFragmentPadding = 0

这会导致文本顶部与容器顶部对齐:

textView.textContainerInset = .zero

需要使用这两行代码才能完全移除边距/内边距。


2
这对我来说适用于两行,但当我到达五行时,文本被截断了。 - livings124
7
请注意,第二行也可以写成:self.descriptionTextView.textContainerInset = UIEdgeInsetsZero; 本翻译保持原意,尽可能通俗易懂。 - jessepinho
21
lineFragmentPadding设置为0是我正在寻找的技巧。我不知道为什么苹果公司让UITextView内容与其他控件对齐如此困难。 - phatmann
3
这是正确的答案。使用这种解决方案不会发生水平滚动。 - Michael
2
lineFragmentPadding 不应该用来修改边距。根据文档所述,行片段填充不是用来表达文本边距的。相反,您应该在文本视图上使用插图、调整段落边距属性或更改文本视图在其父视图中的位置。 - Martin Berger
显示剩余9条评论

488

2023最新资讯

iOS中最傻的bug之一。

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

以下是该类:

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

别忘了在Inspector中关闭scrollEnabled!

  1. 该解决方案在Storyboard中正常工作。

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

你完成了。通常情况下,这应该是你大多数需要的。

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

(一个常见的动态更改高度的示例是随着用户输入而更改高度。)

这里是苹果破损的UITextView...

Interface Builder中的UITextView截图

这里是 UITextViewFixed

Interface Builder中的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)在极少数情况下,为了修复苹果的另一个微妙错误,您需要添加:

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

(3) 或许我们应该添加:

contentInset = UIEdgeInsets.zero

UITextViewFixed中添加.lineFragmentPadding = 0后,就可以达到预期效果。

不过令人难以置信的是,在当前 iOS(2023年)中这个方法根本就不起作用!或许未来需要添加这行代码。

UITextView在 iOS 中的崩溃问题是移动计算领域最奇怪的事情之一。这个问题已经持续了十年,但却依然没有得到解决!

最后,这里有一个类似的小提示适用于TextField如何在 Swift 中设置 UITextField 的最大字符长度

完全随机的小技巧:如何添加“...”

通常情况下,你会像使用一个 UILabel 一样使用 UITextView。所以你希望它使用省略号“...”截断文本,那么可以添加以下代码:

 textContainer.lineBreakMode = .byTruncatingTail

如果你想让高度为零,当没有文本时可使用的小技巧

通常你使用一个文本视图仅用于显示文本。所以,你使用lines "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添加填充非常困难。

事实上,这是一篇关于如何“最终”正确地向UILabel添加填充的完整讨论:向UILabel添加空格/填充。在某些情况下,如果您正在进行动态高度单元格的困难布局,则有时最好用UILabel做法处理。


1
为什么您说在检查器中关闭scrollEnabled是必要的?当我这样做时,我的文本视图似乎会变得更小。 - shim
1
滚动视图框架的高度与内容大小无关。OP提出的问题没有提到高度。只是想理解您的回答。 - shim
3
我已经在@Fattie的答案下发布了一份更新后的答案,它帮助我真正通过启用/禁用translatesAutoresizingMaskIntoConstraints这个特殊技巧来摆脱所有插图。 仅凭这个答案,我无法使用自动布局删除视图层次结构中的所有边距(一些奇怪的底部边距无法消失)。 它还处理了使用systemLayoutSizeFitting计算视图大小的问题。由于有bug的UITextView,以前会返回无效的大小。 - Fab1n
2
我在表格中使用了TextViews,其中单行文本的高度没有正确计算(自动布局)。为了解决这个问题,我不得不重写didMoveToSuperview并在那里调用设置。 - El Horrible
4
我用这个在我的应用程序中,它导致layoutSubviews和setup之间发生递归调用,从而引起堆栈溢出错误!这里有问题。 - Tometoyou
显示剩余10条评论

259

这个解决方法是在2009年iOS 3.0发布时编写的,现在已经不适用了。

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

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

其中nameField是一个UITextView。我使用的字体是Helvetica 16点大小,这只是为特定字段大小而定制的解决方案。这使得左侧偏移与左侧齐平,并使顶部偏移在其所绘制的框中。

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

nameField.textAlignment = NSTextAlignmentLeft;

例如,将其右对齐并且 UIEdgeInsetsMake 似乎根本不影响右边缘。

至少,使用 .contentInset 属性可让您以“正确”的位置放置字段,并调整偏差而无需抵消您的 UITextView


1
是的,它可以在iOS 7中工作。确保UIEdgeInset设置为正确的值。它可能与UIEdgeInsetsMake(-4,-8,0,0)不同。 - app_
15
在IOS 7中,我发现UIEdgeInsetsMake(0,-4,0,-4)效果最好。 - Racura
2
是的,那些是示例数字。我说过:“这只是我绘制特定字段大小的自定义解决方案。”主要是想说明您必须根据您独特的布局情况来调整这些数字。 - Michael
3
UITextAlignmentLeft 在iOS 7中已经废弃,请使用 NSTextAlignmentLeft。 - Jordi Kroon
7
我不明白为什么人们会投票支持使用硬编码数值的答案。这样做极有可能在未来的iOS版本中出现错误,是个很糟糕的想法。请注意保持原意,使语言更通俗易懂,不得添加任何额外内容。 - ldoogy
显示剩余6条评论

87

在已有的一些好答案的基础上,这里提供了一个纯Storyboard / Interface Builder-based解决方案,适用于iOS 7.0+。

为UITextView设置以下键的用户定义的运行时属性(User Defined Runtime Attributes)

[具体内容需要根据上下文和实际情况进行翻译]

textContainer.lineFragmentPadding
textContainerInset

Interface Builder


5
这显然是最佳答案,特别是如果你需要一个仅限XIB的解决方案。 - yano
17
textContainer.lineFragmentPadding | textContainerInset 文本容器.行片段填充|文本容器插入。 - Luca De Angelis
3
这就是一个美好回答的要素——简单易行,即使三年后仍然有效 :) - Shai Mishali
这个答案受到了人们的赞扬。很抱歉,但这种设置属性的方式非常糟糕和懒惰。如果属性发生变化,你将会在运行时而不是编译时遇到崩溃。用户自定义运行时属性是一个糟糕的技巧,当苹果还在推动故事板和XIB时。请永远不要这样做,放弃所有的故事板,直接在代码中工作。 - undefined

47
在 iOS 5 上,UIEdgeInsetsMake(-8,-8,-8,-8); 看起来非常有效。

44

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

这里是user1687195的答案,未修改textContainer.lineFragmentPadding(因为文档说明这不是预期的用法)。

这对iOS 7及更高版本非常有效。

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

这实际上是相同的结果,只不过更加干净,因为它没有误用lineFragmentPadding属性。


1
我尝试了基于这个答案的另一个解决方案,它运行得非常好。解决方案是:self.textView.textContainer.lineFragmentPadding = 0; - Bakyt Abdrasulov
1
@BakytAbdrasulov 请阅读我的回答。虽然您的解决方案有效,但它不符合文档要求(请参见我的回答中的链接)。lineFragmentPadding并不用于控制边距。这就是为什么您应该使用textContainerInset的原因。 - ldoogy

25

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

屏幕截图显示iOS 7.1和iOS 6.1,其中contentInset = {{-10,-5},{0,0}}

用户定义的运行时属性

输出


1
在iOS 7中对我有效。 - kubilay
仅为用户定义的运行时属性更新 - 太棒了! - hris.to

17

所有这些答案都回答了标题问题,但我想提出一些解决方法来解决原帖中提出的问题。

文本内容大小

计算 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;

确定页面大小

最后,现在您已经获取了可见文本大小和内容,可以通过将textHeighttextSize中减去来快速确定偏移量:

// 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

使用所有这些方法后,我没有触及我的插图区并且可以到达我想要的光标或文本位置。


17
您可以使用UITextView的textContainerInset属性:
textView.textContainerInset = UIEdgeInsetsMake(10, 10, 10, 10);

(顶部、左侧、底部、右侧)


1
我想知道为什么这个答案没有被选为最佳答案。textContainerInset确实满足了我的需求。 - KoreanXcodeWorker
当我想将textContainerInset属性的底部值更改为新值时,更新操作并不起作用。请参见https://dev59.com/_GIk5IYBdhLWcg3wRcQo。 - Evan R
@MaggiePhillips 这不是最好的答案,因为硬编码的值不可能是正确的方法,在某些情况下肯定会失败。您需要考虑lineFragmentPadding。 - ldoogy

14

这是Fattie非常有帮助的答案的更新版本,它添加了两行重要的代码,帮助我在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后才能如此。

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

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

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

如原始答案中所述:

不要忘记在检查器中关闭scrollEnabled!这个解决方案在storyboard和运行时都有效。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,