UITextView animating change of frame does not animate text reallocation - ios

I have a UITextView, and I am trying to animate a change of frame when the user taps on a button. Basically the text view grows larger to fit the screen so it can display more text, and then when the user taps the button again it shrinks to its original frame.
I perform the animation using blocks, like this:
if(!isDisplayingDetailView)
{
//Expand view
[UIView animateWithDuration:0.5
animations:^{
self.detailView.frame = self.view.frame;
}
completion:^(BOOL finished){
isDisplayingDetailView = YES;
}];
}
else{
//Shrink view.
[UIView animateWithDuration:0.5
animations:^{
self.detailView.frame = self.detailFrame;
}
completion:^(BOOL finished){
isDisplayingDetailView = NO;
}];
}
Where self.detailView is the UITextView, and self.detailFrame is just a CGRect holding the original frame. isDisplayingDetailView is just a BOOL to check if the view is expanded or not.
My problem is that the text resize is not animating. I made some screenshots from a test app to illustrate my problem:
App default view:
The expanded view:
The textview just a moment after tapping the button:
As you can see the text automatically shrinks to the final frame size, without any kind of animation, while the bounds still are animating. I guess that's the right behavior, but I'd like to know if it's possible to animate the text along with its view.

The text in textviews doesn't always act as expected. For this you'll have to set up a NSTimer and set the frame size on every tick.
Do something like:
textview.frame = CGRectMake (textview.frame.origin.x, textview.frame.origin.y, textview.frame.size.width-1, textview.frame.size.height-1);
Then when it's done I would completely remove the textview from superview and set it to nil, and then init a new one. The reason for this is that when changes the frames of textviews for some reason padding is added around the text's box. You won't notice it unless you change the frame a probably at least 100 times.
[textview removeFromSuperview];
textview = nil;
textview = [[UITextView alloc] initWithFrame:CGRectMake(x, y, width, height)];
textview.text = yourTextString;
//You will also have to set whatever non default properties you want, such as text or background color
[view addSubview:textview];

Another solution is to animate the bounds change.
There are a few ways, but here's a simple subclass of UITextView which does exactly this.
import Foundation
import UIKit
import QuartzCore
class SmoothTextView : UITextView {
override func actionForLayer(layer: CALayer!, forKey key: String!) -> CAAction! {
if key == "bounds" {
let x = super.actionForLayer(layer, forKey: "backgroundColor")
if let action:CAAnimation = x as? CAAnimation {
let transition = CATransition()
transition.type = kCATransitionFade
transition.beginTime = action.beginTime
transition.duration = action.duration
transition.speed = action.speed
transition.timeOffset = action.timeOffset
transition.repeatCount = action.repeatCount
transition.repeatDuration = action.repeatDuration
transition.autoreverses = action.autoreverses
transition.fillMode = action.fillMode
transition.timingFunction = action.timingFunction
transition.delegate = action.delegate
return transition
}
}
return super.actionForLayer(layer, forKey: key)
}
}
This as of iOS 8.
As an extra tweak, you might want to configure the text of your text view instance by adjusting the text container by zeroing all padding or insets:
textView.textContainer.lineFragmentPadding = 0.0
textView.textContainerInset = UIEdgeInsetsZero

Related

IOS/Objective-C: Slide updated view in from right on swipe

I would like to emulate the swipe to left to show next picture seen in the photos app, among other places so that the new picture slides in from right to left using animation.
However, in my case, the view has more than one photo. It has a caption as well.
I am able to do this without animation by just updating the photo and text upon swipe. This just changes the photo and caption on screen, however, without the right to left animation technique.
Is there a way to show the old view moving to the left while the new updated view moves in from the right.
The following moves the view to the left but renders the screen black as nothing is put in its place.
//In handleSwipe called by gesture recognizer
CGRect topFrame = self.view.frame;
topFrame.origin.x = -320;
[UIView animateWithDuration:0.3 delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
self.view.frame = topFrame; _myImage=newImage;
_mycaption=newcaption;} completion:^(BOOL finished){ }];
Thanks for any suggestions.
Edit:
The view in question is a detail view that you get to from a tableview through a segue. It currently already uses a scrollview for vertical scrolling as the content can exceed a single screen in the vertical dimension.
I have found a way to get the old photo and caption to move left and the new to come in from right using a completion block, however, it is less than satisfactory, because there is a gap between the two actions when the screen is black. This seems endemic to completion approach because new image does not start to move until old image has completed moving.
- (IBAction)swipedLeft:(id)sender
{
CGRect initialViewFrame = self.view.frame;
CGRect movedToLeftViewFrame = CGRectMake(initialViewFrame.origin.x - 375, initialViewFrame.origin.y, initialViewFrame.size.width, initialViewFrame.size.height);
CGRect movedToRightViewFrame = CGRectMake(initialViewFrame.origin.x + 375, initialViewFrame.origin.y, initialViewFrame.size.width, initialViewFrame.size.height);
[UIView animateWithDuration:0.1
animations:^{self.view.frame = movedToLeftViewFrame;
//above moves old image out of view.
}
completion:^(BOOL finished)
{
self.view.frame = movedToRightViewFrame;
[self loadNextItem];//changes pic and text.
[UIView animateWithDuration:0.1
animations:^{self.view.frame = initialViewFrame;
//moves new one in
}
completion:nil];
}];
}
I suggest using a UIPageViewController, and manage each image/set of images as a separate view controller.
A page view controller supports either a page curl style transition or a slide transition. You'd want the slide transition.
Three ways to build this using existing components:
UIPageViewController (see DuncanC's answer).
UICollectionView with pagingEnabled set to true. Use a full-screen-sized cell and a UICollectionViewFlowLayout with scrollDirection set to horizontal.
UICollectionView as above, but instead of setting pagingEnabled to true, implement scrollViewWillEndDragging:withVelocity:targetContentOffset: in your delegate. This allows you to have a gutter between each cell while still having each cell fill the screen. See this answer.
It's ScrollView+PageControl
I hope it can help you.
- (void)viewDidLoad {
    [super viewDidLoad];
    self.scrollView = [[UIScrollView alloc] init];
    self.scrollView.delegate =self;
    self.scrollView.translatesAutoresizingMaskIntoConstraints =NO;
    self.scrollView.pagingEnabled =YES;
 self.scrollView.contentSize =CGSizeMake(ViewWidth*VIewCount, ViewHeight);
    [self.view addSubview:self.scrollView];
 self.pageControl = [[UIPageControl alloc] init];
   self. pageControl.translatesAutoresizingMaskIntoConstraints =NO;
    [self.view addSubview:self.pageControl];
    self. pageControl.numberOfPages = VIewCount;
    self.pageControl.currentPage = 0;
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:self.scrollView
                                                           attribute:NSLayoutAttributeCenterX
                                                           relatedBy:NSLayoutRelationEqual
                                                              toItem:self.view
                                                           attribute:NSLayoutAttributeCenterX
                                                          multiplier:1.0f
                                                            constant:0.0f]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"[self.scrollView(width)]"
                                                                     options:0
                                                                     metrics:#{#"width":#(ViewWidth)}
                                                                       views:NSDictionaryOfVariableBindings(self.scrollView)]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|-50-[self.scrollView(height)]"
                                                                      options:0
                                                                      metrics:#{#"height":#(HEIGHT_VIEW)}
                                                                        views:NSDictionaryOfVariableBindings(self.scrollView)]];
     
   
  
 
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:[_scrollView]-5-[pageControl(15)]"
                                                                      options:NSLayoutFormatAlignAllCenterX
                                                                      metrics:nil
                                                                        views:NSDictionaryOfVariableBindings(self.scrollView, self.pageControl)]];
//[imgArr addObject:[UIImage imageNamed:...]];
    for (NSInteger index = 0; index<ViewCount; index++) {
        UIImageView *addSubview = [[UIImageView alloc] initWithFrame:CGRectMake(index*ViewWidth, 0, ViewWidth, ViewHeight)];
// addSubView.image = [imgArr objectAtIndex:index];
        [self.scrollView addSubview:addSubview];
    }
}
 
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    NSInteger currentOffset = scrollView.contentOffset.x;
    NSInteger index = currentOffset / ViewWidth;
     
    if (currentOffset % ViewWidth == 0) {
        pageControl.currentPage = index;
    }
}
 

Animation of UILabel and UITextField (Facebook POP framework)

I'm using the Facebook POP framework to animate UILabels and UITextFields when the user entered a wrong input. I want to simulate the kind of spring animation when you enter the wrong password in on a mac.
I've writting this method for doing the shake animation:
- (void)shakeAnimation:(UITextField *)textField :(UILabel *)label
{
//This method takes a textField and a label as input, and animates them accordingly.
//When the animation is done, the button is available for touch again, and the animation is removed.
POPSpringAnimation *positionAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
positionAnimation.springBounciness = 20; //just parameters, nothing fancy
positionAnimation.velocity = #4000;
[positionAnimation setCompletionBlock:^(POPAnimation *animation, BOOL finished) {
self.signUpButton.userInteractionEnabled = YES;
}];
[textField.layer pop_addAnimation:positionAnimation forKey:#"positionAnimation"];
[label.layer pop_addAnimation:positionAnimation forKey:#"positionAnimation"];
[positionAnimation removedOnCompletion];
}
However, When I'm calling this method like so: [self shakeAnimation:self.emailField :self.emailLabel];
the UILabels only move about 50px to the right and then stop moving, whereas the UITextFields don't do anything. It doesn't seem like an animation at all, just an adjustment of the frame.
Instead of adding the same POPSpringAnimation to both the textField layer and the label layer, make two POPSpringAnimations and add one to each. Like this :
POPSpringAnimation *anim1 = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
anim1.springBounciness = 20; //just parameters, nothing fancy
anim1.velocity = #400;
[anim1 setCompletionBlock:^(POPAnimation *animation, BOOL finished) {
self.button.userInteractionEnabled = YES;
}];
POPSpringAnimation *anim2 = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
anim2.springBounciness = 20; //just parameters, nothing fancy
anim2.velocity = #400;
[textField.layer pop_addAnimation:anim1 forKey:#"positionAnimation"];
[label.layer pop_addAnimation:anim2 forKey:#"positionAnimation"];
[anim1 removedOnCompletion];
[anim2 removedOnCompletion];

Animated UIButtons snap back to original position after opening UIAlertView

I am making a Welcome screen with two UITextFields, a Forgot Password button and two additional buttons (signInButton and createAccountButton). These two buttons are placed in an UIView (inputsView) along with the text fields.
As soon as one of the text fields is focused, I animate the screen to adjust for the keyboard, resize the createAccountButton button and slide in the signInButton. I'm using the following code:
[UIView animateWithDuration:.48 delay:0 usingSpringWithDamping:1 initialSpringVelocity:.42 options:(UIViewAnimationOptionBeginFromCurrentState) animations:^{
CGRect f3 = inputsView.frame;
f3.origin.y = 112;
inputsView.frame = f3;
[inputsView layoutSubviews];
CGRect f4 = createAccountButton.bounds;
f4.size.width = 160;
createAccountButton.bounds = f4;
CGRect f5 = createAccountButton.frame;
f5.origin.x = 160;
createAccountButton.frame = f5;
CGRect f6 = signInButton.frame;
f6.origin.x = 0;
signInButton.frame = f6;
CGRect f7 = buttonSeparator.frame;
f7.origin.x = 160;
buttonSeparator.frame = f7;
[signInButton layoutSubviews];
[createAccountButton layoutSubviews];
} completion:nil];
This all works fine, but if I then tap the Forgot Password button to open a UIAlertView, the buttons pop back to their original positions. Strangely enough, the buttonSeparator, which is a 1px UIView does not snap back.
Am I forgetting something, or is this a bug in the iOS 7 SDK?
Most likely you are using autolayout
Disable the autolayout property.
Reason why its happening : Autolayout will update the views to their original position using current constraints.
Hope it helps.

Animating the width of UISearchBar frame

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)
}
}

How to animate the background color of a UILabel?

This looks like it should work, but doesn't. The color turns green at once.
self.labelCorrection.backgroundColor = [UIColor whiteColor];
[UIView animateWithDuration:2.0 animations:^{
self.labelCorrection.backgroundColor = [UIColor greenColor];
}];
I can't find it documented anywhere, but it appears the backgroundColor property of UILabel is not animatable, as your code works fine with a vanilla UIView. This hack appears to work, however, as long as you don't set the background color of the label view itself:
#import <QuartzCore/QuartzCore.h>
...
theLabel.layer.backgroundColor = [UIColor whiteColor].CGColor;
[UIView animateWithDuration:2.0 animations:^{
theLabel.layer.backgroundColor = [UIColor greenColor].CGColor;
} completion:NULL];
Swift
(important) Set the UILabel's background color to clear (either in IB or in code). For example:
override func viewDidLoad() {
super.viewDidLoad()
myLabel.backgroundColor = UIColor.clear
}
Animate the layer background color.
#IBAction func animateButtonTapped(sender: UIButton) {
UIView.animate(withDuration: 1.0, animations: {
self.myLabel.layer.backgroundColor = UIColor.red.cgColor
})
}
Note that CGColor is added after the UIColor.
Result
Swift 3
To animate your label background color from white to green, set up your label like this:
self.labelCorrection.backgroundColor = UIColor.clear
self.labelCorrection.layer.backgroundColor = UIColor.white.cgColor
Animate like this:
UIView.animate(withDuration: 2.0) {
self.labelCorrection.layer.backgroundColor = UIColor.green.cgColor
}
To go back to the original state, so you can animate again, make sure you remove animations:
self.labelCorrection.layer.backgroundColor = UIColor.white.cgColor
self.labelCorrection.layer.removeAllAnimations()
You CAN animate it, but I had to first set it (programmatically) to clearColor for some reason. Without that, the animation either didn't work or wasn't visible.
I am animating the background color of a UILabel in a custom table cell. Here is the code in the willDisplayCell method. I wouldn't try to set the animation in cellForRow, since a lot of the layout gets tinkered with by the table.
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
((RouteListCell *)cell).name.backgroundColor = [UIColor clearColor];
[((RouteListCell *)cell).name backgroundGlowAnimationFromColor:[UIColor whiteColor] toColor:[UIColor redColor] clearAnimationsFirst:YES];
}
Here's my animation routine:
-(void) backgroundGlowAnimationFromColor:(UIColor *)startColor toColor:(UIColor *)destColor clearAnimationsFirst:(BOOL)reset;
{
if (reset)
{
[self.layer removeAllAnimations];
}
CABasicAnimation *anAnimation = [CABasicAnimation animationWithKeyPath:#"backgroundColor"];
anAnimation.duration = 1.00;
anAnimation.repeatCount = HUGE_VAL;
anAnimation.autoreverses = YES;
anAnimation.fromValue = (id) startColor.CGColor; // [NSNumber numberWithFloat:1.0];
anAnimation.toValue = (id) destColor.CGColor; //[NSNumber numberWithFloat:0.10];
[self.layer addAnimation:anAnimation forKey:#"backgroundColor"];
}
Do the color animation manually, based off of an NSTimer or CADisplayLink callback at some reasonable frame rate (say 20 fps). You will have to calculate your own color change curve in RGB or HSV based on the fractional elapsed time over the full animation time (2.0 seconds in your example), or use an array of 40 intermediate colors.
Please try this,
[UIView transitionWithView:yourView duration:0.2 options:UIViewAnimationOptionTransitionNone animations:^{
[yourView setBackgroundColor:[UIColor colorWithRed:0.8588 green:0.8588 blue:0.8588 alpha:1]];
}completion:^(BOOL finished) {
}];
It works for me.
I recently came across this problem once again and after some research I figured that basically nothing changed (and probably won't in foreseeable future), so I ended up using Facebook POP framework (not only) for that.
You can easily do animations of basic properties on both view and layer, but in addition, it provides smooth transition between colors (and basically whatever you want, even your custom types, with some additional coding). So for background color, you would do something like this:
// Create spring animation (there are more types, if you want)
let animation = POPSpringAnimation(propertyNamed: kPOPViewBackgroundColor)
// Configure it properly
animation.autoreverses = false
animation.removedOnCompletion = true
animation.fromValue = UIColor.yourBeginningColor()
animation.toValue = UIColor.yourFinalColor()
// Add new animation to your view - animation key can be whatever you like, serves as reference
label.pop_addAnimation(animation, forKey: "YourAnimationKey")
Viola, now you can animate background colors on everything that has that property!
Swift 4.1 UILabel Extension:
Add this extension to your code:
extension UILabel {
/** Animates the background color of a UILabel to a new color. */
func animateBackgroundColor(to color : UIColor?, withDuration : TimeInterval, completion : ((Bool) -> Void)? = nil) {
guard let newColor = color else { return }
self.backgroundColor = .clear
UIView.animate(withDuration: withDuration, animations: {
self.layer.backgroundColor = newColor.cgColor
}, completion: completion)
}
}
You should use it like this:
myUILabel.animateBackgroundColor(to: .red, withDuration: 0.5)
Now, if you want to restore the original color, use it like this:
// Storing the orignal color:
let originalColor = myUILabel.backgroundColor
// Changing the background color:
myUILabel.animateBackgroundColor(to: .red, withDuration: 0.5)
// ... Later:
// Restoring the original color:
myUILabel.animateBackgroundColor(to: originalColor, withDuration: 0.5, completion: {
(value: Bool) in
myUILabel.backgroundColor = originalColor
})
it says in the docs that backgroundColor is animatable
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html#//apple_ref/doc/uid/TP40006816-CH3-SW131
Don't really know since when.
If you don't want to dip down into Quartz, per answer one of the prev answers, create an empty UIView of same size as UILabel and position it in same spot as the UILabel (and in Z order, under it) and you can animate that UIView's background color (I've tried this, and it works for me).
Here is the reference:
animations A block object containing
the changes to commit to the views.
This is where you programmatically
change any animatable properties of
the views in your view hierarchy. This
block takes no parameters and has no
return value. This parameter must not
be NULL
What I understand about this is when your view call the animation block, then your view label background color is commited to be green since that time. Then, if you don't change the label background color to something else, then it will always be green. This is what I guess

Resources