Autoresizing UIView with UILabel inside, smoothly - uiview

I have a UIView, that contains a UILabel (and nothing else) inside it. I wish to expand and contract the view, making it look as though the label is expanding and contracting (via a button tap). I'm using a NSLayoutConstraint on the height of the view:
func labelExpansion() {
if (isExpanded) {
myViewConstraint.constant = shortLabelHeight
} else {
myViewConstraint.constant = longLabelHeight
}
UIView.animate(withDuration: 2.2, animations: {
self.view.layoutIfNeeded()
})
}
The problem is, the text in the label looks like it 'jumps'. As the label is resizing, the position of the text changes, until the animation is finished, when the label correctly redraws to the top of the view.
I have also tried removing the surrounding view, and adjusting the height constraint of the label alone; that was similarly jumpy.
How can I stop this jump during the animation, and fix the top of the label to the top of the view?

After trying a few things, I found an easy solution: set the Content Mode of the UILabel to 'Top'.
myLabel.contentMode = .top

Related

Animate height change UITableView but ignore UILabel?

Currently, I expand the height of my UITableViewCell to add a 45px bottom bar to the view. When I call tableView.beginUpdates() and tableView.endUpdates(), the height animates correctly, but it causes my text to appear to be re-drawn from the center of the cell to the top.
This is pretty jarring, because the text is actually in the same place after this bar is shown and it seems to be animating because a new bottom anchor for the UILabel is set. Is there any way to just animate the bottom bar and keep the UILabel in the same location? Attached is what is currently ocurring:
https://i.imgur.com/eMaD4nP.gifv
I have tried running it in an animation-less completion block like so:
UIView.performWithoutAnimation {
tableView.beginUpdates()
tableView.endUpdates()
}
which keeps the text in the same place, but does not animate the bottom bar at all (as was expected with this method). Ideally, I could achieve the same with the UILabel and not with the rest of the UITableViewCell.
Edit: Some of my AL code that is running before I do beginUpdates(). I am using a library called Anchorage which makes the AL syntax a lot easier to understand, but this is effectively making visual layout constraint language with a custom syntax
NSLayoutConstraint.deactivate(menuHeight)
menuHeight = batch {
menu.heightAnchor == CGFloat(45)
menu.horizontalAnchors == contentView.horizontalAnchors
menu.bottomAnchor == contentView.bottomAnchor
title.bottomAnchor == menu.topAnchor - CGFloat(8)
menu.topAnchor == title.bottomAnchor + CGFloat(8)
}

IOS/Objective-C: MakeUILabel Flow to Multiple Lines with Autolayout

I am trying to get a UILabel to flow to multiple lines and push the elements below it downwards using autolayout.
Here is my code called in viewwillappear.
self.myLabel.numberOfLines = 0;
self.myLabel.lineBreakMode = NSLineBreakByWordWrapping;
[_myLabel setContentCompressionResistancePriority:1000
forAxis:UILayoutConstraintAxisVertical];
[self.myLabel setNeedsDisplay];
[self.myLabel layoutIfNeeded];
[self.view setNeedsDisplay];
[self.view layoutIfNeeded];
The UILabel is constrained at the top to the content view and the image below it is constrained to the bottom of the UILabel. The label also has a height constraint of >=21.
On initial load, the label only shows one line.
The weird thing is that after launching a modal VC and the closing it, the label does go to multiple lines but fails to push down the elements below it.
On initial load.
After launching and canceling modal VC
I am wondering if the problem has something to do with timing of laying out subviews but have tried almost every thing.
Thanks in advance for any suggestions.
Simple example:
Create a new ViewController.
Add a button, constrain 40-pts from the top and centered horizontally
Add a UIView (green view). Constrain centered horizontally, width of 240, and top-space to button of 20.
Add two labels to the green view.
Top label, number of lines = 0, leading and trailing constraints of 16, top constraint of 8 (to green superview)
Bottom label, centered horizontally, top space to Top label of 8, bottom constraint of 8 (to green superview).
Connect the Top label to #IBOutlet var multiLineLabel: UILabel! in the view controller.
Connect the button touch-up-inside to the #IBAction func didTap(_ sender: Any) function in the view controller.
NO CHANGES to priorities.
Run the app. Each tap of the button will add text to the top / multi-line label, and it will "push down" the bottom label, which will, in turn, "push down" the bottom edge of the green view.
class ExpandingLabelViewController: UIViewController {
#IBOutlet var multiLineLabel: UILabel!
#IBAction func didTap(_ sender: Any) {
if let s = multiLineLabel.text {
multiLineLabel.text = s + " Here is some more text."
}
}
}
Once you have that working, you can add other elements to the view. Just make sure you have a "chain" of vertical spacing constraints, with the top-most element constrained to the top of the green superview, and the bottom-most element constrained to the bottom of the green superview.

Set label text in navigation title when scrolling

In the storyboard, I have a label and many other objects in a UIScrollView. (The UIScrollView is in the view).
I want to set the navigation bar's title to the title of the label when the label scrolls past it.
I've tried this code but it doesn't work:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(self.lbl.frame.origin.x==60) {
self.navigationController!.navigationBar.topItem!.title = lbl.text
}
}
What should i do? I am new to Swift.
First off, I suspect you're actually trying to find the label's y value (i.e. vertical offset) as opposed to its x value, i.e. self.lbl.frame.origin.x==60.
Secondly you don't want the label's frame since that CGRect represents the label's position within the scrollview (which won't change); instead, you want the label's position within the superview.
That said, try this:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if(scrollView.convert(lbl.frame.origin, to: self.view).y <= 60) {
navigationController!.navigationBar.topItem!.title = lbl.text
}
}
There are several problems with your code:
self.lbl.frame.origin.x==60 What are the chances that this will ever be true? That's like flipping a coin and hoping it lands on its edge. The top of the label will never be exactly at a certain point.
The label's origin will never change; scrolling a scroll view doesn't change the frame of its subviews. You need to look at the scroll view's content offset (i.e. how far is it scrolled).
You need to complete your tests. What should happen when the user scrolls back the other way? Also, do you want to change the navigation item title repeatedly, every instant the user is scrolling, or just once?

ContentInset of scrollView

This is my view, to the left is the view when keyboard is not showing and to the right is what I want to achieve when keyboard is showing.
All my fields are added to a scrollView that I then set constraints like this, field1 topConstraint is set to scrollView top and field4 bottomConstraint is set to scrollView bottom. all other fields are attached to the one below.
As you can see from the right most mockup, when keyboard is shown I want to readjust the view like that. So what i'm doing now is:
func keyboardWillShow(notification: NSNotification) {
self.field2.snp_updateConstraints { make -> Void in
make.bottom.equalTo(self.field3.snp_top).offset(8)
}
with this i adjust the large space between field2 and field3, so far so good. I then go on to set the contentInset:
let contentInset = UIEdgeInsetsMake(0, 0,getKeyboardSize(notification).height, 0)
self.scrollView.contentInset = contentInset
self.scrollView.scrollIndicatorInsets = contentInset
but the scrollView is not adjust the way I want because field3 is still hiding half behind the keyboard.
The reason for making this post is that I clearly don't understand how contentInsets is working with scrollView and I would like to avoid setting contentOffset manually, if thats even possible.
There seems to be a problem with the constraints i'm setting. Current constraint for field4 is that i set it to be tied to the view and not scrollView bottom and I'm setting each edge of the scrollView to be tied to the view.
Updates:
If i set field4 bottom constraint to be tied to the scrollView, the scrollViews height is 0, How can it not know what size it should be if I just before set the scrollView to be as big as the view itself? This has really confused me..
Updates 2
I'm setting the scrollView constraints like this:
self.scrollView.snp_makeConstraints { make -> Void in
make.top.equalTo(self.snp_topLayoutGuideBottom)
make.left.right.equalTo(self.view)
make.bottom.equalTo(self.snp_bottomLayoutGuideBottom)
}
And from the UIDebugger I can see that the height of the scrollView is the same but the Y coordinate has changed to 20, but I can still not scroll in it?? Also when debugging I can set the scrollViews contentSize to be greater then the frame, that means it should be scrollable, but it isn't.
Update 3
I have came to the understanding now that Field1 and Field4 both need to be attached to scrollViews top and bottom. Therefore there is a difference between the scrollViews frame and scrollViews contentSize because it's works as intending when contentSize > frame the scrollView is scrollable.
So this is big step forward for me and the only weird thing now is the contentInset.
My understanding with the contentInset is that you specify how much the view should shrink in either direction. So as it's is now i'm doing the inset with the height of the keyboard on the bottom of the scrollView but it's not scrolling, but when I'm setting the top contentInset to some random negative digit like -100 it's scrolls, why is this behaviour not achieved when the bottom inset is adjusted?
So I solved this with the following:
I made sure that Field1 and Field4 constraints was set to the scrollview, this way the scrollView was automatically able to calculate the height, and also made it possible to scroll if some content was outside the view. I then calculated the visible frame from the keyboard top and frame.
func keyboardWillShow(notification: NSNotification) {
self.Field2.snp_updateConstraints { make -> Void in
make.bottom.equalTo(self.Field3.snp_top)
}
self.Field3.snp_updateConstraints { make -> Void in
make.bottom.equalTo(self.Field4.snp_top)
}
self.view.layoutIfNeeded()
let contentInset = UIEdgeInsetsMake(0, 0,getKeyboardSize(notification).height + 4, 0)
let visibleRect = UIEdgeInsetsInsetRect(self.scrollView.frame, contentInset)
let scrollToOffset = max(0,self.Field4.frame.maxY - visibleRect.height)
self.scrollView.setContentOffset(CGPoint(x: 0, y: scrollToOffset), animated: true)
}
The key was after changing the constraints, I needed to invalidate the current layout and layout the views again, that way we calculations where the updated constraints where could be done correctly.

iOS Centring UITableViewCell label with disclosure indicator

I am looking for a way to enter a label within a table cell that also has a disclosure indicator. The problem i'm having at the moment is that it seems like the disclosure indicator is being ignored when calculating the label's positions
Heres a picture:
So as you can see the label is centred in the area between the left side of the cell and the left side of the indicator, if it was centred in the cell it would sit below the nav bar heading.
Any help is appreciated thankyou
From within the storyboard
Okay, first an explanation for your issue. It has to do with the anatomy of a UITableViewCell. With anatomy, I mean the fact that the UITableViewCell for you is just a container for another container, which is the contentView (you can also see this one in your storyboard).
When you are operating in Storyboards, you are solely operating on the contentView, not on the actual UITableViewCell. So, when you setup your UILabel to be centered on the X-axis with AutoLayout, AutoLayout will always try to center it within the contentView, not in the outer container (i.e. the UITableViewCell). Then, when you add a disclosure indicator to the UITableViewCell, the contentView automatically gets shrinked in its width because the cell makes space for the disclosure indicator and wants to prevent you from adding UI elements in the right area that is reserved for the disclosure indicator.
Now, you have a few options around this:
you can edit the constraint directly and add a constant to it (which has to be the same value that the label gets shifted when you'd remove the indicator)
don't use the default disclosure indicator (i.e. don't tick the checkbox in Storyboards) and just add a UIImageView with an image that looks identical.
To not be bound to any constants you can calculate the difference in widths of frame and contentView.frame. So first create an outlet collection like so:
#property (strong, nonatomic) IBOutletCollection(NSLayoutConstraint) NSArray *centerConstraintsToOffset;
Then add the center constraints that you want to be centered horizontally in cell to that outlet collection:
And finally add this code to your cell:
override func layoutSubviews() {
super.layoutSubviews()
for constraint in centerConstraintsToOffset {
constraint.constant = (frame.size.width - contentView.frame.size.width) / 2.0
}
}
This also gives you flexibility of adding or removing cell accessories on the go, and your views will always be perfectly center aligned. Even if you remove the accessory at all.
Pavel's answer fixed the issue for me. After creating the IBOutlet collection as his answer demonstrates, here is his code example edited for Swift 3:
override func layoutSubviews() {
super.layoutSubviews()
for constraint: NSLayoutConstraint in self.centerConstraintsToOffset {
constraint.constant = (frame.size.width - contentView.frame.size.width) / 2.0
}
}
As you've already noticed, adding a 'UITableViewCellAccessoryDisclosureIndicator' will shrink the space allotted for your cell's contentView. Another solution that doesn't require a custom indicator or guessing at an offset would be to programmatically add a UILabel to the root view of the cell, not the contentView. For example:
#property UILabel *label;
// ...
- (void)layoutSubviews
{
[super layoutSubviews];
[self.label removeFromSuperview];
self.label = [[UILabel alloc] init];
self.label.text = #"Motorsport";
[self.label sizeToFit];
label.center = CGPointMake(self.center.x, self.size.height/2);
[self addSubview:self.label];
}

Resources