iDev
iDev

Reputation: 478

Block always returning nil

I'm trying to record a UIView animation for a WatchKit app. First, I implemented the function with out the block which return zero frames being recorded. This was due to the [recorder stop] being called before the animation completed (I think). So, I added a completion block. Now, it never has self.completion is YES. I want the completion block to notify me when the animation is complete. What am I missing here?

ViewController.m

-(void)runAnimation{

ALBatteryView *batteryView = [[ALBatteryView alloc] initWithFrame:CGRectMake(0, 0, 64, 64)];
  [self.batteryIcon addSubview:batteryView];
  recorder.view = _batteryIcon;
  [recorder start];
  [batteryView setBatteryLevelWithAnimation:YES forValue:[UIDevice currentDevice].batteryLevelInPercentage inPercent:YES];
  CGFloat batteryPer = [UBattery batteryLevel];
  batteryPercentage.text = [NSString stringWithFormat:@"%.0f%%", batteryPer];
  battery = [NSString stringWithFormat:@"%.0f%%", batteryPer];
  [batteryView batteryAnaminationWithCompletion:^(BOOL finished){
    if (finished){
      [recorder stop];
    }
  }];
}

AlBatteryView.h

@interface ALBatteryView : UIView {
UIView *batteryFill;
}
@property (nonatomic, strong) void(^completion)(BOOL finished);

- (void)setBatteryLevelWithAnimation:(BOOL)isAnimated forValue:(CGFloat)batteryLevel inPercent:(BOOL)inPercent;
- (void)reload;
- (void)batteryAnaminationWithCompletion:(void (^)(BOOL finished))completion;
@end

ALBatteryView.m

- (void)setBatteryLevelWithAnimation:(BOOL)isAnimated forValue:(CGFloat)batteryLevel inPercent:(BOOL)inPercent {

  // Declare the newWidth and save the correct battery level
  // based on inPercent value
  CGFloat newWidth;
  CGFloat newBatteryLevel = (inPercent) ? batteryLevel : batteryLevel * 100;
  // Set the new width
  newWidth = kOnePercent * newBatteryLevel;
  // If animated proceed with the animation
  // else assign the value without animates

  if (isAnimated)
      [UIView animateWithDuration:2.0 animations:^{
          /* This direct assignment is possible
           * using the UIView+ALQuickFrame category
           * http://github.com/andrealufino/ALQuickFrame */
          batteryFill.width = newWidth;
          // Set the color based on battery level
          batteryFill.backgroundColor = [self setColorBasedOnBatteryLevel:newBatteryLevel];
          if (self.completion) {
              self.completion(YES);
          }
      }];

  else
      batteryFill.width = newWidth;

}
-(void)batteryAnaminationWithCompletion:(void (^)(BOOL finished))completion{
  self.completion = completion;
}

Upvotes: 0

Views: 284

Answers (4)

danh
danh

Reputation: 62686

The interface is unduly complex if the only objective is animation. There's no need for a block property at all. Consider the following...

// ALBatteryView.h
@interface ALBatteryView : UIView

// up to you, but I'd clean up the animating interface by making a property of this class determine
// how it handles levels in all cases as either absolute or %
@property(assign, nonatomic) BOOL inPercent;

// use naming style like the sdk...
- (void)setBatteryLevel:(CGFloat)batteryLevel animated:(BOOL)animated completion:(void (^)(BOOL))completion;

@end

In the implementation...

// ALBatteryView.m
@interface ALBatteryView ()

// hide the internal view property here.  make it weak, and assign it after [self subView:...
@property(weak,nonatomic) UIView *batteryFill;

@end

In just that one animation method, using a few tricks in UIView animation, you can accomplish everything that it looks like you need...

- (void)setBatteryLevel:(CGFloat)batteryLevel animated:(BOOL)animated completion:(void (^)(BOOL))completion {

    // one trick is that UIView animation with zero duration just
    // changes the animatable properties and calls its completion block
    NSTimeInterval duration = (animated)? 2.0 : 0.0;

    CGFloat newBatteryLevel = (self.inPercent)? batteryLevel : batteryLevel * 100;
    CGFloat newWidth = kOnePercent * newBatteryLevel;
    // don't name that color-returning method "set" color.  The "set" part happens here...
    UIColor *newColor = [self colorBasedOnBatteryLevel:newBatteryLevel];

    // now, the animation in just one shot, passing the block parameter
    // which has the same signature as the UIView animation block param
    [UIView animateWithDuration:duration animations:^{
        self.batteryFill.width = newWidth;
        self.batteryFill.backgroundColor = newColor;
    } completion:completion]; 
}

Upvotes: 0

andykkt
andykkt

Reputation: 1706

If I am reading it right,

When you call -(void)runAnimation it calls the animation block right here and animation block will referencing the completion which is nil at the moment

  [batteryView setBatteryLevelWithAnimation:YES forValue:[UIDevice currentDevice].batteryLevelInPercentage inPercent:YES];

If you are thinking animationWithDuration call the block 2 second later than.. it's wrong.. self.completion is nil at the moment and animation's duration is 2 second but self.completion is not animatable and it calls right up front.. you need some kind of lock if you want to wait this animation to be finished.

[UIView animateWithDuration:2.0 animations:^{
        /* This direct assignment is possible
         * using the UIView+ALQuickFrame category
         * http://github.com/andrealufino/ALQuickFrame */
        batteryFill.width = newWidth;
        // Set the color based on battery level
        batteryFill.backgroundColor = [self setColorBasedOnBatteryLevel:newBatteryLevel];
        if (self.completion) {
                    self.completion(YES);
        }
    }];

Just print out NSLog before animateWithDuration and NSLog from inside of block to see when if (self.completion) is referenced..

Upvotes: 0

A O
A O

Reputation: 5698

You need to set your block property before you call for the animation (setBatteryLevelWithAnimation). Otherwise it will be nil when you try to access it before it's set.

Also you should set your block property directly, it will be more clear, because that's what your -batteryAnaminationWithCompletion method does (btw it should be spelled "Animation") From:

  [batteryView batteryAnaminationWithCompletion:^(BOOL finished){
    if (finished){
      [recorder stop];
    }
  }];

To:

  [batteryView setCompletion:^(BOOL finished){
    if (finished){
      [recorder stop];
    }
  }];

Upvotes: 1

Eiko
Eiko

Reputation: 25632

You should set your completion block before starting the animation (change code order).

Then, your block should be declared as copy.

@property (nonatomic, copy) void(^completion)(BOOL finished);

Then, delete the (wrongly named) batteryAnaminationWithCompletion method altogether, and just use the property:

batteryView.completion = ...

Upvotes: 0

Related Questions