Home > Back-end >  Animating NSView property of custom structure type
Animating NSView property of custom structure type

Time:01-13

I have an NSView subclass with a property declared like this in the header:

@property (nonatomic) BaseRange range;

BaseRange is defined as:

typedef struct BaseRange {          
    float start;
    float len;
} BaseRange;

I want to animate the range property using the NSAnimatablePropertyContainer protocol.

My class overrides defaultAnimationForKey: as required.

The problem is that when I call [myView.animator setRange:<some value>] (myView being a instance of the class in question), myView is sent -setNilValueForKey: for the range key at every step of the animation, except for the final value. IOW, the animation doesn't work.

If I define the range property like so:

@property (nonatomic) NSSize range;

and don't change anything else, no -setNilValueForKey: message is sent, but rather intermediate values for the range key, as normal.

But I don't want to use NSSize, because the range key should represent a range rather than a a size.

Any suggestions?

CodePudding user response:

Provided you return CAPropertyAnimation or its subclass from the [NSAnimatablePropertyContainer defaultAnimationForKey:] method, the behaviour is expect, as it works with strict set of values:

  • integers and doubles
  • CGRect, CGPoint, CGSize, and CGAffineTransform structures
  • CATransform3D data structures
  • CGColor and CGImage references

To my knowledge CAAnimation and its subclasses cannot work with arbitrary values beyond this set and focused primarily to work with Core Graphics properties (layers, frames, colors, etc..). On macOS, however, you can use NSAnimation class instead, which is much more flexible but requires additional customisation to your class. First you should subclass NSAnimation itself and override the -[NSAnimation setCurrentProgress:] method (this is not mandatory, but otherwise you won't be able to get smooth enough transition between animation steps):

- (void)setCurrentProgress:(NSAnimationProgress)currentProgress {
    [super setCurrentProgress:currentProgress];

    // Range owner refers to the object (view) with the property of custom struct type
    // Range Key Path helps to locate the property inside the object
    if (!_rangeOwner || !_rangeKeyPath) {
        return;
    }

    static const char *const kTDWRangeEncoding = @encode(TDWRange);

    // Wraps new range with NSValue
    NSValue *newRange = [NSValue value:&(TDWRange){
        .location = (_targetRange.location - _originalRange.location) * currentProgress   _originalRange.location,
        .length = (_targetRange.length - _originalRange.length) * currentProgress   _originalRange.length
    } withObjCType:kTDWRangeEncoding];

    // Sends new value to the object that owns the range property
    [_rangeOwner setValue:newRange
               forKeyPath:_rangeKeyPath];
}

In this implementation:

  • TDWRange refers to the custom structure which represents the range;
  • _rangeOwner refers to the object which has a property of type TDWRange;
  • _rangeKeyPath refers to the key path by which the NSAnimation subclass can find the property;
  • _targetRange is the value towards which the animation interpolates;
  • _originalRange is the value of the property before animation gets started.

Then, in your custom view class you should provide a separate means to update a property with the given animation. Provided the animation class is called TDWRangeAnimation and the range property is reachable through @"range" key path, such a method may look like this:

- (void)setRange:(TDWRange)range animated:(BOOL)animated {
    if (animated) {
        TDWRangeAnimation *rangeAnimation = [[TDWRangeAnimation alloc] initWithRangeOwnder:self
                                                                              rangeKeyPath:@"range"
                                                                               targetRange:range
                                                                                  duration:0.4
                                                                            animationCurve:NSAnimationEaseOut];
        rangeAnimation.animationBlockingMode = NSAnimationNonblocking;
        [rangeAnimation startAnimation];
    } else {
        self.range = range;
    }
}

You are not required to retain the animation object, since it's maintained by the run loop until the animation finishes.


P.S. Feel free to refer to the gist in case you need a complete implementation sample.

  • Related