I have a UITableViewCell containing a UITextView and a UIImageView. The text view has scrolling disabled. The UITableViewCell does use auto layout to calculate its height to fit the content.
If there is more text than in the screenshot I want the text to flow around the image and use the space below it too. Therefore I use exclusion path on the text view. The exclusion Path gets set correctly and the text flows around the image.
Exclusion paths must be set after auto layout has calculated the frames. As the exclusion path blocks some view space on the text view the containing text does not fit any more in the height auto layout calculated for the text view.
How can I make this work together with auto layout?
I tried to set a height constraint on the text view after setting exclusion paths:
let size = textView.sizeThatFits(CGSize(width: textView.frame.size.width, height: CGFloat(MAXFLOAT)))
textView.heightAnchor.constraint(equalToConstant: size.height).isActive = true
This does not help. I need to call layoutSubviews() on the UITableViewCell to force it to recalculate its height. However this will create an endless loop as I set my exclusion paths in viewDidLayoutSubviews().
I found that UITextView does NOT recalculate intrinsicContentSize when isScrollEnabled is false and textContainer.exclusionPaths changes.
The solution was simple:
class FixedTextView : UITextView {
func setExclusionPaths(_ paths: [UIBezierPath]) {
textContainer.exclusionPaths = paths
if !isScrollEnabled {
invalidateIntrinsicContentSize()
}
}
}
I had a similar issue where I couldn't feasibly access viewDidLayoutSubviews() without a lot of excessive coupling, so I feel your pain.
Suppose you have view A and the subviews include a UITextView.
In view A, override layoutSubviews()
override public func layoutSubviews() {
super.layoutSubviews()
// At this point, UITextView is laid out and has a valid frame.
// Set exclusion paths here, now that you have enough info to set coordinates.
super.layoutSubviews() // do it again.
}
This is similar to the approach taken with UILabel and maxPreferredWidth on prior iOS versions.
iOS: Multi-line UILabel in Auto Layout
One caveat: I don't know that setting a constraint in here will help you, as the layout cycles don't really work that way, but according to this Update Constraints of Cell Subview , you might be able to get away with doing layoutIfNeeded().
Related
I'm having a problem with my table view cells as they do not adjust automatically with its content.
I have a label for a title and another label for a name. There is a text view below the two labels which is never displayed when the simulator runs.
This is
what the Table View Cell is supposed to look like, however, this is what the Table View Cell displays.
I have pinned all elements inside the table view cell to the content view using constraints. I read up that adjusting the table view cell height itself will not work, so, I have to adjust the height from the table view itself.
It is set to automatic but it is not adjusting as seen here. I have also tried to set the estimated height to automatic but to no avail. The only solution was to set a custom height but it would look extremely weird if the text view contains only a few text as there would be a large white space. I did not add any code at all to adjust the size.
These are the following constraints:
Table View
Name Label
Title Label
Text View
First You need to add height constraint for textview and add its IBOUTlet then you need to override the updateconstraint of cell and do following in update constraints method.
override func updateConstraints() {
super.updateConstraints()
self.textViewHeightConstraint.constant = self.textView.contentSize.height
}
and also for name label add bottom constraint.
By default the UITextView will not resize itself to fit its content. While you could use #Waqas Sultan approach, I would recommend to let the textView size itself. To achieve that, just use this:
textView.isScrollEnabled = false
Or, in storyboards, find the Scroll Enabled attributed and uncheck it.
This would make textView to size itself.
However, from the constraints you show it is hard to tell if there are really enough constraints to determine the proper frames for all the content - there are constraints related to Review label, but who knows how you constrained that label.
Not seeing all the relevant constraints in the tableView cell, I cannot guarantee that this will be enough to make it work as you expect (you might have forgotten about just a single one constraint, and it might be messing up your whole layout).
Hey buddy i would like you to try this way.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == desiredIndexPath { // the index where you want automatic dimension
return UITableViewAutomaticDimension
} else {
return 100 // the height of every other cell.
}
}
note: Make sure that you do not give height to the label. Otherwise the label wont expand according to content.
First of all this is not a question about how to automatically size the cells inside the tableview, moreover how to automatically resize the entire tableview.
So I have a scrollview which has a tableview and 2 other views as its subviews. The tableview's cells already automatically resize itself, again this question is not about the individual cells. However, the tableview does not resize at all.
What I have done:
1) Set up the tableview to have a top, bottom, leading and trailing constraint
2) Set the cells up to have auto layout enabled
3) * I do not know the cell size at build time
4) I have disabled scrolling mode on tableview
So long story short, how can I go along to get the tableview to resize itself?
Edit
The cells contain a label which can have various lines of text, so therefore the cells, which use auto layout, should then determine the height of the table view.
The following images show how the view is set up:
As you can see the tableview is only a small part of the view and since the scrollview from the tableview is deactivated there should, and aren't, any scrolling problems.
EDIT 2
This is actually how it should end however, i am calculating this on my own and everytime I want to make a small change to the cells the whole code, which calculates the height of the cell, needs to be rewritten and it is quite difficult for me to get the height just right.
Edit 3
Until now I had a height constraint on the tableview which I calculated manually, however removing this constraint and trying to let auto layout handle the tableview height size creates the following error:
Scroll View
Need constraint for: Y position or height
I can conclude therefore that the tableview does not know how to automatically calculate the height based on its cells with autolayout.
You don't need to create a height constraint or set frame whatsoever. Create a subclass of UITableView and recalculate its intrinsicContentSize every time its contentSize changes aka new data added or removed. Here is all you needed:
class SelfSizingTableView: UITableView {
override var contentSize: CGSize {
didSet {
invalidateIntrinsicContentSize()
setNeedsLayout()
}
}
override var intrinsicContentSize: CGSize {
let height = min(.infinity, contentSize.height)
return CGSize(width: contentSize.width, height: height)
}
}
You can change your UITableView's frame by using tableview.frame = CGRect(x: <some_x>, y: <some_y>, width: <some_width>, height: <some_height>)
If your UITableViewCells use auto layout then they should resize when the UITableView's frame changes.
I'm attempting to make a UIScrollView only allow zooming in the horizontal direction. The scroll view is setup with a pure autolayout approach. The usual approach is as suggested in a number of Stack Overflow answers (e.g. this one) and other resources. I have successfully used this in apps before autolayout existed. The approach is as follows: in the scroll view's content view, I override setTransform(), and modify the passed in transform to only scale in the x direction:
override var transform: CGAffineTransform {
get { return super.transform }
set {
var t = newValue
t.d = 1.0
super.transform = t
}
}
I also reset the scroll view's content offset so that it doesn't scroll vertically during the zoom:
func scrollViewDidZoom(scrollView: UIScrollView) {
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
}
This works very nicely when not using autolayout:
However, when using autolayout, the content offset ends up wrong during zooming. While the content view only scales in the horizontal direction, it moves vertically:
I've put an example project on GitHub (used to make the videos in this question). It contains two storyboards, Main.storyboard, and NoAutolayout.storyboard, which are identical except that Main.storyboard has autolayout turned on while NoAutolayout does not. Switch between them by changing the Main Interface project setting and you can see behavior in both cases.
I'd really rather not switch off autolayout as it solves a number of problems with implementing my UI in a much nicer way than is required with manual layout. Is there a way to keep the vertical content offset correct (that is, zero) during zooming with autolayout?
EDIT: I've added a third storyboard, AutolayoutVariableColumns.storyboard, to the sample project. This adds a text field to change the number of columns in the GridView, which changes its intrinsic content size. This more closely shows the behavior I need in the real app that prompted this question.
Think I figured it out. You need to apply a translation in the transform to offset the centering UIScrollView tries to do while zooming.
Add this to your GridView:
var unzoomedViewHeight: CGFloat?
override func layoutSubviews() {
super.layoutSubviews()
unzoomedViewHeight = frame.size.height
}
override var transform: CGAffineTransform {
get { return super.transform }
set {
if let unzoomedViewHeight = unzoomedViewHeight {
var t = newValue
t.d = 1.0
t.ty = (1.0 - t.a) * unzoomedViewHeight/2
super.transform = t
}
}
}
To compute the transform, we need to know the unzoomed height of the view. Here, I'm just grabbing the frame size during layoutSubviews() and assuming it contains the unzoomed height. There are probably some edge cases where that's not correct; might be a better place in the view update cycle to calculate the height, but this is the basic idea.
Try setting translatesAutoresizingMaskIntoConstraints
func scrollViewDidZoom(scrollView: UIScrollView) {
gridView.translatesAutoresizingMaskIntoConstraints = true
}
In the sample project it works. If this is not sufficient, try creating new constraints programmatically in scrollViewDidEndZooming: might help.
Also, if this does not help, please update the sample project so we can reproduce the problem with variable intrinsicContentSize()
This article by Ole Begemann helped me a lot How I Learned to Stop Worrying and Love Cocoa Auto Layout
WWDC 2015 video
Mysteries of Auto Layout, Part 1
Mysteries of Auto Layout, Part 2
And so luckily, there's a flag for that. It's called translatesAutoResizingMask IntoConstraints [without space].
It's a bit of a mouthful, but it pretty much does what it says. It makes views behave the way that they did under the Legacy Layout system but in an Auto Layout world.
Although #Anna's solution works, UIScrollView provides a way of working with Auto Layout. But because scroll views works a little differently from other views, constraints are interpreted differently too:
Constraints between the edges/margins of scroll view and its contents attaches to the scroll view's content area.
Constraints between the height, width, or centers attach to the scroll view’s frame.
Constraints between scroll view and views outside scroll view works like an ordinary view.
So, when you add a subview to the scroll view pinned to its edges/margins, that subview becomes the scroll view's content area or content view.
Apple suggests the following approach in Working with Scroll Views:
Add the scroll view to the scene.
Draw constraints to define the scroll view’s size and position, as normal.
Add a view to the scroll view. Set the view’s Xcode specific label to Content View.
Pin the content view’s top, bottom, leading, and trailing edges to the scroll view’s corresponding edges. The content view now defines
the scroll view’s content area.
(...)
(...)
Lay out the scroll view’s content inside the content view. Use constraints to position the content inside the content view as normal.
In your case, the GridView must be inside the content view:
You can keep the grid view constraints that you have been using but, now, attached to content view. And for the horizontal-only zooming, keep the code exactly as it is. Your transform overriding handle it very well.
I'm not sure if this satisfies your requirement of 100% autolayout, but here's one solution, where the UIScrollView is set with autolayout and gridView added as a subview to it.
Your ViewController.swift should look like this:
// Add an outlet for the scrollView in interface builder.
#IBOutlet weak var scrollView: UIScrollView!
// Remove the outlet and view for gridView.
//#IBOutlet var gridView: GridView!
// Create the gridView here:
var gridView: GridView!
// Setup some views
override func viewDidLoad() {
super.viewDidLoad()
// The width for the gridView is set to 1000 here, change if needed.
gridView = GridView(frame: CGRect(x: 0, y: 0, width: 1000, height: self.view.bounds.height))
scrollView.addSubview(gridView)
scrollView.contentSize = CGSize(width: gridView.frame.width, height: scrollView.frame.height)
gridView.contentMode = .ScaleAspectFill
}
func viewForZoomingInScrollView(scrollView: UIScrollView) -> UIView? {
return gridView
}
func scrollViewDidZoom(scrollView: UIScrollView) {
scrollView.contentOffset = CGPoint(x: scrollView.contentOffset.x, y: 0.0)
scrollView.contentSize = CGSize(width: scrollView.contentSize.width, height: scrollView.frame.height)
}
A lot of the solutions I've seen here include changing the cell's background to an image and using sections for rows rather than just rows themselves. I'm looking to have only two sections and have each cell expand in height on tap, so neither of those solutions would work.
I saw one solution includes setting the frame of the cell in the layoutSubviews() function like so:
override func layoutSubviews() {
super.layoutSubviews()
self.frame = CGRectOffset(self.frame, 0, 10);
}
When I do this however, it only gives margin to one cell and that's only when I tap on the cell.
Is there a surefire way to add spacing in between UITableViewCells without being hacky and breaking the cell layouts in the process?
I did this yesterday pretty easily with auto layout.
I set the background of the cell and it's content view to clear, then I created a new view and setup constraints all around it and put my labels inside of it. The height changes dynamically based on the label so I needed to use UITableViewAutomaticDimension for the row height and give it an estimated row height as well.
I don't see why this wouldn't work for expanding it on a tap as well, you just might have to reload the cell.
make the cell and it's contentView transparent
contentView addSubview customContentView and layout your cell on customContentView
customContentView pin to contentView top leading trailing with offset 0 but pin to bottom with offset 10 //the margin height
I have a UIView that is a footerview of a uitableview. At run time, the user enters text into a uitextview within the footerview that should adjust to the size of the text content with a height constraint in autolayout.
All other objects in the view (labels, imageviews) have appropriate constraints to accommodate the expansion of the textview.
HOWEVER the height of the overall footerview will not change size, and it is impossible to use autolayout on the tableview footerview height.
Does anyone have a solution? Thanks
Haven't found an actual, elegant, solution yet, but I've postponed fixing this by using a workaround:
Setting the frame of the view used as a footer to be as large as you might possible need. In my case this meant giving it about 60px of spare vertical room. Since it's the footer and there's nothing below it to reposition the user won't be affected by the workaround.
The contents of the footer view are pinned to the top and have enough space to expand when needed.
For the record: my view is loaded from a nib file.
Although in theory the size one gives to the top level view in interface builder is just for design-time and the runtime size should be calculated based on constraints and the resulting intrinsic size, for this specific case I found the height stays the same as it was in IB.
We can change the height of the footer view run time by the following code:
func methodToChangeTableViewFooterHeight()
{
var footerView:UIView = self._tableView.tableFooterView! as UIView
var frame:CGRect = footerView.frame
frame.size.height = self.heightCollectionCS.constant + 10
footerView.frame = frame
self._tableView.tableFooterView! = footerView
}
Here , self.heightCollectionCS.constant is the height constraint for our Collection View.
We can use text content height on that place.
You may try to set again the footer view each time you footer height changes, to inform the table it should change the footer height. Or use inset. From within the footer view:
SetNeedsLayout()
LayoutIfNeeded()
ownertable.TableFooterView = this
Sorry about that, misread that question long ago. You can access the footer directly through the tableview's property tableFooterView.
What you could do is create your default footer in a xib or in your viewDidLoad:. Once you need to increase the size of the footer, you can pull out the UIView from that property and edit its frame if necessary to make it larger.
So make sure the tableFooterView gets assigned a UIView because it is nil by default. To just make the height taller, you can use self.tableView.tableFooterView.frame = CGRectMake(whatever rect you need);