rattletrap99
rattletrap99

Reputation: 1479

UIView animateWithDuration behaves differently in iOS 8

I just switched from iOS 7.1 to iOS 8.1. am encountering the following problem with UIView animateWithDuration. Pictures being worth a thousand words, I made videos of the behavior under each iOS:

iOS 7.1:

https://vimeo.com/113887812

iOS 8.1:

https://vimeo.com/113887814

It's an iPhone app.

I should note that I've searched SO and other places before posting. It seems to be a recognized problem, but what I've found are a few references to "using constraint-based animation" (which I didn't understand) and turning off Auto layout, but no real consensus.

I've tried a couple of stabs, but have gotten nowhere. If someone can point me to (or walk me through) an explicit fix, I'd really appreciate it.

I should mention that the app is intended to work only in the portrait position, so I gave no thought to auto layout during the development process.

All the views involved are created programmatically, except the scroller itself.

Here is the code. I included the whole method, which is pretty long, but I really don't understand what has changed in the new iOS, so am being on the safe side. The animateWithDuration call is pretty deep into the chunk:


-(void) displayDayChart
{
    [self.scroller setBackgroundColor:[UIColor clearColor]];

    UIImage *image = [UIImage imageNamed:@"GrayPaper.png"];
    self.chartView.backgroundColor = [UIColor colorWithPatternImage:image];

    [self.view addSubview:self.scroller];
    [self.scroller setScrollEnabled:YES];

    [self.scroller setContentSize:CGSizeMake(320, 1800)];
    [self.scroller setContentOffset:CGPointMake(0.0, 0.0) animated:YES];

    dayBarArray = [[NSMutableArray alloc]init];
    infoLabelArray = [[NSMutableArray alloc]init];
    timeLabelArray = [[NSMutableArray alloc]init];

    timeFrameDC = - 86400; // 24 hours for this test, selectable in real life

    [self.scroller.subviews makeObjectsPerformSelector: @selector(removeFromSuperview)];

    timeframeDCStart = [NSDate dateWithTimeIntervalSinceNow:timeFrameDC];
    timeframeDCEnd = [timeframeDCStart dateByAddingTimeInterval:abs(timeFrameDC)];

    actPredicate = [NSPredicate predicateWithFormat:@"(startTime >= %@ AND stopTime <= %@) OR ((startTime <= %@ AND stopTime >= %@) OR (startTime <= %@ AND stopTime == NULL))",timeframeDCStart,timeframeDCEnd,timeframeDCStart,timeframeDCStart,timeframeDCEnd];

    NSFetchedResultsController *dayActivityFRC = [TimedActivity MR_fetchAllSortedBy:@"startTime" ascending:YES withPredicate:actPredicate groupBy:nil delegate:nil];


    double taDuration;
    double activityPercent;
    int barHeight;
    int lastHeight = 0;
    int tickerCount = 1;
    int barY;
    int lastBarY = 0;
    CGRect lastBarFrame;



#pragma mark DisplayDayChart setup

    for (TimedActivity *activity in dayActivityFRC.fetchedObjects)
    {
        // Create bar and associated labels

        thisBar = [[DayChartBar alloc] initWithFrame:CGRectZero];

        thisInfoLabel = [[UILabel alloc]initWithFrame:CGRectZero];
        thisTimeLabel = [[UILabel alloc]initWithFrame:CGRectZero];
        arrowView = [[UIView alloc]initWithFrame:CGRectZero];

        thisBar.tag = tickerCount;

        thisInfoLabel.tag = thisBar.tag + 100;
        thisTimeLabel.tag = thisBar.tag + 200;

        arrowView.tag = thisBar.tag + 300;
        UIImage *image = [UIImage imageNamed:@"LeftArrowRed.png"];


        // Add them to the scroller

        [self.scroller addSubview:thisBar];
        [self.scroller addSubview:thisInfoLabel];
        [self.scroller addSubview:thisTimeLabel];
        [self.scroller addSubview:arrowView];



        NSCalendar *calendar = [NSCalendar currentCalendar];


        if (!activity.stopTime) // Currently running
        {
            if ([activity.startTime timeIntervalSinceDate:timeframeDCStart] < 0)
            {
                dayBarDurationString = @"24:00:00";
                taDuration = (fabs([timeframeDCStart timeIntervalSinceDate:timeframeDCEnd]));
            }

            else
            {
                NSDateComponents *components= [calendar components:NSDayCalendarUnit|NSHourCalendarUnit|NSMinuteCalendarUnit|NSSecondCalendarUnit fromDate:activity.startTime toDate:timeframeDCEnd options:0];
                NSInteger hours = [components hour];
                NSInteger minutes = [components minute];
                NSInteger seconds =[components second];
                dayBarDurationString = [NSString stringWithFormat:@"%02li:%02li:%02li",(long)hours,(long)minutes,(long)seconds];

                taDuration = (fabs([activity.startTime timeIntervalSinceDate:timeframeDCEnd]));
            }
        }

        else // Either early edge case or fully encapsulated
        {
            if ([activity.startTime timeIntervalSinceDate:timeframeDCStart] < 0) // Early edge case
            {
                NSDateComponents *components= [calendar components:NSDayCalendarUnit|NSHourCalendarUnit|NSMinuteCalendarUnit|NSSecondCalendarUnit fromDate:timeframeDCStart toDate:activity.stopTime options:0];
                NSInteger hours = [components hour];
                NSInteger minutes = [components minute];
                NSInteger seconds =[components second];
                dayBarDurationString = [NSString stringWithFormat:@"%02li:%02li:%02li",(long)hours,(long)minutes,(long)seconds];

                taDuration = (fabs([activity.stopTime timeIntervalSinceDate:timeframeDCStart]));
            }

            else if ([activity.startTime  timeIntervalSinceDate:timeframeDCStart] > 0) // Encapsulated
            {
                NSDateComponents *components= [calendar components:NSDayCalendarUnit|NSHourCalendarUnit|NSMinuteCalendarUnit|NSSecondCalendarUnit fromDate:activity.startTime toDate:activity.stopTime options:0];
                NSInteger hours = [components hour];
                NSInteger minutes = [components minute];
                NSInteger seconds =[components second];
                dayBarDurationString = [NSString stringWithFormat:@"%02li:%02li:%02li",(long)hours,(long)minutes,(long)seconds];

                taDuration = [activity.duration doubleValue];
            }

        }


        NSDateFormatter *localDateFormatter = [[NSDateFormatter alloc] init];
        [localDateFormatter setDateFormat:@"MMM dd"];

        NSDateFormatter *localTimeFormatter = [[NSDateFormatter alloc] init];
        [localTimeFormatter setDateFormat:@"hh:mm a"];



        NSString * timeLabelDateString = [[NSString alloc]init];

        NSString * timeLabelTimeString = [[NSString alloc]init];

        NSString * timeLabelString = [[NSString alloc]init];


        if (([activity.startTime timeIntervalSinceDate:timeframeDCStart] < 0))
        {
            timeLabelDateString = [localDateFormatter stringFromDate:timeframeDCStart];
            timeLabelTimeString = [localTimeFormatter stringFromDate:timeframeDCStart];
        }

        else
        {
            timeLabelDateString = [localDateFormatter stringFromDate:activity.startTime];
            timeLabelTimeString = [localTimeFormatter stringFromDate:activity.startTime];
        }


        timeLabelString = [NSString stringWithFormat:@"%@ - %@",timeLabelDateString,timeLabelTimeString];

        thisBar.endColor = activity.color;

        activityPercent = fabs((taDuration / timeFrameDC) * 100);

        if (activityPercent > 100)
        {
            activityPercent = 100;
        }

        if (activityPercent < 1.9)
        {
            activityPercent = 1.9;
        }


        barHeight = abs((self.scroller.contentSize.height - 100) * (activityPercent / 100));

        if (tickerCount == 1)
        {
            barY = self.scroller.contentSize.height -10;
        }

        else
        {
            barY = (lastBarY - lastHeight);
        }



#pragma mark DisplayDayChart animation


        [UIView animateWithDuration:.8
                              delay:0.0
                            options: UIViewAnimationCurveEaseInOut // Deprecated, but still works
                         animations:^
         {
             // Starting state *************************************

             thisBar.frame = CGRectMake(20, self.scroller.contentSize.height, 130, 0);
             thisBar.backgroundColor = [UIColor blackColor];

             thisInfoLabel.frame = CGRectMake(20, self.scroller.contentSize.height, thisBar.frame.size.width, 30);

             thisTimeLabel.frame = CGRectMake((thisBar.frame.origin.x) + (thisBar.frame.size.width + 35), self.scroller.contentSize.height, 150, 15);

             thisTimeLabel.textColor = [UIColor blueColor];

             arrowView.frame = CGRectMake(thisTimeLabel.frame.origin.x - 20, thisTimeLabel.frame.origin.y, 16, 16);


             thisInfoLabel.textColor = [UIColor clearColor];


             // End state *************************************

             thisBar.frame = CGRectMake(20, barY, 130, - barHeight);
             thisBar.backgroundColor = thisBar.endColor;

             thisBar.layer.shadowColor = [[UIColor blackColor] CGColor];
             thisBar.layer.frame = CGRectInset(thisBar.layer.frame, 0.0, 3.0);

             thisBar.layer.shadowOpacity = 0.7;
             thisBar.layer.shadowRadius = 4.0;
             thisBar.layer.shadowOffset = CGSizeMake(5.0f, 5.0f);


             // Position infoLabel

             thisInfoLabel.frame = CGRectMake(20, self.scroller.contentSize.height + ((thisBar.frame.size.height / 2) - 30), thisBar.frame.size.width, 30);


             thisTimeLabel.frame = CGRectMake((thisBar.frame.origin.x) + (thisBar.frame.size.width + 35), (thisBar.frame.origin.y) + (thisBar.frame.size.height) - 5, 150, 15);

             thisInfoLabel.textColor = [UIColor blackColor];

             dayBarNameString = activity.name;

             [thisInfoLabel setFont:[UIFont fontWithName:@"Noteworthy-Bold" size:16.0]];

             thisInfoLabel.numberOfLines = 0;

             thisInfoLabel.textAlignment = NSTextAlignmentCenter;
//             thisInfoLabel.layer.borderWidth = 1;


             thisInfoLabel.text = [NSString stringWithFormat:@"%@\n%@",dayBarNameString,dayBarDurationString];

             [thisInfoLabel sizeToFit];


             if ( thisInfoLabel.frame.size.height >= thisBar.frame.size.height)
             {
                 thisInfoLabel.text = [NSString stringWithFormat:@"%@",dayBarNameString];
             }

             else
             {
                 thisInfoLabel.text = [NSString stringWithFormat:@"%@\n%@",dayBarNameString,dayBarDurationString];
             }

             [thisInfoLabel sizeToFit];
             [thisInfoLabel setHidden:NO];

             thisInfoLabel.center = thisBar.center;

             arrowView.frame = CGRectMake(thisTimeLabel.frame.origin.x - 20, thisTimeLabel.frame.origin.y, 16, 16);
             arrowView.backgroundColor = [UIColor colorWithPatternImage:image];

             thisTimeLabel.textColor = [UIColor blueColor];
             [thisTimeLabel setFont:[UIFont boldSystemFontOfSize:13]];
             thisTimeLabel.numberOfLines = 0;

             thisTimeLabel.textAlignment = NSTextAlignmentLeft;

             thisTimeLabel.text = timeLabelString;

             [thisTimeLabel sizeToFit];


         }
                         completion:^(BOOL finished)
         {

             [self repositionLabels];

         }];


        [dayBarArray addObject:thisBar];
        [infoLabelArray addObject:thisInfoLabel];
        [timeLabelArray addObject:thisTimeLabel];
        taDuration = 0;
        tickerCount ++;


        lastBarY = barY;
        lastHeight = barHeight;
        lastBarFrame = thisBar.frame;

        [self positionInitialDayBarView];

    }
}

Upvotes: 0

Views: 614

Answers (2)

matt
matt

Reputation: 535851

I still don't understand why the switch to iOS 8 brought about such a large change

I expect that one reason for the difference is that setting the text of a label (which you are doing in your animation block) triggers layout immediately in iOS 8, which was not the case in iOS 7. Layout means that now all the constraints are instantly obeyed, thus putting everything back into its original position (because the constraints have not changed).

See, for example, my answer here: https://stackoverflow.com/a/26964376/341994

And here: https://stackoverflow.com/a/27085648/341994

And here: https://stackoverflow.com/a/26953986/341994

Upvotes: 0

Fogmeister
Fogmeister

Reputation: 77651

OK, first off. You're losing a lot of convenience stuff by refusing to use Interface Builder. It's like saying "I only use Xcode 3 because it used to work for me so I just kept it.". Interface Builder is an awesome tool that makes creating UIs ridiculously easy.

The actual problem

You said that you are not using AutoLayout. That is not correct.

The default behaviour (in iOS8 definitely and also iOS7 I think) is that when you create a UIView (or any UI element) and add it to a view then the system will take the auto resizing mask of the view and apply Auto Layout constraints to match the resizing mask.

This means that when you do...

UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, 100, 21)];
[self.view addSubView:label];

Then the label has auto layout constraints added to it to fix it to the position (10, 10) and the size (100, 21).

You can't then change the frame by doing...

label.frame = CGRectMake(10, 50, 100, 21);

Because the auto layout constraints will just put it right back where it started.

Solutions

There are two solutions to this.

The correct solution is to learn how to use AutoLayout. It has been around now for 3 years and is becoming more and more important to iOS development (even just on portrait iPhone apps you now have 4 different devices sizes to deal with). If you don't start learning AutoLayout you will be left behind. It is pretty much a requirement for all iOS development now.

The incorrect and very very bad solution is to stop the system from adding these constraints. You can do this by adding the line...

label.translatesAutoresizingMaskIntoConstraints = NO;

after creating the label (and all other interface elements).

This will stop those automatic constraints from being added and stop them interfering with your layout stuff.

This is the wrong solution though. If you are doing this you are just delaying the point at which you have to learn Auto Layout.

Upvotes: 5

Related Questions