Related
I made a button that adds some text to a textView and I want it to automatically scroll to the bottom as it is pressed so that user would be able to see the new text added.
I can't use this solution in Swift because I don't know Objective-C.
Does anyone know how can I scroll to the bottom of a textView in Swift? Thanks.
Tried both content offset and scrolltoview solutions, get mixed results and choppy scrolling; having looked around the below seemed to work and produce consistent scrolling to the bottom when needed.
In viewdidload:
self.storyTextView.layoutManager.allowsNonContiguousLayout = false
Then when needed:
let stringLength:Int = self.storyTextView.text.characters.count
self.storyTextView.scrollRangeToVisible(NSMakeRange(stringLength-1, 0))
Swift 4
let bottom = NSMakeRange(textLog.text.count - 1, 1)
textLog.scrollRangeToVisible(bottom)
Swift 3
let bottom = NSMakeRange(textLog.text.characters.count - 1, 1)
textLog.scrollRangeToVisible(bottom)
Update: thanks #AntoineRucquoy for Swift 4 reminder!
Simply, where myTextView is the UITextView in question:
let bottom = myTextView.contentSize.height
myTextView.setContentOffset(CGPoint(x: 0, y: bottom), animated: true) // Scrolls to end
So if you click the link you posted the accepted answer shows this objective-C code:
-(void)scrollTextViewToBottom:(UITextView *)textView
{
if(textView.text.length > 0 )
{
NSRange bottom = NSMakeRange(textView.text.length -1, 1);
[textView scrollRangeToVisible:bottom];
}
}
So your challenge is to convert that code to Swift.
Break it into pieces and tackle them one at a time. First, the method definition itself.
The method is called scrollTextViewToBottom. It takes a UITextView as a parameter, and does not return a result. How would you write that method definition in Swift?
Next look that the body of the method. The if statement should be exactly the same in Swift.
The creation of an NSRange is all but identical. You just need to change it a little bit:
let bottom = NSMakeRange(textView.text.length -1, 1)
The part that's probably the hardest for somebody who doesn't know Objective-C is the method call. It's sending the message scrollRangeToVisible to the object textView. The parameter passed is bottom. See if you can rewrite that line in Swift. Then put the whole thing together.
I use the following in an app that scrolls to the bottom automatically when text is added:
First when initializing your textView, do the following:
textView.layoutManager.allowsNonContiguousLayout = false
textView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)
Then add the following observer method:
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
var bottom = textView.contentSize.height - textView.frame.size.height
if bottom < 0 {
bottom = 0
}
if textView.contentOffset.y != bottom {
textView.setContentOffset(CGPoint(x: 0, y: bottom), animated: true)
}
}
setting allowsNonContiguousLayout to false fixed contentSize problems for me.
Adding the contentSize observer will observe for any new changes in the contentSize of the textView and call the -observeValue(forKeyPath...) function when changes are made.
In the -observeValue(...) function, we first get the bottom (y contentOffset when fully scrolled to the bottom). We then check if that value is negative, meaning that the contentSize height is smaller than the textView frame height and you can't really do any scrolling. If you try to programmatically scroll with that negative value, it will cause that infamous jitter that many people know and love. So to avoid this jitter we simply set the value to what it already should be, 0 or you can also just return.
Then we just test to see if the contentOffset doesn't already equal the bottom value, we give it that new value. This avoids setting the contentOffset when it doesn't need to be set.
Language:Swift
Follow steps as below:
//Declare
#IBOutlet weak var trTextDataRead: UITextView!
//Cunstom method
func insertTextView(text: String){
//Insert text
trTextDataRead.text.append(text)
//Scroll to the end
let btm = NSMakeRange(trTextDataRead.text.lengthOfBytes(using: String.Encoding.utf8), 0)
trTextDataRead.scrollRangeToVisible(btm)
}
If you're dealing with the UITextView's attributedText property:
in viewDidLoad()
self.storyTextView.layoutManager.allowsNonContiguousLayout = false
in your scrolling method
let stringLength:Int = self.storyTextView.attributedText.string.characters.count
self.storyTextView.scrollRangeToVisible(NSMakeRange(stringLength-1, 0))
Swift 4
private func textViewScrollToBottom() {
let bottomRange = NSMakeRange(self.myTextView.text.count - 1, 1)
self.myTextView.scrollRangeToVisible(bottomRange)
}
UITextView has a property contentOffsent. You can either set textView.contentOffset or textView.setContentOffset(offset, animated: true)
For example if the contentSize of your text view is (100, 500) but the height of the text view is only 100, then to scroll to the bottom, set the contentOffset property to (0, 400) (this is for a vertical text view). More generically the formula for scrolling to the bottom is textView.contentSize.height-textView.height. Every time your button is pressed, set the offset.
I would also really recommend reading the documentation and trying to figure it out. Swift and iOS is quite well documented and a question like this is easily searchable via Google.
Edit: This works because UITextView inherits from UIScrollView.
Sidenote: I wrote a UITextView subclass where you can set the vertical text alignment so if you set the text alignment to .Bottom, the text will align with the bottom of the view.
class TextView: UITextView {
enum VerticalAlignment: Int {
case Top = 0, Middle, Bottom
}
var verticalAlignment: VerticalAlignment = .Middle
//override contentSize property and observe using didSet
override var contentSize: CGSize {
didSet {
let textView = self
let height = textView.bounds.size.height
let contentHeight:CGFloat = contentSize.height
var topCorrect: CGFloat = 0.0
switch(self.verticalAlignment){
case .Top:
textView.contentOffset = CGPointZero //set content offset to top
case .Middle:
topCorrect = (height - contentHeight * textView.zoomScale)/2.0
topCorrect = topCorrect < 0 ? 0 : topCorrect
textView.contentOffset = CGPoint(x: 0, y: -topCorrect)
case .Bottom:
topCorrect = textView.bounds.size.height - contentHeight
topCorrect = topCorrect < 0 ? 0 : topCorrect
textView.contentOffset = CGPoint(x: 0, y: -topCorrect)
}
if contentHeight >= height { //if the contentSize is greater than the height
topCorrect = contentHeight - height //set the contentOffset to be the
topCorrect = topCorrect < 0 ? 0 : topCorrect //contentHeight - height of textView
textView.contentOffset = CGPoint(x: 0, y: topCorrect)
}
}
}
// MARK: - UIView
override func layoutSubviews() {
super.layoutSubviews()
let size = self.contentSize //forces didSet to be called
self.contentSize = size
}
}
In the above example (pulled directly from my subclass), you'll notice I make extensive use of the contentOffset property. I do some calculations to figure out where the offset should be based on the vertical alignment property and then set the content offset property according (which is how you programmatically scroll with a scroll view)
A lot of people are explaining how to scroll to the bottom, but one thing to note is that this won't work if you place it in viewDidLoad. For example: I needed to use this to scroll a log to the bottom when the page loaded. In order to do this, I had to implement the following code
- (void) viewDidLoad {
[super viewDidLoad];
[_logTextView setText:[Logger loadLogText]];
}
- (void) viewDidLayoutSubviews {
[_logTextView
setContentOffset:CGPointMake(
0.0,
_logTextView.contentSize.height
- _logTextView.frame.size.height
)
animated:NO
];
}
The actual scrolling of the UITextView cannot be done in viewDidLoad.
In my viewDidLoad implementation I set the text for the text box.
In my viewDidLayoutSubviews implementation I set the content offset for the UITextView by generating a CGPoint using the height of the text views content minus the height of the text view itself. This way, when it scrolls to the bottom, the bottom of the text is not at the top of the box and instead, the bottom of the text is at the bottom of the box.
Swift 5 - with extension to UITextView + avoid slow scroll in large texts (this issue killed my main Thread)
extension UITextView {
func scrollToBottom() {
// IMPORTANT - only use (text as NSString) to get the length, since text.length is O(N) and it will kill the main thread.
let length = (text as NSString).length
if length > 1 {
scrollRangeToVisible(NSMakeRange(length - 1, 1))
}
}
}
I'm porting an app from Objective-C into pure Swift and I'm facing strange problem.
I've got AlertView class which is replacement for standard UIAlertView (now UIAlertController) for displaying animatable popups. AlertView is created in UIViewController extension - it's inited with view controller's view frame and added as a subview.
AlertView has a property which is a PopupView's instance - custom UIView subclass with xib (on Autolayout). This popup should has dynamic height depends on its contents (multiline message label).
Now when I'm trying to animate this popup in AlertView class:
when I set in PopupView setTranslatesAutoresizingMaskIntoConstraints(false) - view's height is correct but setting its frame in animation doesn't work as expected - view is sticked to the top left corner
when I set setTranslatesAutoresizingMaskIntoConstraints(true) - animation works as expected BUT view has a size from xib (won't expand according to contents)
What can be wrong here?
EDIT
Showing popup method:
private func showPopup(popupView: PopupView)
{
var beginFrame = popupView.frame
beginFrame.origin.y = -beginFrame.size.height
beginFrame.origin.x = self.bounds.size.width/2 - beginFrame.width/2
popupView.frame = beginFrame
var endFrame = beginFrame
endFrame.origin.y = self.bounds.size.height/2 - endFrame.size.height/2
popupView.hidden = false
DLog(beginFrame)
UIView.animateWithDuration(kAnimationTime, delay: 0, usingSpringWithDamping: kAnimationDamping, initialSpringVelocity: kAnimationSpringVelocity, options: UIViewAnimationOptions.CurveEaseIn, animations:
{ () -> Void in
DLog(endFrame)
popupView.frame = endFrame
}, completion: nil)
}
in both cases it shows in console:
(72.5, -155.0, 230.0, 155.0)
(72.5, 256.0, 230.0, 155.0)
EDIT2
setTranslatesAutoresizingMaskIntoConstraints(false)
setTranslatesAutoresizingMaskIntoConstraints(true)
Ok, got solution. I've stopped mixing autolayout and direct frame modifications and use pure autolayout instead.
I want to use AutoLayout to size and layout a view in a manner that is reminiscent of UIImageView's aspect-fit content mode.
I have a subview inside a container view in Interface Builder. The subview has some inherent aspect ratio which I wish to respect. The container view's size is unknown until runtime.
If the container view's aspect ratio is wider than the subview, then I want the subview's height to equal the parent view's height.
If the container view's aspect ratio is taller than the subview, then I want the subview's width to equal the parent view's width.
In either case I wish the subview to be centered horizontally and vertically within the container view.
Is there a way to achieve this using AutoLayout constraints in Xcode 6 or in previous version? Ideally using Interface Builder, but if not perhaps it is possible to define such constraints programmatically.
You're not describing scale-to-fit; you're describing aspect-fit. (I have edited your question in this regard.) The subview becomes as large as possible while maintaining its aspect ratio and fitting entirely inside its parent.
Anyway, you can do this with auto layout. You can do it entirely in IB as of Xcode 5.1. Let's start with some views:
The light green view has an aspect ratio of 4:1. The dark green view has an aspect ratio of 1:4. I'm going to set up constraints so that the blue view fills the top half of the screen, the pink view fills the bottom half of the screen, and each green view expands as much as possible while maintaining its aspect ratio and fitting in its container.
First, I'll create constraints on all four sides of the blue view. I'll pin it to its nearest neighbor on each edge, with a distance of 0. I make sure to turn off margins:
Note that I don't update the frame yet. I find it easier to leave room between the views when setting up constraints, and just set the constants to 0 (or whatever) by hand.
Next, I pin the left, bottom, and right edges of the pink view to its nearest neighbor. I don't need to set up a top edge constraint because its top edge is already constrained to the bottom edge of the blue view.
I also need an equal-heights constraint between the pink and blue views. This will make them each fill half the screen:
If I tell Xcode to update all the frames now, I get this:
So the constraints I've set up so far are correct. I undo that and start work on the light green view.
Aspect-fitting the light green view requires five constraints:
A required-priority aspect ratio constraint on the light green view. You can create this constraint in a xib or storyboard with Xcode 5.1 or later.
A required-priority constraint limiting the width of the light green view to be less than or equal to the width of its container.
A high-priority constraint setting the width of the light green view to be equal to the width of its container.
A required-priority constraint limiting the height of the light green view to be less than or equal to the height of its container.
A high-priority constraint setting the height of the light green view to be equal to the height of its container.
Let's consider the two width constraints. The less-than-or-equal constraint, by itself, is not sufficient to determine the width of the light green view; many widths will fit the constraint. Since there's ambiguity, autolayout will try to choose a solution that minimizes the error in the other (high-priority but not required) constraint. Minimizing the error means making the width as close as possible to the container's width, while not violating the required less-than-or-equal constraint.
The same thing happens with the height constraint. And since the aspect-ratio constraint is also required, it can only maximize the size of the subview along one axis (unless the container happens to have the same aspect ratio as the subview).
So first I create the aspect ratio constraint:
Then I create equal width and height constraints with the container:
I need to edit these constraints to be less-than-or-equal constraints:
Next I need to create another set of equal width and height constraints with the container:
And I need to make these new constraints less than required priority:
Finally, you asked for the subview to be centered in its container, so I'll set up those constraints:
Now, to test, I'll select the view controller and ask Xcode to update all the frames. This is what I get:
Oops! The subview has expanded to completely fill its container. If I select it, I can see that in fact it's maintained its aspect ratio, but it's doing an aspect-fill instead of an aspect-fit.
The problem is that on a less-than-or-equal constraint, it matters which view is at each end of the constraint, and Xcode has set up the constraint opposite from my expectation. I could select each of the two constraints and reverse its first and second items. Instead, I'll just select the subview and change the constraints to be greater-than-or-equal:
Xcode updates the layout:
Now I do all the same things to the dark green view on the bottom. I need to make sure its aspect ratio is 1:4 (Xcode resized it in a weird way since it didn't have constraints). I won't show the steps again since they're the same. Here's the result:
Now I can run it in the iPhone 4S simulator, which has a different screen size than IB used, and test rotation:
And I can test in in the iPhone 6 simulator:
I've uploaded my final storyboard to this gist for your convenience.
Rob, your answer is awesome!
I also know that this question is specifically about achieving this by using auto-layout. However, just as a reference, I'd like to show how this can be done in code. You set up the top and bottom views (blue and pink) just like Rob showed. Then you create a custom AspectFitView:
AspectFitView.h:
#import <UIKit/UIKit.h>
#interface AspectFitView : UIView
#property (nonatomic, strong) UIView *childView;
#end
AspectFitView.m:
#import "AspectFitView.h"
#implementation AspectFitView
- (void)setChildView:(UIView *)childView
{
if (_childView) {
[_childView removeFromSuperview];
}
_childView = childView;
[self addSubview:childView];
[self setNeedsLayout];
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (_childView) {
CGSize childSize = _childView.frame.size;
CGSize parentSize = self.frame.size;
CGFloat aspectRatioForHeight = childSize.width / childSize.height;
CGFloat aspectRatioForWidth = childSize.height / childSize.width;
if ((parentSize.height * aspectRatioForHeight) > parentSize.height) {
// whole height, adjust width
CGFloat width = parentSize.width * aspectRatioForWidth;
_childView.frame = CGRectMake((parentSize.width - width) / 2.0, 0, width, parentSize.height);
} else {
// whole width, adjust height
CGFloat height = parentSize.height * aspectRatioForHeight;
_childView.frame = CGRectMake(0, (parentSize.height - height) / 2.0, parentSize.width, height);
}
}
}
#end
Next, you change the class of the blue and pink views in the storyboard to be AspectFitViews. Finally you set two outlets to your viewcontroller topAspectFitView and bottomAspectFitView and set their childViews in viewDidLoad:
- (void)viewDidLoad {
[super viewDidLoad];
UIView *top = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 500, 100)];
top.backgroundColor = [UIColor lightGrayColor];
UIView *bottom = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 500)];
bottom.backgroundColor = [UIColor greenColor];
_topAspectFitView.childView = top;
_bottomAspectFitView.childView = bottom;
}
So it's not hard to do this in code and it is still very adaptable and works with variably-sized views and different aspect ratios.
Update July 2015: Find a demo app here: https://github.com/jfahrenkrug/SPWKAspectFitView
This is for macOS.
I have problem to use Rob's way to achieve aspect fit on the OS X application. But I made it with another way -- Instead of using width and height, I used leading, trailing, top and bottom space.
Basically, add two leading spaces where one is >= 0 #1000 required priority and another one is = 0 #250 low priority. Do same settings to trailing, top and bottom space.
Of course, you need to set aspect ratio and centre X and centre Y.
And then job's done!
I needed a solution from the accepted answer, but executed from the code. The most elegant way I've found is using Masonry framework.
#import "Masonry.h"
...
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.width.equalTo(view.mas_height).multipliedBy(aspectRatio);
make.size.lessThanOrEqualTo(superview);
make.size.equalTo(superview).with.priorityHigh();
make.center.equalTo(superview);
}];
I found myself wanting aspect-fill behavior so that a UIImageView would maintain its own aspect ratio and entirely fill the container view. Confusingly, my UIImageView was breaking BOTH high-priority equal-width and equal-height constraints (described in Rob's answer) and rendering at full resolution.
The solution was simply to set the UIImageView's Content Compression Resistance Priority lower than the priority of the equal-width and equal-height constraints:
This is a port of #rob_mayoff's excellent answer to a code-centric approach, using NSLayoutAnchor objects and ported to Xamarin. For me, NSLayoutAnchor and related classes have made AutoLayout much easier to program:
public class ContentView : UIView
{
public ContentView (UIColor fillColor)
{
BackgroundColor = fillColor;
}
}
public class MyController : UIViewController
{
public override void ViewDidLoad ()
{
base.ViewDidLoad ();
//Starting point:
var view = new ContentView (UIColor.White);
blueView = new ContentView (UIColor.FromRGB (166, 200, 255));
view.AddSubview (blueView);
lightGreenView = new ContentView (UIColor.FromRGB (200, 255, 220));
lightGreenView.Frame = new CGRect (20, 40, 200, 60);
view.AddSubview (lightGreenView);
pinkView = new ContentView (UIColor.FromRGB (255, 204, 240));
view.AddSubview (pinkView);
greenView = new ContentView (UIColor.Green);
greenView.Frame = new CGRect (80, 20, 40, 200);
pinkView.AddSubview (greenView);
//Now start doing in code the things that #rob_mayoff did in IB
//Make the blue view size up to its parent, but half the height
blueView.TranslatesAutoresizingMaskIntoConstraints = false;
var blueConstraints = new []
{
blueView.LeadingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.LeadingAnchor),
blueView.TrailingAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TrailingAnchor),
blueView.TopAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.TopAnchor),
blueView.HeightAnchor.ConstraintEqualTo(view.LayoutMarginsGuide.HeightAnchor, (nfloat) 0.5)
};
NSLayoutConstraint.ActivateConstraints (blueConstraints);
//Make the pink view same size as blue view, and linked to bottom of blue view
pinkView.TranslatesAutoresizingMaskIntoConstraints = false;
var pinkConstraints = new []
{
pinkView.LeadingAnchor.ConstraintEqualTo(blueView.LeadingAnchor),
pinkView.TrailingAnchor.ConstraintEqualTo(blueView.TrailingAnchor),
pinkView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor),
pinkView.TopAnchor.ConstraintEqualTo(blueView.BottomAnchor)
};
NSLayoutConstraint.ActivateConstraints (pinkConstraints);
//From here, address the aspect-fitting challenge:
lightGreenView.TranslatesAutoresizingMaskIntoConstraints = false;
//These are the must-fulfill constraints:
var lightGreenConstraints = new []
{
//Aspect ratio of 1 : 5
NSLayoutConstraint.Create(lightGreenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, lightGreenView, NSLayoutAttribute.Width, (nfloat) 0.20, 0),
//Cannot be larger than parent's width or height
lightGreenView.WidthAnchor.ConstraintLessThanOrEqualTo(blueView.WidthAnchor),
lightGreenView.HeightAnchor.ConstraintLessThanOrEqualTo(blueView.HeightAnchor),
//Center in parent
lightGreenView.CenterYAnchor.ConstraintEqualTo(blueView.CenterYAnchor),
lightGreenView.CenterXAnchor.ConstraintEqualTo(blueView.CenterXAnchor)
};
//Must-fulfill
foreach (var c in lightGreenConstraints)
{
c.Priority = 1000;
}
NSLayoutConstraint.ActivateConstraints (lightGreenConstraints);
//Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
var lightGreenLowPriorityConstraints = new []
{
lightGreenView.WidthAnchor.ConstraintEqualTo(blueView.WidthAnchor),
lightGreenView.HeightAnchor.ConstraintEqualTo(blueView.HeightAnchor)
};
//Lower priority
foreach (var c in lightGreenLowPriorityConstraints)
{
c.Priority = 750;
}
NSLayoutConstraint.ActivateConstraints (lightGreenLowPriorityConstraints);
//Aspect-fit on the green view now
greenView.TranslatesAutoresizingMaskIntoConstraints = false;
var greenConstraints = new []
{
//Aspect ratio of 5:1
NSLayoutConstraint.Create(greenView, NSLayoutAttribute.Height, NSLayoutRelation.Equal, greenView, NSLayoutAttribute.Width, (nfloat) 5.0, 0),
//Cannot be larger than parent's width or height
greenView.WidthAnchor.ConstraintLessThanOrEqualTo(pinkView.WidthAnchor),
greenView.HeightAnchor.ConstraintLessThanOrEqualTo(pinkView.HeightAnchor),
//Center in parent
greenView.CenterXAnchor.ConstraintEqualTo(pinkView.CenterXAnchor),
greenView.CenterYAnchor.ConstraintEqualTo(pinkView.CenterYAnchor)
};
//Must fulfill
foreach (var c in greenConstraints)
{
c.Priority = 1000;
}
NSLayoutConstraint.ActivateConstraints (greenConstraints);
//Low priority constraint to attempt to fill parent as much as possible (but lower priority than previous)
var greenLowPriorityConstraints = new []
{
greenView.WidthAnchor.ConstraintEqualTo(pinkView.WidthAnchor),
greenView.HeightAnchor.ConstraintEqualTo(pinkView.HeightAnchor)
};
//Lower-priority than above
foreach (var c in greenLowPriorityConstraints)
{
c.Priority = 750;
}
NSLayoutConstraint.ActivateConstraints (greenLowPriorityConstraints);
this.View = view;
view.LayoutIfNeeded ();
}
}
Maybe this is the shortest answer, with Masonry, which also supports aspect-fill and stretch.
typedef NS_ENUM(NSInteger, ContentMode) {
ContentMode_aspectFit,
ContentMode_aspectFill,
ContentMode_stretch
}
// ....
[containerView addSubview:subview];
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
if (contentMode == ContentMode_stretch) {
make.edges.equalTo(containerView);
}
else {
make.center.equalTo(containerView);
make.edges.equalTo(containerView).priorityHigh();
make.width.equalTo(content.mas_height).multipliedBy(4.0 / 3); // the aspect ratio
if (contentMode == ContentMode_aspectFit) {
make.width.height.lessThanOrEqualTo(containerView);
}
else { // contentMode == ContentMode_aspectFill
make.width.height.greaterThanOrEqualTo(containerView);
}
}
}];
I couldn't find any ready-to-go fully programatical solution, so here is my take in swift 5 for the aspect fill extension to a view:
extension UIView {
public enum FillingMode {
case full(padding:Int = 0)
case aspectFit(ratio:CGFloat)
// case aspectFill ...
}
public func addSubview(_ newView:UIView, withFillingMode fillingMode:FillingMode) {
newView.translatesAutoresizingMaskIntoConstraints = false
addSubview(newView)
switch fillingMode {
case let .full(padding):
let cgPadding = CGFloat(padding)
NSLayoutConstraint.activate([
newView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: cgPadding),
newView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -cgPadding),
newView.topAnchor.constraint(equalTo: topAnchor, constant: cgPadding),
newView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -cgPadding)
])
case let .aspectFit(ratio):
guard ratio != 0 else { return }
NSLayoutConstraint.activate([
newView.centerXAnchor.constraint(equalTo: centerXAnchor),
newView.centerYAnchor.constraint(equalTo: centerYAnchor),
newView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
newView.leadingAnchor.constraint(equalTo: leadingAnchor).usingPriority(900),
newView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
newView.trailingAnchor.constraint(equalTo: trailingAnchor).usingPriority(900),
newView.topAnchor.constraint(greaterThanOrEqualTo: topAnchor),
newView.topAnchor.constraint(equalTo: topAnchor).usingPriority(900),
newView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor),
newView.bottomAnchor.constraint(equalTo: bottomAnchor).usingPriority(900),
newView.heightAnchor.constraint(equalTo: newView.widthAnchor, multiplier: CGFloat(ratio)),
])
}
}
}
And here is the priority extension (comes from another thread but I "secured it" with some pattern matching between 1...1000:
extension NSLayoutConstraint {
/// Returns the constraint sender with the passed priority.
///
/// - Parameter priority: The priority to be set.
/// - Returns: The sended constraint adjusted with the new priority.
func usingPriority(_ priority: Int) -> NSLayoutConstraint {
self.priority = UILayoutPriority( (1...1000 ~= priority) ? Float(priority) : 1000 )
return self
}
}
Hope it helps ~
I'm updating an old app with an AdBannerView and when there is no ad, it slides off screen. When there is an ad it slides on the screen. Basic stuff.
Old style, I set the frame in an animation block.
New style, I have a IBOutlet to the auto-layout constraint which determines the Y position, in this case it's distance from the bottom of the superview, and modify the constant:
- (void)moveBannerOffScreen {
[UIView animateWithDuration:5 animations:^{
_addBannerDistanceFromBottomConstraint.constant = -32;
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
[UIView animateWithDuration:5 animations:^{
_addBannerDistanceFromBottomConstraint.constant = 0;
}];
bannerIsVisible = TRUE;
}
And the banner moves, exactly as expected, but no animation.
UPDATE: I re-watched WWDC 12 talk Best Practices for Mastering Auto Layout which covers animation. It discusses how to update constraints using CoreAnimation:
I've tried with the following code, but get the exact same results:
- (void)moveBannerOffScreen {
_addBannerDistanceFromBottomConstraint.constant = -32;
[UIView animateWithDuration:2 animations:^{
[self.view setNeedsLayout];
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
_addBannerDistanceFromBottomConstraint.constant = 0;
[UIView animateWithDuration:2 animations:^{
[self.view setNeedsLayout];
}];
bannerIsVisible = TRUE;
}
On a side note, I have checked numerous times and this is being executed on the main thread.
Two important notes:
You need to call layoutIfNeeded within the animation block. Apple actually recommends you call it once before the animation block to ensure that all pending layout operations have been completed
You need to call it specifically on the parent view (e.g. self.view), not the child view that has the constraints attached to it. Doing so will update all constrained views, including animating other views that might be constrained to the view that you changed the constraint of (e.g. View B is attached to the bottom of View A and you just changed View A's top offset and you want View B to animate with it)
Try this:
Objective-C
- (void)moveBannerOffScreen {
[self.view layoutIfNeeded];
[UIView animateWithDuration:5
animations:^{
self._addBannerDistanceFromBottomConstraint.constant = -32;
[self.view layoutIfNeeded]; // Called on parent view
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
[self.view layoutIfNeeded];
[UIView animateWithDuration:5
animations:^{
self._addBannerDistanceFromBottomConstraint.constant = 0;
[self.view layoutIfNeeded]; // Called on parent view
}];
bannerIsVisible = TRUE;
}
Swift 3
UIView.animate(withDuration: 5) {
self._addBannerDistanceFromBottomConstraint.constant = 0
self.view.layoutIfNeeded()
}
I appreciate the answer provided, but I think it would be nice to take it a bit further.
The basic block animation from the documentation
[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
// Make all constraint changes here
[containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];
but really this is a very simplistic scenario. What if I want to animate subview constraints via the updateConstraints method?
An animation block that calls the subviews updateConstraints method
[self.view layoutIfNeeded];
[self.subView setNeedsUpdateConstraints];
[self.subView updateConstraintsIfNeeded];
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionLayoutSubviews animations:^{
[self.view layoutIfNeeded];
} completion:nil];
The updateConstraints method is overridden in the UIView subclass and must call super at the end of the method.
- (void)updateConstraints
{
// Update some constraints
[super updateConstraints];
}
The AutoLayout Guide leaves much to be desired but it is worth reading. I myself am using this as part of a UISwitch that toggles a subview with a pair of UITextFields with a simple and subtle collapse animation (0.2 seconds long). The constraints for the subview are being handled in the UIView subclasses updateConstraints methods as described above.
Generally, you just need to update constraints and call layoutIfNeeded inside the animation block. This can be either changing the .constant property of an NSLayoutConstraint, adding remove constraints (iOS 7), or changing the .active property of constraints (iOS 8 & 9).
Sample Code:
[UIView animateWithDuration:0.3 animations:^{
// Move to right
self.leadingConstraint.active = false;
self.trailingConstraint.active = true;
// Move to bottom
self.topConstraint.active = false;
self.bottomConstraint.active = true;
// Make the animation happen
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}];
Sample Setup:
Controversy
There are some questions about whether the constraint should be changed before the animation block, or inside it (see previous answers).
The following is a Twitter conversation between Martin Pilkington who teaches iOS, and Ken Ferry who wrote Auto Layout. Ken explains that though changing constants outside of the animation block may currently work, it's not safe and they should really be change inside the animation block.
https://twitter.com/kongtomorrow/status/440627401018466305
Animation:
Sample Project
Here's a simple project showing how a view can be animated. It's using Objective C and animates the view by changing the .active property of several constraints.
https://github.com/shepting/SampleAutoLayoutAnimation
// Step 1, update your constraint
self.myOutletToConstraint.constant = 50; // New height (for example)
// Step 2, trigger animation
[UIView animateWithDuration:2.0 animations:^{
// Step 3, call layoutIfNeeded on your animated view's parent
[self.view layoutIfNeeded];
}];
Swift 4 solution
UIView.animate
Three simple steps:
Change the constraints, e.g.:
heightAnchor.constant = 50
Tell the containing view that its layout is dirty and that the autolayout should recalculate the layout:
self.view.setNeedsLayout()
In animation block tell the layout to recalculate the layout, which is equivalent of setting the frames directly (in this case the autolayout will set the frames):
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
Complete simplest example:
heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
Sidenote
There is an optional 0th step - before changing the constraints you might want to call self.view.layoutIfNeeded() to make sure that the starting point for the animation is from the state with old constraints applied (in case there were some other constraints changes that should not be included in animation):
otherConstraint.constant = 30
// this will make sure that otherConstraint won't be animated but will take effect immediately
self.view.layoutIfNeeded()
heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
UIViewPropertyAnimator
Since with iOS 10 we got a new animating mechanism - UIViewPropertyAnimator, we should know that basically the same mechanism applies to it. The steps are basically the same:
heightAnchor.constant = 50
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
self.view.layoutIfNeeded()
}
animator.startAnimation()
Since animator is an encapsulation of the animation, we can keep reference to it and call it later. However, since in the animation block we just tell the autolayout to recalculate the frames, we have to change the constraints before calling startAnimation. Therefore something like this is possible:
// prepare the animator first and keep a reference to it
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
self.view.layoutIfNeeded()
}
// at some other point in time we change the constraints and call the animator
heightAnchor.constant = 50
self.view.setNeedsLayout()
animator.startAnimation()
The order of changing constraints and starting an animator is important - if we just change the constraints and leave our animator for some later point, the next redraw cycle can invoke autolayout recalculation and the change will not be animated.
Also, remember that a single animator is non-reusable - once you run it, you cannot "rerun" it. So I guess there is not really a good reason to keep the animator around, unless we use it for controlling an interactive animation.
Swift solution:
yourConstraint.constant = 50
UIView.animate(withDuration: 1.0, animations: {
yourView.layoutIfNeeded
})
Storyboard, Code, Tips and a few Gotchas
The other answers are just fine but this one highlights a few fairly important gotchas of animating constraints using a recent example. I went through a lot of variations before I realized the following:
Make the constraints you want to target into Class variables to hold a strong reference. In Swift I used lazy variables:
lazy var centerYInflection:NSLayoutConstraint = {
let temp = self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
return temp!
}()
After some experimentation I noted that one MUST obtain the constraint from the view ABOVE (aka the superview) the two views where the constraint is defined. In the example below (both MNGStarRating and UIWebView are the two types of items I am creating a constraint between, and they are subviews within self.view).
Filter Chaining
I take advantage of Swift's filter method to separate the desired constraint that will serve as the inflection point. One could also get much more complicated but filter does a nice job here.
Animating Constraints Using Swift
Nota Bene - This example is the storyboard/code solution and assumes
one has made default constraints in the storyboard. One can then
animate the changes using code.
Assuming you create a property to filter with accurate criteria and get to a specific inflection point for your animation (of course you could also filter for an array and loop through if you need multiple constraints):
lazy var centerYInflection:NSLayoutConstraint = {
let temp = self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
return temp!
}()
....
Sometime later...
#IBAction func toggleRatingView (sender:AnyObject){
let aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)
self.view.layoutIfNeeded()
//Use any animation you want, I like the bounce in springVelocity...
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.75, options: [.CurveEaseOut], animations: { () -> Void in
//I use the frames to determine if the view is on-screen
if CGRectContainsRect(self.view.frame, self.ratingView.frame) {
//in frame ~ animate away
//I play a sound to give the animation some life
self.centerYInflection.constant = aPointAboveScene
self.centerYInflection.priority = UILayoutPriority(950)
} else {
//I play a different sound just to keep the user engaged
//out of frame ~ animate into scene
self.centerYInflection.constant = 0
self.centerYInflection.priority = UILayoutPriority(950)
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}) { (success) -> Void in
//do something else
}
}
}
The many wrong turns
These notes are really a set of tips that I wrote for myself. I did all the don'ts personally and painfully. Hopefully this guide can spare others.
Watch out for zPositioning. Sometimes when nothing is apparently
happening, you should hide some of the other views or use the view
debugger to locate your animated view. I've even found cases where a User Defined Runtime
Attribute was lost in a storyboard's xml and led to the animated
view being covered (while working).
Always take a minute to read the documentation (new and old), Quick
Help, and headers. Apple keeps making a lot of changes to better
manage AutoLayout constraints (see stack views). Or at least the AutoLayout Cookbook. Keep in mind that sometimes the best solutions are in the older documentation/videos.
Play around with the values in the animation and consider using
other animateWithDuration variants.
Don't hardcode specific layout values as criteria for determining
changes to other constants, instead use values that allow you to
determine the location of the view. CGRectContainsRect is one
example
If needed, don't hesitate to use the layout margins associated with
a view participating in the constraint definition
let viewMargins = self.webview.layoutMarginsGuide: is on example
Don't do work you don't have to do, all views with constraints on the
storyboard have constraints attached to the property
self.viewName.constraints
Change your priorities for any constraints to less than 1000. I set
mine to 250 (low) or 750 (high) on the storyboard; (if you try to change a 1000 priority to anything in code then the app will crash because 1000 is required)
Consider not immediately trying to use activateConstraints and
deactivateConstraints (they have their place but when just learning or if you are using a storyboard using these probably means your doing too much ~ they do have a place though as seen below)
Consider not using addConstraints / removeConstraints unless you are
really adding a new constraint in code. I found that most times I
layout the views in the storyboard with desired constraints (placing
the view offscreen), then in code, I animate the constraints previously created in the storyboard to move the view around.
I spent a lot of wasted time building up constraints with the new
NSAnchorLayout class and subclasses. These work just fine but it
took me a while to realize that all the constraints that I needed
already existed in the storyboard. If you build constraints in code
then most certainly use this method to aggregate your constraints:
Quick Sample Of Solutions to AVOID when using Storyboards
private var _nc:[NSLayoutConstraint] = []
lazy var newConstraints:[NSLayoutConstraint] = {
if !(self._nc.isEmpty) {
return self._nc
}
let viewMargins = self.webview.layoutMarginsGuide
let minimumScreenWidth = min(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height)
let centerY = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.webview.centerYAnchor)
centerY.constant = -1000.0
centerY.priority = (950)
let centerX = self.ratingView.centerXAnchor.constraintEqualToAnchor(self.webview.centerXAnchor)
centerX.priority = (950)
if let buttonConstraints = self.originalRatingViewConstraints?.filter({
($0.firstItem is UIButton || $0.secondItem is UIButton )
}) {
self._nc.appendContentsOf(buttonConstraints)
}
self._nc.append( centerY)
self._nc.append( centerX)
self._nc.append (self.ratingView.leadingAnchor.constraintEqualToAnchor(viewMargins.leadingAnchor, constant: 10.0))
self._nc.append (self.ratingView.trailingAnchor.constraintEqualToAnchor(viewMargins.trailingAnchor, constant: 10.0))
self._nc.append (self.ratingView.widthAnchor.constraintEqualToConstant((minimumScreenWidth - 20.0)))
self._nc.append (self.ratingView.heightAnchor.constraintEqualToConstant(200.0))
return self._nc
}()
If you forget one of these tips or the more simple ones such as where to add the layoutIfNeeded, most likely nothing will happen: In which case you may have a half baked solution like this:
NB - Take a moment to read the AutoLayout Section Below and the
original guide. There is a way to use these techniques to supplement
your Dynamic Animators.
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 1.0, options: [.CurveEaseOut], animations: { () -> Void in
//
if self.starTopInflectionPoint.constant < 0 {
//-3000
//offscreen
self.starTopInflectionPoint.constant = self.navigationController?.navigationBar.bounds.height ?? 0
self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
} else {
self.starTopInflectionPoint.constant = -3000
self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
}
}) { (success) -> Void in
//do something else
}
}
Snippet from the AutoLayout Guide (note the second snippet is for using OS X). BTW - This is no longer in the current guide as far as I can see. The preferred techniques continue to evolve.
Animating Changes Made by Auto Layout
If you need full control over animating changes made by Auto Layout, you must make your constraint changes programmatically. The basic concept is the same for both iOS and OS X, but there are a few minor differences.
In an iOS app, your code would look something like the following:
[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
// Make all constraint changes here
[containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];
In OS X, use the following code when using layer-backed animations:
[containterView layoutSubtreeIfNeeded];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[context setAllowsImplicitAnimation: YES];
// Make all constraint changes here
[containerView layoutSubtreeIfNeeded];
}];
When you aren’t using layer-backed animations, you must animate the constant using the constraint’s animator:
[[constraint animator] setConstant:42];
For those who learn better visually check out this early video from Apple.
Pay Close Attention
Often in documentation there are small notes or pieces of code that lead to bigger ideas. For example attaching auto layout constraints to dynamic animators is a big idea.
Good Luck and May the Force be with you.
Working Solution 100% Swift 5.3
i have read all the answers and want to share the code and hierarchy of lines which i have used in all my applications to animate them correctly, Some solutions here are not working, you should check them on slower devices e.g iPhone 5 at this moment.
self.btnHeightConstraint.constant = 110
UIView.animate(withDuration: 0.27) { [weak self] in
self?.view.layoutIfNeeded()
}
I was trying to animate Constraints and was not really easy to found a good explanation.
What other answers are saying is totally true: you need to call [self.view layoutIfNeeded]; inside animateWithDuration: animations:. However, the other important point is to have pointers for every NSLayoutConstraint you want to animate.
I created an example in GitHub.
Working and just tested solution for Swift 3 with Xcode 8.3.3:
self.view.layoutIfNeeded()
self.calendarViewHeight.constant = 56.0
UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
Just keep in mind that self.calendarViewHeight is a constraint referred to a customView (CalendarView). I called the .layoutIfNeeded() on self.view and NOT on self.calendarView
Hope this help.
There is an article talk about this:
http://weblog.invasivecode.com/post/42362079291/auto-layout-and-core-animation-auto-layout-was
In which, he coded like this:
- (void)handleTapFrom:(UIGestureRecognizer *)gesture {
if (_isVisible) {
_isVisible = NO;
self.topConstraint.constant = -44.; // 1
[self.navbar setNeedsUpdateConstraints]; // 2
[UIView animateWithDuration:.3 animations:^{
[self.navbar layoutIfNeeded]; // 3
}];
} else {
_isVisible = YES;
self.topConstraint.constant = 0.;
[self.navbar setNeedsUpdateConstraints];
[UIView animateWithDuration:.3 animations:^{
[self.navbar layoutIfNeeded];
}];
}
}
Hope it helps.
In the context of constraint animation, I would like to mention a specific situation where I animated a constraint immediately within a keyboard_opened notification.
Constraint defined a top space from a textfield to top of the container. Upon keyboard opening, I just divide the constant by 2.
I was unable to achieve a conistent smooth constraint animation directly within the keyboard notification. About half the times view would just jump to its new position - without animating.
It occured to me there might be some additional layouting happening as result of keyboard opening.
Adding a simple dispatch_after block with a 10ms delay made the animation run every time - no jumping.
Is it possible to animate the frame width of a UISearchBar? I find when I apply uiview animations to widen the bounds of a search bar it pops immediately to the final result as if the object internally is assuming control of how it animates and not allowing me to apply my own animations to it smoothly.
If I animate the position it moves smoothly, but I suspect the fact that the text input adjusts according to the presence of the cancel button might mean we don't have public access to animate the width through UIView animation. The sample snippet below slides the bar from x = 0 to 100 but pops the width to 600 pixels wide.
CGRect searchBarFrame = self.searchViewController.searchBar.frame;
searchBarFrame.origin.x = 100;
searchBarFrame.size.width = 600;
[UIView animateWithDuration:1.0
delay:0.0
options:0
animations:^{
self.searchViewController.searchBar.frame = searchBarFrame;
}
completion:^(BOOL completion){
}];
there is an "issue" with UISearchBar due to the inner views forcing the resize to ignore the animation. However, this can be overcome by the use of - layoutSubviews. I have included the expand and contract code in my project below
[UIView animateWithDuration:.3
animations:^ {
CGRect newBounds = locationSearch.frame;
newBounds.size.width += 215; //newBounds.size.width -= 215; to contract
locationSearch.frame = newBounds;
[locationSearch layoutSubviews];
}];
Hope this helps.
FYI, you can use UIViewAnimationOption instead of calling layoutsubviews explicitly,
So the code would look something like this..
[UIView animateWithDuration:0.5
delay:0
options:UIViewAnimationOptionLayoutSubviews
animations:^{
//Set the frame you want to the search bar
}
completion:^(BOOL finished) {
}];
This is how to overcome enlargement just to the Left side in swift
(This code will enlarge/shrinks the searchBar 93 pixels over the left side when user start/end editing)
SharedNavigationbBar is the UIView that implements the UISearchBarDelegate
searchBarWidth is an outlet to a constrain holding the width of the UISearchBar
An autolayout constrain must exists on your storyboard or nib file to allow resizing into the left side.
In this case, the neighbord left component is an UIButton.
Add the following code as an extension or inside your class to perform the animated resizing.
extension SharedNavigationBar: UISearchBarDelegate
{
//amount of pixels to enlarge to the left
private var offsetSearchBarLeft:CGFloat
{
get {
return 93
}
}
///Enlarges search bar
func searchBarTextDidBeginEditing(searchBar: UISearchBar) {
self.animateSearchBar(self.searchBar, enlarge: true)
}
///Shrinks search bar
func searchBarTextDidEndEditing(searchBar: UISearchBar) {
self.animateSearchBar(self.searchBar, enlarge: false)
}
//shrinks or enlarge the searchbar (this will be the function to call inside the animation)
private func animateSearchBar(searchBar:UISearchBar, enlarge:Bool)
{
///Important here, for this to work, the option and the searchbar size must be handled this way
UIView.animateWithDuration(0.3, delay: 0.0, options: UIViewAnimationOptions.LayoutSubviews, animations: { [weak self] () -> Void in
let multiplier: CGFloat = enlarge ? 1 : -1
let origin = searchBar.frame.origin.x + self!.offsetSearchBarLeft * multiplier
let width = searchBar.frame.width + self!.offsetSearchBarLeft * multiplier
//This Block of code, setting the new frame, needs to be inside the animation in order to work
var newBounds:CGRect = searchBar.frame;
newBounds.origin.x = origin
newBounds.size.width = width
//Sets the new frame
self?.searchBarWidth.constant = width
searchBar.frame = newBounds
}, completion: nil)
}
}