DTAttributedTextView does not relayout - ios

When the DTAttributedTextView's frame changes the content (text) inside the view is not relayouted. I have the text view in my tableView cell and its height is specified by a layout constraint, which value I change in code. Width changes automatically with the constraints based on the cell width.
I have tried to manually call setNeedsLayout, layoutIfNeeded, relayoutText, setNeedsDisplay. Nothing seems to help. Why is the content layout locked this way?
This seems to work for the height, but not for the width:
override func layoutSubviews() {
super.layoutSubviews()
comment.attributedTextContentView.layoutFrame = DTCoreTextLayoutFrame(
frame: comment.frame,
layouter: comment.attributedTextContentView.layouter)
comment.relayoutText()
}

Its necessary to set the layouter to nil before calling relayoutText
comment.attributedTextContentView.layoutFrame = DTCoreTextLayoutFrame(
frame: comment.frame,
layouter: comment.attributedTextContentView.layouter)
//gets rid of cached layouter
comment.layouter = nil
comment.relayoutText()

Related

A mystery about iOS autolayout with table views and self-sizing table view cells

To help in following this question, I've put up a GitHub repository:
https://github.com/mattneub/SelfSizingCells/tree/master
The goal is to get self-sizing cells in a table view, based on a custom view that draws its own text rather than a UILabel. I can do it, but it involves a weird layout kludge and I don't understand why it is needed. Something seems to be wrong with the timing, but then I don't understand why the same problem doesn't occur for a UILabel.
To demonstrate, I've divided the example into three scenes.
Scene 1: UILabel
In the first scene, each cell contains a UILabel pinned to all four sides of the content view. We ask for self-sizing cells and we get them. Looks great.
Scene 2: StringDrawer
In the second scene, the UILabel has been replaced by a custom view called StringDrawer that draws its own text. It is pinned to all four sides of the content view, just like the label was. We ask for self-sizing cells, but how will we get them?
To solve the problem, I've given StringDrawer an intrinsicContentSize based on the string it is displaying. Basically, we measure the string and return the resulting size. In particular, the height will be the minimal height that this view needs to have in order to display the string in full at this view's current width, and the cell is to be sized to that.
class StringDrawer: UIView {
#NSCopying var attributedText = NSAttributedString() {
didSet {
self.setNeedsDisplay()
self.invalidateIntrinsicContentSize()
}
}
override func draw(_ rect: CGRect) {
self.attributedText.draw(with: rect, options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin], context: nil)
}
override var intrinsicContentSize: CGSize {
let measuredSize = self.attributedText.boundingRect(
with: CGSize(width:self.bounds.width, height:10000),
options: [.truncatesLastVisibleLine, .usesLineFragmentOrigin],
context: nil).size
return CGSize(width: UIView.noIntrinsicMetric, height: measuredSize.height.rounded(.up) + 5)
}
}
But something's wrong. In this scene, some of the initial cells have some extra white space at the bottom. Moreover, if you scroll those cells out of view and then back into view, they look correct. And all the other cells look fine. That proves that what I'm doing is correct, so why isn't it working for the initial cells?
Well, I've done some heavy logging, and I've discovered that at the time intrinsicContentSize is called initially for the visible cells, the StringDrawer does not yet correctly know its own final width, the width that it will have after autolayout. We are being called too soon. The width we are using is too narrow, so the height we are returning is too tall.
Scene 3: StringDrawer with workaround
In the third scene, I've added a workaround for the problem we discovered in the second scene. It works great! But it's horribly kludgy. Basically, in the view controller, I wait until the view hierarchy has been assembled, and then I force the table view to do another round of layout by calling beginUpdates and endUpdates.
var didInitialLayout = false
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if !didInitialLayout {
didInitialLayout = true
UIView.performWithoutAnimation {
self.tableView.beginUpdates()
self.tableView.endUpdates()
}
}
}
The Mystery
Okay, so here are my questions:
(1) Is there a better, less kludgy workaround?
(2) Why do we need this workaround at all? In particular, why do we have this problem with my StringDrawer but not with a UILabel? Clearly, a UIlabel does know its own width early enough for it to give its own content size correctly on the first pass when it is interrogated by the layout system. Why is my StringDrawer different from that? Why does it need this extra layout pass?

Swift UITableView can't calculate content height properly

I'm having this weird issue with UITableView that can't calculate it's content's height properly.
I have custom UITableView class that is embedded in another custom UITableView, I want it to auto-adjust it's height to fit content so I have already:
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return self.contentSize
}
And now when I use:
self.estimatedRowHeight = UITableView.automaticDimension // non-zero value like 40 isn't working either
self.rowHeight = UITableView.automaticDimension
the output is the frame that is not full height, when I turn "Scrolling enabled" in this TableView it's scrollable with full content (don't want that):
Now when I change
self.estimatedRowHeight = UITableView.automaticDimension
to:
self.estimatedRowHeight = 0
the output is exactly what I would want to have except the content text is cut...
Here's my CommentCell:
Console isn't showing any errors with autolayout in any case.
Do you maybe know what's going on? I have spent literally days trying to get those comments to work and that's the last thing I need.
If you need any more info please just tell me.
Edit:
If i change estimatedRowHeight to a large number for example 500 I get loads of empty space under cells:
So it looks like TableView can't fix the cell height to content. Maybe this will help someone.
Maybe it's about the textfield inside the CellView. Did you set it's Layout to wraps?
Also I would try to set it's intrinsic size value to 'placeholder' inside the Size Inspector.

TableView resizing parent view iOS

This is a problem that has been bugging me for quite some time.
Assume a view that holds a tableView with X items. The goal is to make that view resize so that it is as high as the contents of the tableView.
An approach
Calculate the contents of the tableView in total ( e.g if there are 5 rows and each is 50 units high, its just a multiplication matter ). Then set the tableView constrained at a 0 0 0 0 into the view and set the view height to 250.
This works well for fixed height cell sizes. However!
a) How would the problem be approached for dynamic height cells though with complex constraints in a scenario where resizing happens automatically and the tableHeightForRow is set to UITableViewAutomaticDimension?
b) An idea could be using tableView.contentSize. However when would we retrieve that value safely in order to set the parent view frame accordingly? Is that even possible?
Thanks everyone
If you have a UITableView subclass, you can set up a property observer on the contentSize like this:
override var contentSize: CGSize {
didSet {
// make delegate call or use some other mechanism to communicate size change to parent
}
}
The most straightforward approach to this in my opinion is to use Autolayout. If you take this approach, you can use the contentSize to automatically invalidate the intrinsicContentSize which is what autolayout uses to dynamically size elements (as long as they don't have higher priority placement constraints restricting or explicitly setting their size).
Something like this:
override var contentSize: CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
return contentSize
}
Then, just add your table view to your parent view hierarchy with valid placement constraints and a content hugging/compression resistance of required.

How to get a correct automatic row height calculation when modifying the layout inside a UITableViewCell's layoutSubviews() method?

I have a UITableView with cells that are variable in height, laid out with Auto Layout in Interface Builder (rowHeight = UITableViewAutomaticDimension). When the user rotates the device I perform some manual layout changes inside the cell that affect the intrinsicContentSize one of the cell's subviews. As these changes are dependent on the initial frame of that particular subview I can only apply these changes once the layout engine has resolved the constraints and applied the correct frames to all subviews for the new (rotated) layout. This happens inside the layoutSubviews() method. So to apply my manual changes I override that method and first call the super implementation.
override func layoutSubviews() {
// Call `super` implementation to let the layout engine do its job
// and apply the new frames to subviews after resolving constraints.
super.layoutSubviews()
// 🔄 Apply manual layout changes here
}
Now the problem is that my manual layout changes possibly also change the cell's height. Hence I need a way to inform the table view that a second layout pass is required for this cell before the (automatic) height calculation returns. (Otherwise the table view will just use the row height it computed before the manual layout changes.)
Usually you do this by invalidating the layout with a setNeedsLayout() call on the view that needs another layout pass. However, I cannot do this from inside the layoutSubviews() method because it would result in an infinite loop of layout passes and crash the app.
So I tried to figure out a way if the layout had changed in this particular call of layoutSubviews() and came up with this approach:
override func layoutSubviews() {
// Get old image view size before layout pass
let previousImageSize = imageView.frame.size
// Run a layout pass
super.layoutSubviews()
// Get the new image view size
let newImageSize = imageView.frame.size
// Only trigger a new layout pass if the size changed
if newImageSize != previousImageSize {
// 🔄 Apply manual layout changes here
// Trigger another layout pass
setNeedsLayout()
}
}
(In this example, the imageView is a subview of the cell on which my layout depends.)
When running this code I realized that the code inside the if branch is never executed, i.e. the imageView size never changes inside the layoutSubviews() method. But it does change - only at a different time.
That's inconsistent to my understanding Auto Layout in general:
A view is always responsible for laying out its subviews, i.e. for setting their frames appropriately and this is done in layoutSubviews().
So here are my questions:
Why does the system change the image view's size from outside the layoutSubviews() method?
and more importantly:
How can I get the table view to run a second layout pass when computing the row height for a cell?

Unable to get the height of a UILabel with numberOfLines = 0

When I set a text to my label and check myLabel.frame.size.height I get always the same value, as if being always a single line, even if the text I set takes multiple lines... How could I get the "final" height?
Thanks
Try it inside your View Controller's viewDidLayoutSubviews function:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print(myLabel.frame.size.height)
}
Your label won't have it's final size until then.

Resources