Skip to content

Instantly share code, notes, and snippets.

@TheDreamsWind
Last active January 12, 2023 13:03
Show Gist options
  • Save TheDreamsWind/e1935dff79d4caf19bc33e96c0c02b04 to your computer and use it in GitHub Desktop.
Save TheDreamsWind/e1935dff79d4caf19bc33e96c0c02b04 to your computer and use it in GitHub Desktop.
[SO-a/75096664/5690248] Animation class to interpolate custom structure property of a Cocoa class
//
// TDWView.h
//
// Created by Aleksandr Medvedev on 12.01.2023.
//
#import <AppKit/AppKit.h>
typedef struct {
CGFloat location;
CGFloat length;
} TDWRange;
NS_ASSUME_NONNULL_BEGIN
@interface TDWView : NSView
@property (assign, nonatomic, direct) TDWRange range;
- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithRange:(TDWRange)range
frame:(NSRect)frame NS_DESIGNATED_INITIALIZER __attribute__((objc_direct));
- (void)setRange:(TDWRange)range animated:(BOOL)animated __attribute__((objc_direct));
@end
NS_ASSUME_NONNULL_END
//
// TDWView.m
//
// Created by Aleksandr Medvedev on 12.01.2023.
//
#import "TDWView.h"
NS_ASSUME_NONNULL_BEGIN
@interface TDWRangeAnimation : NSAnimation
@property (weak, nonatomic, direct) id rangeOwner;
@property (copy, nonatomic, direct) NSString *rangeKeyPath;
@property (assign, nonatomic, direct) TDWRange originalRange;
@property (assign, nonatomic, direct) TDWRange targetRange;
- (instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithRangeOwnder:(nullable id)owner
rangeKeyPath:(nullable NSString *)keyPath
targetRange:(TDWRange)range
duration:(NSTimeInterval)duration
animationCurve:(NSAnimationCurve)animationCurve NS_DESIGNATED_INITIALIZER __attribute__((objc_direct));
@end
NS_ASSUME_NONNULL_END
@implementation TDWRangeAnimation
#pragma mark Lifecycle
- (instancetype)initWithDuration:(NSTimeInterval)duration
animationCurve:(NSAnimationCurve)animationCurve {
return [self initWithRangeOwnder:nil
rangeKeyPath:nil
targetRange:(TDWRange){ 0, 0 }
duration:duration
animationCurve:animationCurve];
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
_rangeOwner = nil;
_rangeKeyPath = nil;
_targetRange = (TDWRange){ 0, 0 };
_originalRange = (TDWRange){ 0, 0 };
}
return self;
}
- (instancetype)initWithRangeOwnder:(id)owner
rangeKeyPath:(NSString *)keyPath
targetRange:(TDWRange)range
duration:(NSTimeInterval)duration
animationCurve:(NSAnimationCurve)animationCurve {
if (self = [super initWithDuration:duration
animationCurve:animationCurve]) {
_rangeOwner = owner;
_rangeKeyPath = keyPath;
_targetRange = range;
if (owner && keyPath) {
NSValue *rangeValue = [owner valueForKeyPath:keyPath];
[rangeValue getValue:&_originalRange];
} else {
_originalRange = (TDWRange){ 0, 0 };
}
}
return self;
}
#pragma mark NSAnimation
- (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];
}
@end
#pragma mark -
NS_ASSUME_NONNULL_BEGIN
__attribute__((__objc_direct_members__))
@interface TDWView ()<NSAnimationDelegate>
@property (weak, nonatomic, nullable) TDWRangeAnimation *rangeAnimation;
@end
NS_ASSUME_NONNULL_END
@implementation TDWView
#pragma mark Lifecycle
- (instancetype)initWithFrame:(NSRect)frameRect {
return [self initWithRange:(TDWRange){ 0, 0 } frame:frameRect];
}
- (instancetype)initWithRange:(TDWRange)range frame:(NSRect)frame {
if (self = [super initWithFrame:frame]) {
_range = range;
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
_range = (TDWRange){ 0, 0 };
}
return self;
}
static void *kRangeKVOContext = &kRangeKVOContext;
- (void)awakeFromNib {
[super awakeFromNib];
[self addObserver:self
forKeyPath:@"range"
options:0
context:kRangeKVOContext];
}
#pragma mark Interface
- (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.delegate = self;
self.rangeAnimation = rangeAnimation;
[rangeAnimation startAnimation];
} else {
self.range = range;
}
}
#pragma mark NSAnimationDelegate
- (BOOL)animationShouldStart:(NSAnimation *)animation {
if (animation == self.rangeAnimation) {
NSLog(@"Range animation begins with value: { Location: %f, Length: %f }", _range.location, _range.length);
}
return YES;
}
- (void)animationDidEnd:(NSAnimation *)animation {
if (animation == self.rangeAnimation) {
NSLog(@"Range animation finished with value: { Location: %f, Length: %f }", _range.location, _range.length);
}
}
#pragma mark KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (context == kRangeKVOContext) {
NSLog(@"Range changed: { Location: %f, Length: %f }", _range.location, _range.length);
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment