Files
2018-11-01 11:24:51 +02:00

2275 lines
84 KiB
Objective-C

//
// TimelineAnimation.m
// TimelineAnimations
//
// Created by Georges Boumis on 14/09/2015.
// Copyright (c) 2015-2016 Abzorba Games. All rights reserved.
//
#import "TimelineAnimation.h"
#import "TimelineEntity.h"
#import "TimelineAnimationProtected.h"
#import "TimelineAnimationsProgressMonitorLayer.h"
#import "KeyValueBlockObservation.h"
#import "TimelineAnimationsBlankLayer.h"
#import "TimelineAudio.h"
#import "TimelineAudioAssociation.h"
#import "TimelineAudioAssociation_Internal.h"
#import "TimelineAnimationNotifyBlockInfo.h"
#import "TimelineAnimationsDisplayLink.h"
#import "NSArray+TimelineSwiftyAdditions.h"
#import "TimelineAnimationDescription.h"
#import "PrivateTypes.h"
@import ObjectiveC;
#import "GroupTimelineAnimation.h"
#import "TimelineAudioAssociation_Internal.h"
#import "NSSet+TimelineSwiftyAdditions.h"
#import "TimelineAnimationWeakLayerBox.h"
TimelineAnimationExceptionName ImmutableTimelineAnimationException = @"ImmutableTimelineAnimation";
TimelineAnimationExceptionName EmptyTimelineAnimationException = @"EmptyTimeline";
TimelineAnimationExceptionName ClearedTimelineAnimationException = @"ClearedTimelineAnimation";
TimelineAnimationExceptionName OngoingTimelineAnimationException = @"OngoingTimelineAnimation";
TimelineAnimationExceptionName TimelineAnimationTimeNotificationOutOfBoundsException = @"TimelineAnimationTimeNotificationOutOfBounds";
TimelineAnimationExceptionName TimelineAnimationMethodNotImplementedYetException = @"MethodNotImplementedYet";
TimelineAnimationExceptionName TimelineAnimationUnsupportedMessageException = @"UnsupportedMessage";
TimelineAnimationExceptionName TimelineAnimationConflictingAnimationsException = @"ConflictingAnimations";
TimelineAnimationExceptionName TimelineAnimationInvalidNumberOfBlocksException = @"TimelineAnimationInvalidNumberOfBlocksException";
TimelineAnimationExceptionName TimelineAnimationElementsNotInHierarchyException = @"TimelineAnimationElementsNotInHierarchyException";
NSErrorDomain const TimelineAnimationsErrorDomain = @"TimelineAnimationsErrorDomain";
NSErrorUserInfoKey const TimelineAnimationNameKey = @"name";
NSErrorUserInfoKey const TimelineAnimationSummaryKey = @"summary";
@interface TimelineAnimation ()
@property (nonatomic, strong) TimelineAnimationsDisplayLink *displayLink;
@property (nonatomic, strong) NSMutableSet<TimelineEntity *> *unfinishedEntities;
@property (nonatomic, assign) ObservationID progressObservationID;
@end
@implementation TimelineAnimation
@synthesize progress=_progress;
#pragma mark - Initializers
- (instancetype)initWithStart:(nullable TimelineAnimationOnStartBlock)onStart
completion:(nullable TimelineAnimationCompletionBlock)completion {
self = [super init];
if (self) {
_onStart = onStart;
self.onUpdate = nil;
_completion = completion;
_animations = [[NSMutableArray alloc] init];
_blankLayers = [[NSMutableArray alloc] init];
_progressNotificationAssociations = [[ProgressNotificationAssociations alloc] init];
_timeNotificationAssociations = [[NotificationAssociations alloc] init];
_paused = NO;
_speed = 1.0f;
_progress = 0.0f;
}
return self;
}
- (instancetype)init {
return [self initWithStart:nil completion:nil];
}
- (instancetype)initWithStart:(nullable TimelineAnimationOnStartBlock)onStart {
return [self initWithStart:onStart completion:nil];
}
- (instancetype)initWithUpdate:(TimelineAnimationOnUpdateBlock)onUpdate
preferredFramesPerSecond:(NSInteger)preferredFramesPerSecond {
NSParameterAssert(onUpdate != nil);
NSParameterAssert(preferredFramesPerSecond != 0);
self = [self initWithStart:nil completion:nil];
if (self) {
self.onUpdate = onUpdate;
self.preferredFramesPerSecond = preferredFramesPerSecond;
}
return self;
}
- (instancetype)initWithCompletion:(TimelineAnimationCompletionBlock)completion {
return [self initWithStart:nil completion:completion];
}
+ (instancetype)timelineAnimationWithCompletion:(TimelineAnimationCompletionBlock)completion {
return [[TimelineAnimation alloc] initWithCompletion:completion];
}
+ (instancetype)timelineAnimation {
return [[TimelineAnimation alloc] initWithStart:nil completion:nil];
}
+ (instancetype)timelineAnimationOnStart:(TimelineAnimationOnStartBlock)onStart
completion:(TimelineAnimationCompletionBlock)completion {
return [[TimelineAnimation alloc] initWithStart:onStart completion:completion];
}
- (void)dealloc {
[self _cleanUp];
// _blankLayers = nil;
// _animations = nil;
_originate = nil;
_parent = nil;
}
#pragma mark - On Update Methods -
- (void)setOnUpdate:(TimelineAnimationOnStartBlock)onUpdate {
if (onUpdate == nil) {
[self _removeDisplayLink];
_onUpdate = nil;
return;
}
_onUpdate = [onUpdate copy];
// Create the display link
[self _createDisplayLink];
}
#pragma mark - Display Link Methods -
- (void)_createDisplayLink {
self.displayLink = [TimelineAnimationsDisplayLink displayLinkPreferredFramesPerSecond:self.preferredFramesPerSecond
block:^(CFTimeInterval timestamp) {
[self displayLinkTick:timestamp];
}];
[self.displayLink pause];
}
- (void)_removeDisplayLink {
[self.displayLink stop];
self.displayLink = nil;
// [self.displayLink invalidate];
// self.displayLink = nil;
}
- (void)_startDisplayLinkIfNeeded {
if (self.displayLink) {
[self callOnStart];
[self.displayLink resume];
}
}
- (void)_pauseDisplayLink {
[self.displayLink pause];
}
- (void)displayLinkTick:(CFTimeInterval)timestamp {
if (_onUpdate != nil) {
_onUpdate();
}
else {
// if no update exist then the display link should cease to exist
[self _removeDisplayLink];
}
}
#pragma mark - Adding Animation Methods -
- (TimelineEntity *)lastEntity {
__block TimelineEntity *res = nil;
__block RelativeTime maxTime = 0;
for (TimelineEntity *const entity in _animations) {
const RelativeTime endTime = entity.endTime;
if (endTime >= maxTime) {
maxTime = endTime;
res = entity;
}
};
return res;
}
- (void)_addTimelineEntity:(TimelineEntity *)timelineEntity {
{ // check if already in
const BOOL alreadyIn = [_animations containsObject:timelineEntity];
guard (!alreadyIn) else {
NSIndexSet *const indexes = [_animations indexesOfObjectsPassingTest:^BOOL(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
const BOOL result = [entity isEqual:timelineEntity];
*stop = result;
return result;
}];
// raise conflict
TimelineEntity *const entity = _animations[indexes.firstIndex];
[self __raiseConflictingAnimationExceptionBetweenEntity:entity
andEntity:timelineEntity];
return;
}
}
{ // check if conflicting
NSIndexSet *const indexes = [_animations indexesOfObjectsPassingTest:^BOOL(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
const BOOL result = [entity conflictingWith:timelineEntity];
*stop = result;
return result;
}];
const BOOL conflicting = (indexes.count != 0);
guard (not(conflicting)) else {
// raise conflict
TimelineEntity *const entity = _animations[indexes.firstIndex];
[self __raiseConflictingAnimationExceptionBetweenEntity:entity
andEntity:timelineEntity];
return;
}
}
// add the timeline entity
[_animations addObject:timelineEntity];
}
#pragma mark - Animation Control Methods -
- (void)callOnStart {
// call general on start
if (!_onStartCalled && _onStart) {
_onStart();
_onStartCalled = YES;
}
if (self.isRepeating) {
guard (!_repeat.onStartCalled) else { return; }
if (_repeatOnStart) {
_repeatOnStart(_repeat.iteration);
_repeat.onStartCalled = YES;
}
}
}
- (void)callOnComplete:(BOOL)gracefullyFinished {
guard (_unfinishedEntities.count == 0) else { return; }
[self _callOnComplete:gracefullyFinished];
}
- (void)_callOnComplete:(BOOL)gracefullyFinished {
self.finished = YES;
self.started = NO;
// repeat
const BOOL repeats = [self _repeatIfNeededHasGracefullyFinished:gracefullyFinished];
if (repeats) { return; }
if ((_onCompletionCalled == NO) && (_completion != nil)) {
_completion(gracefullyFinished);
_onCompletionCalled = YES;
}
[self _removeDisplayLink];
}
- (BOOL)_repeatIfNeededHasGracefullyFinished:(BOOL)gracefullyFinished {
guard (self.isRepeating) else { return NO; }
guard (gracefullyFinished) else {
NSAssert(gracefullyFinished != NO,
@"TimelineAnimations: the following animation did not gracefully finish %@",
[self summary]);
return NO;
}
BOOL hasMoreIterations = (BOOL)(_repeat.iteration < _repeat.count) || (self.isInfinitelyRepeating);
// call repeatCompletion if any
if ((_repeatCompletion != nil) && not(_repeat.onCompleteCalled)) {
// inform the user that an iteration completed
// also ask him if he wants to stop
BOOL shouldStop = NO;
_repeatCompletion(gracefullyFinished, _repeat.iteration, &shouldStop);
hasMoreIterations = hasMoreIterations && not(shouldStop);
_repeat.onCompleteCalled = YES;
}
guard (hasMoreIterations) else { return NO; }
// increment iteration count
if ((TimelineAnimationRepeatIteration)UINT64_MAX - _repeat.iteration < (TimelineAnimationRepeatIteration)1ULL) {
_repeat.iteration = (TimelineAnimationRepeatIteration)0ULL;
}
_repeat.iteration += (TimelineAnimationRepeatIteration)1ULL;
guard (self.isNonEmpty) else { return NO; } // has animations
// replay
__weak typeof(self) welf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(self) strelf = welf;
guard (strelf != nil) else { return; }
guard (not(strelf.isPaused)) else { return; }
guard (not(strelf.isCleared)) else { return; }
[strelf _replay];
});
return YES;
}
- (TimelineAnimationRepeatCount)repeatCount {
// not possible to overflow as -setRepeatCount: always subtracts 1
return _repeat.count + (TimelineAnimationRepeatCount)1LL;
}
- (void)setRepeatCount:(TimelineAnimationRepeatCount)repeatCount {
guard (repeatCount >= (TimelineAnimationRepeatCount)1LL) else {
NSAssert(false, @"TimelineAnimations: Wrong repeat count. Should be greater than 0.");
return;
}
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
const TimelineAnimationRepeatCount realRepeatCount = repeatCount - (TimelineAnimationRepeatCount)1LL;
_repeat.count = realRepeatCount;
_repeat.iteration = (TimelineAnimationRepeatIteration)0LL;
_repeat.isRepeating = (realRepeatCount != (TimelineAnimationRepeatCount)0LL);
}
- (void)_replay {
[self _prepareForRepeat];
[self play];
}
- (void)_prepareForRepeat {
[self reset];
// restore the begin time for repeating animations.
// when a repeating timeline is included in a group timeline at an non-zero
// offset then we should bring the .beginTime back to zero, when -play
// is called. Trust boumis on this.
const RelativeTime begin = self.beginTime;
if (begin != 0.0) {
self.beginTime = 0.0;
}
}
- (void)reset {
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
// prepare for replay
for (TimelineEntity *const entity in _animations) {
[entity reset];
};
_repeat.onStartCalled = NO;
_repeat.onCompleteCalled = NO;
_onStartCalled = NO;
_onCompletionCalled = NO;
self.finished = NO;
}
- (void)_prepareForReplay {
_onStartCalled = NO;
_onCompletionCalled = NO;
self.finished = NO;
_repeat.iteration = (TimelineAnimationRepeatIteration)0LL;
_repeat.onStartCalled = NO;
_repeat.onCompleteCalled = NO;
}
- (void)pauseWithCurrentTime:(TimelineAnimationCurrentMediaTimeBlock)currentTime
alreadyPausedLayers:(nonnull NSMutableSet<__kindof CALayer *> *)pausedLayers {
self.paused = YES;
[_animations enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
__strong __kindof CALayer *const slayer = entity.layer;
if ([pausedLayers member:slayer]) {
entity.paused = YES;
return;
}
[entity pauseWithCurrentTime:currentTime];
[pausedLayers addObject:slayer];
}];
[self _pauseDisplayLink];
}
- (void)resumeWithCurrentTime:(TimelineAnimationCurrentMediaTimeBlock)currentTime
alreadyResumedLayers:(nonnull NSMutableSet<__kindof CALayer *> *)resumedLayers {
[_animations enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
__strong __kindof CALayer *const slayer = entity.layer;
if ([resumedLayers member:slayer]) {
entity.paused = NO;
return;
}
[entity resumeWithCurrentTime:currentTime];
[resumedLayers addObject:slayer];
}];
self.paused = NO;
[self _startDisplayLinkIfNeeded];
}
- (NSArray<TimelineEntity *> *)_sortedEntitesUsingKey:(NSString *)key {
NSSortDescriptor *const sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:key
ascending:YES];
NSArray<NSSortDescriptor *> *const descriptors = @[sortDescriptor];
NSArray<TimelineEntity *> *const sortedEntities = [_animations sortedArrayUsingDescriptors:descriptors];
return sortedEntities;
}
#pragma mark - Properties
- (RelativeTime)beginTime {
const RelativeTime begin = [self _sortedEntitesUsingKey:SortKey(beginTime)].firstObject.beginTime;
return begin;
}
- (void)setBeginTime:(RelativeTime)beginTime {
const RelativeTime currentMinBeginTime = self.beginTime;
[self delay:beginTime - currentMinBeginTime];
}
- (RelativeTime)endTime {
const RelativeTime endTime = [self _sortedEntitesUsingKey:SortKey(endTime)].lastObject.endTime;
if (self.isRepeating && !self.isInfinitelyRepeating) {
return (endTime - self.beginTime) * (RelativeTime)self.repeatCount + self.beginTime;
}
return endTime;
}
- (RelativeTime)endTimeWithNoRepeating {
const RelativeTime endTime = [self _sortedEntitesUsingKey:SortKey(endTime)].lastObject.endTime;
return endTime;
}
- (NSTimeInterval)duration {
return (NSTimeInterval)(self.endTime - self.beginTime);
}
- (void)setOnStart:(TimelineAnimationOnStartBlock)onStart {
NSAssert(onStart != nil, @"TimelineAnimations: Use -removeOnStartBlocks instead.");
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
TimelineAnimationOnStartBlock previous = [_onStart copy];
[self _setOnStart:^{
if (previous) {
previous();
}
if (onStart) {
onStart();
}
}];
}
// protected
- (void)_setOnStart:(TimelineAnimationOnStartBlock)onStart {
_onStart = [onStart copy];
}
- (void)_setCompletion:(TimelineAnimationCompletionBlock)completion {
_completion = [completion copy];
}
- (void)setCompletion:(TimelineAnimationCompletionBlock)completion {
NSAssert(completion != nil, @"TimelineAnimations: Use -removeCompletionBlocks instead.");
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
TimelineAnimationCompletionBlock previous = [_completion copy];
[self _setCompletion:^(BOOL finished){
if (previous) {
previous(finished);
}
if (completion) {
completion(finished);
}
}];
}
- (void)setSpeed:(float)speed {
if (speed < 0) {
speed = 0;
}
_speed = speed;
for (TimelineEntity *const entity in _animations) {
entity.speed = speed;
}
}
- (void)setStarted:(BOOL)started {
[self willChangeValueForKey:@"started"];
_started = started;
[self didChangeValueForKey:@"started"];
}
- (void)setPaused:(BOOL)paused {
[self willChangeValueForKey:@"paused"];
_paused = paused;
[self didChangeValueForKey:@"paused"];
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"finished"];
_finished = finished;
[self didChangeValueForKey:@"finished"];
if (finished == YES) {
[self _onFinish];
}
}
- (BOOL)isNonEmpty {
return !self.isEmpty;
}
- (BOOL)isEmpty {
return (_animations.count == 0);
}
- (BOOL)isRepeating {
return _repeat.isRepeating;
}
- (BOOL)isInfinitelyRepeating {
return (self.repeatCount == TimelineAnimationRepeatCountInfinite);
}
- (void)_onFinish {
[self _cleanUp];
}
- (void)_cleanUp {
if (self.progressObservationID) {
[[KeyValueBlockObservation observatory] removeObservationBlocksOfObject:self
forKeyPath:@"progress"
observationID:self.progressObservationID
context:NULL];
}
[_progressLayer removeAllAnimations];
[_progressLayer removeFromSuperlayer];
_progressLayer = nil;
[_blankLayers enumerateObjectsUsingBlock:^(TimelineAnimationsBlankLayer * _Nonnull layer, NSUInteger idx, BOOL * _Nonnull stop) {
[layer removeAllAnimations];
[layer removeFromSuperlayer];
}];
_blankLayers = [[NSMutableArray alloc] init];
// remove blank animations
NSMutableArray<TimelineEntity *> *const blankAnimations = [[NSMutableArray alloc] init];
[_animations enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
guard ([entity.animation.keyPath isEqualToString:TimelineAnimationsBlankLayer.keyPath]) else { return; }
[blankAnimations addObject:entity];
}];
[_animations removeObjectsInArray:blankAnimations];
}
- (NSTimeInterval)nonRepeatingDuration {
const RelativeTime begin = [self _sortedEntitesUsingKey:SortKey(beginTime)].firstObject.beginTime;
const RelativeTime end = [self _sortedEntitesUsingKey:SortKey(endTime)].lastObject.endTime;
return (end - begin);
}
- (NSSet<__kindof CALayer *> *)affectedLayers {
NSMutableSet<CALayer *> *const layers = [[NSMutableSet alloc] init];
[self.animations enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
__strong __kindof CALayer *const layer = entity.layer;
[layers addObject:layer];
}];
return [layers copy];
}
- (NotificationAssociations *)timeNotificationConvertedUsing:(NS_NOESCAPE TimeNotificationCalculation)calculation {
guard (self.timeNotificationAssociations.count > 0 ) else { return self.timeNotificationAssociations; }
NotificationAssociations *const updatedAssociations = [[NotificationAssociations alloc] initWithCapacity:self.timeNotificationAssociations.count];
[self.timeNotificationAssociations enumerateKeysAndObjectsUsingBlock:^(RelativeTimeNumber * _Nonnull key, NSMutableArray<TimelineAnimationNotifyBlockInfo *> *_Nonnull infos, BOOL * _Nonnull stop) {
RelativeTimeNumber *const newKey = calculation(key);
updatedAssociations[newKey] = [infos mutableCopy];
}];
return updatedAssociations;
}
- (TimelineAnimationCurrentMediaTimeBlock)currentTime {
const CFTimeInterval _currentTime = CACurrentMediaTime();
TimelineAnimationCurrentMediaTimeBlock currentTime = ^() {
return _currentTime;
};
return [currentTime copy];
}
- (CALayer *)anyLayer {
return self.cachedAffectedLayers.anyObject.layer;
}
#pragma mark - Exceptions
- (NSSet<TimelineAnimationWeakLayerBox *> *)cachedAffectedLayers {
guard (_cachedAffectedLayers != nil) else {
NSArray<TimelineAnimationWeakLayerBox *> *const boxes = [self.affectedLayers _map:^TimelineAnimationWeakLayerBox *_Nonnull(__kindof CALayer * _Nonnull layer) {
return [[TimelineAnimationWeakLayerBox alloc] initWithLayer:layer];
}];
_cachedAffectedLayers = [[NSSet alloc] initWithArray:boxes];
}
return _cachedAffectedLayers;
}
- (BOOL)_checkForOutOfHierarchyIssues {
// check for out of hierarchy problems
for (TimelineAnimationWeakLayerBox *const box in self.cachedAffectedLayers) {
__strong __kindof CALayer *const layer = box.layer;
guard (layer != nil) else { return YES; }
guard (layer.superlayer != nil) else { return YES; }
}
return NO;
}
- (void)__raiseConflictingAnimationExceptionBetweenEntity:(TimelineEntity *)entity1
andEntity:(TimelineEntity *)entity2 {
NSString *const reason = [[NSString alloc] initWithFormat:
@"Tried to add an animation to the "
"timeline that conflicts with another "
"animation that is already present.\n"
"%@\n"
"%@",
[entity1 debugDescription],
[entity2 debugDescription]];
#ifdef DEBUG
[self __raiseConflictingAnimationExceptionWithReason:reason, nil];
#else
// in RELEASE mode just log the stack trace.
NSArray<NSString *> *const symbols = [NSThread callStackSymbols];
NSString *const desc = @"Follows the stack trace:";
NSUInteger totalLength = ((NSNumber *)[symbols valueForKeyPath:@"@sum.length"]).unsignedIntegerValue;
NSMutableString *const string = [[NSMutableString alloc] initWithCapacity:totalLength + reason.length + desc.length];
[string appendFormat:@"%@\n%@", reason, desc];
[symbols enumerateObjectsUsingBlock:^(NSString * _Nonnull symbol, NSUInteger idx, BOOL * _Nonnull stop) {
[string appendFormat:@"\n%@", symbol];
}];
// always track, always use NSLog
NSLog(@"TimelineAnimations: %@", [string copy]);
#endif /* DEBUG */
}
- (void)__raiseImmutableTimelineExceptionWithSelector:(SEL)sel {
[self __raiseImmutableTimelineAnimationExceptionWithReason:
@"Tried to modify %@.%@ in selector: \"%@\""
"while the animation has already started.",
NSStringFromClass(self.class),
self.name,
NSStringFromSelector(sel)];
}
- (void)___raiseOrLogException:(nonnull TimelineAnimationExceptionName)exception
format:(nonnull NSString *)format
arguments:(va_list)arguments {
guard (TimelineAnimation.errorReporting != nil) else {
[self ___raiseException:exception
format:format
arguments:arguments];
return;
}
NSString *const reason = [[NSString alloc] initWithFormat:format
arguments:arguments];
NSDictionary<NSErrorUserInfoKey, id> *const userInfo = @{
TimelineAnimationNameKey: self.name ?: @"<no-name>",
TimelineAnimationSummaryKey: self.summary,
NSLocalizedDescriptionKey: exception,
NSLocalizedFailureReasonErrorKey: reason
};
const TimelineAnimationsErrorDomainCode code = [self.class errorCodeForException:exception];
NSError *const error = [NSError errorWithDomain:TimelineAnimationsErrorDomain
code:code
userInfo:userInfo];
TimelineAnimation.errorReporting(self, error);
}
- (void)___raiseException:(nonnull TimelineAnimationExceptionName)exception
format:(nonnull NSString *)format
arguments:(va_list)arguments {
NSString *const reason = [[NSString alloc] initWithFormat:format
arguments:arguments];
NSDictionary<NSErrorUserInfoKey, id> *const userInfo = @{
TimelineAnimationNameKey: self.name ?: @"<no-name>",
TimelineAnimationSummaryKey: self.summary,
};
@throw [NSException exceptionWithName:exception
reason:[@"TimelineAnimations: " stringByAppendingString:reason]
userInfo:userInfo];
}
+ (TimelineAnimationsErrorDomainCode)errorCodeForException:(TimelineAnimationExceptionName)exception {
if ([exception isEqual:ImmutableTimelineAnimationException]) {
return TimelineAnimationsErrorDomainCodeImmutbaleTimelineAnimation;
}
else if ([exception isEqual:EmptyTimelineAnimationException]) {
return TimelineAnimationsErrorDomainCodeEmptyTimelineAnimation;
}
else if ([exception isEqual:ClearedTimelineAnimationException]) {
return TimelineAnimationsErrorDomainCodeClearedTimelineAnimation;
}
else if ([exception isEqual:OngoingTimelineAnimationException]) {
return TimelineAnimationsErrorDomainCodeOngoingTimelineAnimation;
}
else if ([exception isEqual:TimelineAnimationTimeNotificationOutOfBoundsException]) {
return TimelineAnimationsErrorDomainCodeTimeNotificationOutOfBounds;
}
else if ([exception isEqual:TimelineAnimationUnsupportedMessageException]) {
return TimelineAnimationsErrorDomainCodeUnsupportedMesasge;
}
else if ([exception isEqual:TimelineAnimationConflictingAnimationsException]) {
return TimelineAnimationsErrorDomainCodeConflictingAnimations;
}
else if ([exception isEqual:TimelineAnimationInvalidNumberOfBlocksException]) {
return TimelineAnimationsErrorDomainCodeInvalidNumberOfBlocks;
}
else if ([exception isEqual:TimelineAnimationMethodNotImplementedYetException]) {
return TimelineAnimationsErrorDomainCodeMethodNotImplementedYet;
}
else if ([exception isEqual:TimelineAnimationElementsNotInHierarchyException]) {
return TimelineAnimationsErrorDomainCodeOutOfHierarchyException;
}
return TimelineAnimationsErrorDomainCodeMethodNotImplementedYet;
}
#define _RAISE_WITH_VA_LIST(e) {\
va_list arguments; \
va_start(arguments, format); \
[self ___raiseOrLogException:(e) \
format:format \
arguments:arguments]; \
va_end(arguments); \
}
- (void)__raiseTimeNotificationOutOfBoundsExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(TimelineAnimationTimeNotificationOutOfBoundsException);
}
- (void)__raiseNotImplementedMethodExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(TimelineAnimationMethodNotImplementedYetException);
}
- (void)__raiseOngoingTimelineAnimationWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(OngoingTimelineAnimationException);
}
- (void)__raiseClearedTimelineAnimationExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(ClearedTimelineAnimationException);
}
- (void)__raiseEmptyTimelineAnimationWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(EmptyTimelineAnimationException);
}
- (void)__raiseImmutableTimelineAnimationExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(ImmutableTimelineAnimationException);
}
- (void)__raiseUnsupportedMessageExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(TimelineAnimationUnsupportedMessageException);
}
- (void)__raiseConflictingAnimationExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(TimelineAnimationConflictingAnimationsException);
}
- (void)__raiseInvalidNumberOfBlocksExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(TimelineAnimationInvalidNumberOfBlocksException);
}
- (void)__raiseInvalidArgumentExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(NSInvalidArgumentException);
}
- (void)__raiseElementsNotInHierarchyExceptionWithReason:(nonnull NSString *)format, ... {
_RAISE_WITH_VA_LIST(TimelineAnimationElementsNotInHierarchyException);
}
#undef _RAISE_WITH_VA_LIST
#pragma mark - NSObject
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isMemberOfClass:[TimelineAnimation class]]) {
return NO;
}
TimelineAnimation *const other = (TimelineAnimation *)object;
const BOOL same = [other.animations isEqualToArray:_animations];
return same;
}
#pragma mark - Debug
- (NSString *)description {
return [[NSString alloc] initWithFormat:
@"<%@: %p; "
"name =\"%@\"; "
"beginTime = \"%.3lf\"; "
"endTime = \"%.3lf\"; "
"duration = \"%.3lf\"; "
"userInfo = %@;>",
NSStringFromClass(self.class),
(void *)self,
_name,
self.beginTime,
self.endTime,
self.duration,
_userInfo];
}
- (NSString *)debugDescription {
NSMutableString *string = [[NSMutableString alloc] initWithFormat:@"<%@: %p; "
"name = \"%@\"; "
"beginTime = \"%.3lf\"; "
"endTime = \"%.3lf\"; ",
NSStringFromClass(self.class),
(void *)self,
_name,
self.beginTime,
self.endTime
];
NSString *repeats = @"0";
if (self.isRepeating) {
if (self.isInfinitelyRepeating) {
repeats = @"inf";
}
else {
repeats = @(self.repeatCount).stringValue;
}
}
[string appendFormat:@"isRepeating(%@) = %@; ", repeats, @(self.isRepeating).stringValue];
if (self.isRepeating) {
[string appendFormat:@"duration = \"%.3lf\", ", self.nonRepeatingDuration];
if (self.isInfinitelyRepeating) {
[string appendFormat:@"repeatingDuration = infinite; "];
}
else {
[string appendFormat:@"repeatingDuration = \"%.3lf\"; ", self.duration];
}
}
[string appendFormat:@"userInfo = %@; "
"animations = %@; "
"timeNotifications = %@; "
"progressNotifications = %@;"
">",
_userInfo,
_animations.debugDescription,
_timeNotificationAssociations.allKeys,
_progressNotificationAssociations.allKeys];
return [string copy];
}
#pragma mark - Progress
- (void)setProgress:(float)progress {
[self willChangeValueForKey:@"progress"];
_progress = progress;
[self didChangeValueForKey:@"progress"];
}
- (void)_setupProgressMonitoring {
_progressLayer = [TimelineAnimationsProgressMonitorLayer layer];
__weak typeof(self) welf = self;
_progressLayer.progressBlock = ^(float progress) {
__strong typeof(self) strelf = welf;
strelf.progress = progress;
};
[_animations.firstObject.layer addSublayer:(CALayer *)_progressLayer];
CABasicAnimation *const anim = [CABasicAnimation animationWithKeyPath:@"progress"];
anim.duration = self.duration;
anim.fromValue = @(0.0);
anim.toValue = @(1.0);
[_progressLayer addAnimation:anim forKey:@"progress"];
}
- (void)_setupProgressNotifications {
// avoid heavy implementation if no progress observer are registered
guard (_progressNotificationAssociations.count > 0) else { return; }
[self _setupProgressMonitoring];
NSMutableSet<ProgressNumber *> *const unfinished = [NSMutableSet setWithArray:_progressNotificationAssociations.allKeys];
__weak typeof(self) welf = self;
self.progressObservationID = [[KeyValueBlockObservation observatory] addObservationBlock:^(NSString * _Nonnull keypath, TimelineAnimation * _Nonnull timeline, NSDictionary * _Nonnull change, void * _Nullable context) {
__strong typeof(self) strelf = welf;
guard (strelf != nil) else { return; }
guard (unfinished.count > 0) else { return; } // already finished
const float progress = ((ProgressNumber *)change[NSKeyValueChangeNewKey]).floatValue;
NSMutableSet<ProgressNumber *> *const finished = [NSMutableSet set];
[unfinished enumerateObjectsUsingBlock:^(ProgressNumber * _Nonnull progressNumber, BOOL * _Nonnull stop) {
const float progressKey = progressNumber.floatValue;
if (progress >= progressKey) {
((TimelineAnimationNotifyBlock)strelf.progressNotificationAssociations[progressNumber])(); // mind fuck, provided it to you by georges boumis :)
[finished addObject:progressNumber]; // mark this progress number as finished
}
}];
[unfinished minusSet:finished];
}
object:self
forKeyPath:@"progress"
options:NSKeyValueObservingOptionNew
context:NULL];
}
#pragma mark - Time Notifications
- (void)_setupTimeNotifications {
guard (_timeNotificationAssociations.count > 0) else { return; }
[_timeNotificationAssociations enumerateKeysAndObjectsUsingBlock:^(RelativeTimeNumber *_Nonnull key, NSMutableArray<TimelineAnimationNotifyBlockInfo *> *_Nonnull infos, BOOL * _Nonnull stop) {
const RelativeTime time = key.doubleValue;
__weak typeof(self) welf = self;
[self insertBlankAnimationAtTime:time
onStart:^{
__strong typeof(welf) strelf = welf;
guard (strelf != nil) else { return; }
[infos enumerateObjectsUsingBlock:^(TimelineAnimationNotifyBlockInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop2) {
[info call:strelf.muteAssociatedSounds];
}];
}
onComplete:nil
withDuration:TimelineAnimationOneFrame];
}];
}
- (void)insertBlankAnimationAtTime:(RelativeTime)time
onStart:(nullable TimelineAnimationOnStartBlock)start
onComplete:(nullable TimelineAnimationCompletionBlock)complete
withDuration:(NSTimeInterval)duration {
// do not uncomment this. it will break GroupTimelineAnimation
// guard (self.isNonEmpty) else { return; }
NSParameterAssert(duration >= TimelineAnimationOneFrame);
TimelineAnimationsBlankLayer *const blankLayer = [[TimelineAnimationsBlankLayer alloc] init];
CABasicAnimation *const blankAnimation = [CABasicAnimation animationWithKeyPath:TimelineAnimationsBlankLayer.keyPath];
blankAnimation.duration = duration;
__strong __kindof CALayer *const anyLayer = _animations.firstObject.layer;
[anyLayer addSublayer:blankLayer];
[_blankLayers addObject:blankLayer];
[self insertAnimation:blankAnimation
forLayer:blankLayer
atTime:time
onStart:start
onComplete:complete];
}
@end
#pragma mark - Populate
@implementation TimelineAnimation (Populate)
- (void)insertAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
atTime:(RelativeTime)time {
[self insertAnimation:animation
forLayer:layer
atTime:time
onStart:nil
onComplete:nil];
}
- (void)insertAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
atTime:(RelativeTime)time
onStart:(TimelineAnimationOnStartBlock)start {
NSParameterAssert(start != nil);
[self insertAnimation:animation
forLayer:layer
atTime:time
onStart:start
onComplete:nil];
}
- (void)insertAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
atTime:(RelativeTime)time
onComplete:(TimelineAnimationCompletionBlock)complete {
NSParameterAssert(complete != nil);
[self insertAnimation:animation
forLayer:layer
atTime:time
onStart:nil
onComplete:complete];
}
- (void)insertAnimations:(NSArray<__kindof CAPropertyAnimation *> *)animations
forLayer:(__kindof CALayer *)layer
atTime:(RelativeTime)time {
[self insertAnimations:animations
forLayer:layer
atTime:time
onStartBlocks:nil
completionBlocks:nil];
}
- (void)insertAnimations:(NSArray<__kindof CAPropertyAnimation *> *)animations
forLayer:(__kindof CALayer *)layer
atTime:(RelativeTime)time
onStartBlocks:(NSArray<TimelineAnimationOnStartBlock> *)onStartBlocks
completionBlocks:(NSArray<TimelineAnimationCompletionBlock> *)completionBlocks {
NSParameterAssert(animations != nil);
NSParameterAssert(animations.count > 0);
if (onStartBlocks != nil) {
NSParameterAssert(onStartBlocks.count == animations.count);
if (onStartBlocks.count != animations.count) {
[self __raiseInvalidNumberOfBlocksExceptionWithReason:
@"Wrong number of 'onStartBlocks' blocks. 'onStartBlocks' in not a "
"1:1 mapping with animations"];
return;
}
}
if (completionBlocks != nil) {
NSParameterAssert(completionBlocks.count == animations.count);
if (completionBlocks.count != animations.count) {
[self __raiseInvalidNumberOfBlocksExceptionWithReason:
@"Wrong number of 'completionBlocks' blocks. "
"'completionBlocks' in not a 1:1 mapping with animations"];
return;
}
}
[animations enumerateObjectsUsingBlock:^(__kindof CAPropertyAnimation * _Nonnull animation, NSUInteger idx, BOOL * _Nonnull stop) {
TimelineAnimationOnStartBlock onStart = nil;
if (onStartBlocks) {
onStart = onStartBlocks[idx];
}
TimelineAnimationCompletionBlock completion = nil;
if (completionBlocks) {
completion = completionBlocks[idx];
}
[self insertAnimation:animation
forLayer:layer
atTime:time
onStart:onStart
onComplete:completion];
}];
}
- (void)insertAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
atTime:(RelativeTime)time
onStart:(nullable TimelineAnimationOnStartBlock)start
onComplete:(nullable TimelineAnimationCompletionBlock)complete {
NSParameterAssert(animation != nil);
NSParameterAssert(layer != nil);
NSParameterAssert(animation.duration > 0.0);
NSParameterAssert(animation.keyPath != nil);
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
if (animation == nil) {
[self __raiseInvalidArgumentExceptionWithReason:
@"Tried to add a 'nil' animation to a %@",
NSStringFromClass(self.class)];
return;
}
if (layer == nil) {
[self __raiseInvalidArgumentExceptionWithReason:
@"Tried to add an animation with a 'nil' layer to a %@",
NSStringFromClass(self.class)];
return;
}
__kindof CAPropertyAnimation *const anim = animation.copy;
TimelineEntity *const entity = [[TimelineEntity alloc] initWithLayer:layer
animation:anim
beginTime:time
onStart:start
onComplete:complete
timelineAnimation:self];
[self _addTimelineEntity:entity];
}
- (void)addAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
withDelay:(NSTimeInterval)delay
onStart:(nullable TimelineAnimationOnStartBlock)onStart
onComplete:(nullable TimelineAnimationCompletionBlock)complete {
NSParameterAssert(animation != nil);
NSParameterAssert(layer != nil);
NSParameterAssert(animation.duration > 0.0);
NSParameterAssert(animation.keyPath != nil);
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
if (animation == nil) {
[self __raiseInvalidArgumentExceptionWithReason:
@"Tried to add a 'nil' animation to a %@",
NSStringFromClass(self.class)];
return;
}
if (layer == nil) {
[self __raiseInvalidArgumentExceptionWithReason:
@"Tried to add an animation with a 'nil' layer to a %@",
NSStringFromClass(self.class)];
return;
}
__kindof CAPropertyAnimation *const anim = animation.copy;
RelativeTime beginTime = 0.0;
TimelineEntity *const lastEntity = [self lastEntity];
if (lastEntity) {
beginTime = lastEntity.endTime + delay;
} else if (delay >= 0.0) {
beginTime = delay;
}
TimelineEntity *const entity = [[TimelineEntity alloc] initWithLayer:layer
animation:anim
beginTime:beginTime
onStart:onStart
onComplete:complete
timelineAnimation:self];
[self _addTimelineEntity:entity];
}
- (void)addAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
withDelay:(NSTimeInterval)delay
onStart:(TimelineAnimationOnStartBlock)onStart {
NSParameterAssert(onStart != nil);
[self addAnimation:animation
forLayer:layer
withDelay:delay
onStart:onStart
onComplete:nil];
}
- (void)addAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
withDelay:(NSTimeInterval)delay
onComplete:( TimelineAnimationCompletionBlock)complete {
NSParameterAssert(complete != nil);
[self addAnimation:animation
forLayer:layer
withDelay:delay
onStart:nil
onComplete:complete];
}
- (void)addAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
withDelay:(NSTimeInterval)delay {
[self addAnimation:animation
forLayer:layer
withDelay:delay
onStart:nil
onComplete:nil];
}
- (void)addAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
onStart:(TimelineAnimationOnStartBlock)onStart
onComplete:(TimelineAnimationCompletionBlock)complete {
NSParameterAssert(onStart != nil);
NSParameterAssert(complete != nil);
[self addAnimation:animation
forLayer:layer
withDelay:0.0
onStart:onStart
onComplete:complete];
}
- (void)addAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
onComplete:(TimelineAnimationCompletionBlock)complete {
NSParameterAssert(complete != nil);
[self addAnimation:animation
forLayer:layer
withDelay:0.0
onStart:nil
onComplete:complete];
}
- (void)addAnimation:(__kindof CAPropertyAnimation *)animation
forLayer:(__kindof CALayer *)layer
onStart:(TimelineAnimationOnStartBlock)onStart {
NSParameterAssert(onStart != nil);
[self addAnimation:animation
forLayer:layer
withDelay:0.0
onStart:onStart
onComplete:nil];
}
- (void)addAnimation:(__kindof CAPropertyAnimation *)animation forLayer:(__kindof CALayer *)layer {
[self addAnimation:animation
forLayer:layer
withDelay:0.0
onStart: nil
onComplete:nil];
}
- (void)addAnimations:(NSArray<__kindof CAPropertyAnimation *> *)animations
forLayer:(__kindof CALayer *)layer
withDelay:(NSTimeInterval)delay {
[self addAnimations:animations
forLayer:layer
withDelay:delay
onStartBlocks:nil
completionBlocks:nil];
}
- (void)addAnimations:(NSArray<__kindof CAPropertyAnimation *> *)animations
forLayer:(__kindof CALayer *)layer
withDelay:(NSTimeInterval)delay
onStartBlocks:(nullable NSArray<TimelineAnimationOnStartBlock> *)onStartBlocks
completionBlocks:(nullable NSArray<TimelineAnimationCompletionBlock> *)completionBlocks {
const RelativeTime time = self.duration + delay;
[self insertAnimations:animations
forLayer:layer
atTime:time
onStartBlocks:onStartBlocks
completionBlocks:completionBlocks];
}
- (void)merge:(TimelineAnimation *)timeline {
NSParameterAssert(timeline != nil);
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
guard ([timeline isMemberOfClass:[TimelineAnimation class]]) else {
NSAssert(false, @"TimelineAnimations: You should merge with same kinds of timelines.");
return;
}
// add only animations
[timeline.animations enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
// can throw
[self _addTimelineEntity:[entity copy]];
}];
if (timeline.onStart) {
self.onStart = timeline.onStart;
}
if (timeline.completion) {
self.completion = timeline.completion;
}
}
@end
#pragma mark - Control Blocks
@implementation TimelineAnimation (ControlBlocks)
- (void)removeOnStartBlocks {
_onStart = nil;
}
- (void)removeCompletionBlocks {
_completion = nil;
}
@end
#pragma mark - Control
@implementation TimelineAnimation (ProtectedControl)
- (void)_playWithCurrentTime:(TimelineAnimationCurrentMediaTimeBlock)currentTime {
NSAssert(self.name != nil, @"TimelineAnimations: You should name your animations.");
NSAssert(self.isNonEmpty || self.onUpdate != nil,
@"TimelineAnimations: Why are you trying to play an empty %@?",
NSStringFromClass(self.class));
if (self.isPaused) {
[self resume];
return;
}
if (self.hasStarted) {
[self __raiseOngoingTimelineAnimationWithReason:
@"TimelineAnimations: You tried to play a non paused or finished %@.\"%@\".",
NSStringFromClass(self.class),
self.name];
return;
}
if (self.isCleared) {
[self __raiseClearedTimelineAnimationExceptionWithReason:
@"TimelineAnimations: You tried to play the cleared %@.\"%@\"",
NSStringFromClass(self.class),
self.name
];
return;
}
self.paused = NO;
if (self.isEmpty && self.onUpdate == nil) {
if (_onStart) {
_onStart();
}
if (_completion) {
_completion(NO);
}
return;
}
[self _setupTimeNotifications];
[self _setupProgressNotifications];
if ([self _checkForOutOfHierarchyIssues]) {
[self __raiseElementsNotInHierarchyExceptionWithReason:
@"TimelineAnimations: You tried to play an animation with lost layers %@.\"%@\".",
NSStringFromClass(self.class),
self.name];
}
self.started = YES;
NSArray<TimelineEntity *> *const sortedEntities = [self _sortedEntitesUsingKey:SortKey(beginTime)];
_unfinishedEntities = [[NSMutableSet alloc] initWithArray:_animations];
[sortedEntities enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
entity.speed = self.speed;
[entity playWithCurrentTime:currentTime
onStart:^{
[self callOnStart];
} onComplete:^(BOOL gracefullyFinished) {
[self.unfinishedEntities removeObject:entity];
[self callOnComplete:gracefullyFinished];
} setModelValues:self.setsModelValues];
}];
[self _startDisplayLinkIfNeeded];
}
@end
@implementation TimelineAnimation (Control)
- (void)play {
[self _playWithCurrentTime:self.currentTime];
}
- (void)replay {
guard (!self.hasStarted) else { return; }
__weak typeof(self) welf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong typeof(self) strelf = welf;
guard (strelf != nil) else { return; }
guard (!strelf.isPaused) else { return; }
guard (!strelf.isCleared) else { return; }
[strelf _prepareForReplay];
[strelf _replay];
});
}
- (void)resume {
guard (self.isPaused) else { return; }
[self resumeWithCurrentTime:self.currentTime
alreadyResumedLayers:[[NSMutableSet alloc] init]];
}
- (void)pause {
guard (self.hasStarted) else { return; }
[self pauseWithCurrentTime:self.currentTime
alreadyPausedLayers:[[NSMutableSet alloc] init]];
}
- (void)clear {
for (TimelineEntity *const entity in _animations) {
[entity clear];
};
[_animations removeAllObjects];
self.paused = NO;
self.started = NO;
self.cleared = YES;
[self removeOnStartBlocks];
[self removeCompletionBlocks];
self.onUpdate = nil;
_progress = 0.0;
}
- (void)stopUpdates {
guard (self.hasStarted) else { return; }
guard (self.onUpdate != nil) else { return; }
self.onUpdate = nil;
self.paused = NO;
self.started = NO;
self.finished = YES;
}
- (void)delay:(const NSTimeInterval)delay {
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
guard (delay != 0.0) else { return; }
for (TimelineEntity *const entity in _animations) {
entity.beginTime += delay;
};
RelativeTime newBeginTime = self.beginTime;
// calculate notification time changes
_timeNotificationAssociations = [self timeNotificationConvertedUsing:^RelativeTimeNumber * _Nonnull(RelativeTimeNumber * _Nonnull key) {
RelativeTime new = Round(key.doubleValue + delay);
if (new <= newBeginTime) {
new = newBeginTime + TimelineAnimationMillisecond;
}
return @(new);
}];
}
- (instancetype)timelineWithDuration:(const NSTimeInterval)duration {
@autoreleasepool {
NSParameterAssert(duration > 0.0);
TimelineAnimation *const updatedTimeline = [self copy];
if ([updatedTimeline respondsToSelector:@selector(setSetsModelValues:)]) {
updatedTimeline.setsModelValues = self.setsModelValues;
}
const NSTimeInterval currentDuration = self.nonRepeatingDuration; {
// checks
const NSUInteger currentDurationInMilliseconds = (NSUInteger)(currentDuration * 1000.0); // in ms
const NSUInteger durationInMilliseconds = (NSUInteger)(duration * 1000.0); // in ms
// if same duration do nothing
guard (durationInMilliseconds != currentDurationInMilliseconds) else {
return updatedTimeline;
}
const NSUInteger millisecond = (NSUInteger)(TimelineAnimationMillisecond * 1000.0);
// if duration is only 1ms then do nothing
if (currentDurationInMilliseconds == millisecond) {
return updatedTimeline;
}
const NSUInteger frame = (NSUInteger)(TimelineAnimationOneFrame * 1000.0);
// if duration is only 16ms (one frame long) then do nothing
if (currentDurationInMilliseconds == frame) {
return updatedTimeline;
}
}
NSArray<TimelineEntity *> *const entities = _animations.copy;
NSMutableArray<TimelineEntity *> *const updatedEntities = [[NSMutableArray alloc] initWithCapacity:entities.count];
const NSTimeInterval newTimelineDuration = duration;
const NSTimeInterval oldTimelineDuration = currentDuration;
const RelativeTime beginTime = self.beginTime;
for (TimelineEntity *const entity in entities) {
// adjust if the entity's .beginTime is not the same as the timeline's .beginTime
const BOOL adjust = fabs((double)(entity.beginTime - beginTime)) >= TimelineAnimationMillisecond;
const NSTimeInterval newEntityDuration = newTimelineDuration * entity.duration / oldTimelineDuration;
TimelineEntity *const updatedEntity = [entity copyWithDuration:newEntityDuration
shouldAdjustBeginTime:adjust
usingTotalBeginTime:beginTime];
[updatedEntities addObject:updatedEntity];
};
updatedTimeline.animations = updatedEntities;
[updatedEntities enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
entity.timelineAnimation = updatedTimeline;
}];
for (TimelineEntity *const entity in entities) {
entity.timelineAnimation = updatedTimeline;
}
updatedTimeline.originate = self;
updatedTimeline.duration = newTimelineDuration;
// calculate notification time changes
const double factor = newTimelineDuration / oldTimelineDuration;
updatedTimeline.timeNotificationAssociations = [self timeNotificationConvertedUsing:^RelativeTimeNumber *(RelativeTimeNumber *key) {
double value = key.doubleValue;
if ((value == TimelineAnimationMillisecond)
|| (fabs((double)(value - TimelineAnimationMillisecond)) < 0.001)) {
return @(TimelineAnimationMillisecond);
}
// if around one frame time
if ((value == TimelineAnimationOneFrame)
|| (fabs((double)(value - TimelineAnimationOneFrame)) < 0.001)) {
return @(TimelineAnimationOneFrame);
}
value *= factor;
if (value < TimelineAnimationMillisecond) {
value = TimelineAnimationMillisecond;
}
return @(Round(value));
}];
return updatedTimeline;
}
}
@end
#pragma mark - Reverse
@implementation TimelineAnimation (Reverse)
- (instancetype)reversed {
return [self reversedWithDuration:self.duration];
}
- (instancetype)reversedWithDuration:(NSTimeInterval)duration {
NSParameterAssert(duration > 0.0);
NSArray<TimelineEntity *> *const sortedEntities = _animations.copy;
NSMutableArray<TimelineEntity *> *const reversedEntities = [[NSMutableArray alloc] initWithCapacity:sortedEntities.count];
const NSTimeInterval timelineDuration = duration;
for (TimelineEntity *const entity in sortedEntities) {
// reverse time
TimelineEntity *const reversedTimelineEntity = [entity reversedCopy];
const RelativeTime endTime = reversedTimelineEntity.endTime;
const RelativeTime beginTime = (timelineDuration - endTime);
reversedTimelineEntity.beginTime = beginTime;
[reversedEntities addObject:reversedTimelineEntity];
};
[reversedEntities sortUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"beginTime" ascending:YES]]];
TimelineAnimation *const reversed = [self copy];
[reversedEntities enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
entity.timelineAnimation = reversed;
}];
if (self.setsModelValues) {
reversed.setsModelValues = YES;
}
reversed.animations = reversedEntities;
reversed.name = [reversed.name stringByAppendingPathExtension:@"reversed"];
reversed.reversed = YES;
reversed.originate = self;
return reversed;
}
@end
#pragma mark - Progress
@implementation TimelineAnimation (Progress)
- (void)playFromProgress:(float)progress catchUpIn:(NSTimeInterval)intervalToCatchUp {
NSParameterAssert(progress >= 0.0 && progress <= 1.0);
[self __raiseNotImplementedMethodExceptionWithReason:
@"The functionality provided by selector "
"\"%@\" is not yet implemented.",
NSStringFromSelector(_cmd)];
}
- (void)playFromProgress:(float)progress {
if (self.hasStarted) {
[self __raiseImmutableTimelineExceptionWithSelector:_cmd];
return;
}
NSParameterAssert(progress >= 0.0 && progress <= 1.0);
if (progress < 0.0) {
progress = 0.0;
}
if (progress > 1.0) {
progress = 1.0;
}
const NSTimeInterval duration = self.duration;
const RelativeTime beginTime = self.beginTime;
const NSTimeInterval diff = duration * progress;
const RelativeTime newBeginTime = beginTime - diff;
self.beginTime = newBeginTime;
[self play];
}
@end
#pragma mark - Notify
@implementation TimelineAnimation (Notify)
- (void)notifyAtProgress:(float)progress
usingBlock:(TimelineAnimationNotifyBlock)block {
if (self.hasStarted) {
[self __raiseImmutableTimelineAnimationExceptionWithReason:
@"You tried to add a progress notification at the ongoing %@.\"%@\"",
NSStringFromClass(self.class),
self.name];
return;
}
NSParameterAssert(progress >= 0.0 && progress <= 1.0);
ProgressNumber *const progressKey = @(progress);
_progressNotificationAssociations[progressKey] = [block copy];
}
- (void)notifyAtTime:(RelativeTime)time
usingBlock:(TimelineAnimationNotifyBlock)block {
if (self.hasStarted) {
[self __raiseImmutableTimelineAnimationExceptionWithReason:
@"You tried to add a time notification %.3lf at the ongoing %@.\"%@\"",
time,
NSStringFromClass(self.class),
self.name];
return;
}
if (time < (RelativeTime)self.beginTime) {
[self __raiseTimeNotificationOutOfBoundsExceptionWithReason:
@"Tried to add a time notification at %.3lf in %@.\"%@\", before its beginTime(%.3lf).",
time,
NSStringFromClass(self.class),
self.name,
self.beginTime];
return;
}
if (self.isEmpty) {
[self __raiseEmptyTimelineAnimationWithReason:
@"Tried to add a time notification at %.3lf in an empty %@.\"%@\"",
time,
NSStringFromClass(self.class),
self.name];
return;
}
if (time >= self.endTimeWithNoRepeating) {
[self __raiseTimeNotificationOutOfBoundsExceptionWithReason:
@"Tried to add a time notification at %.3lf in %@.\"%@\", after its endTime(%.3lf).",
time,
NSStringFromClass(self.class),
self.name,
self.endTimeWithNoRepeating];
return;
}
TimelineAnimationNotifyBlockInfo *const info = [TimelineAnimationNotifyBlockInfo infoWithBlock:block
isSoundNotification:NO];
[self _appendTimelineAnimationNotifyBlockInfo:info atTime:time];
}
- (void)_appendTimelineAnimationNotifyBlockInfo:(TimelineAnimationNotifyBlockInfo *)info atTime:(RelativeTime)time {
RelativeTimeNumber *const timeKey = @(Round(time));
NSMutableArray<TimelineAnimationNotifyBlockInfo *> *infos = _timeNotificationAssociations[timeKey];
if (infos == nil) {
infos = [[NSMutableArray alloc] init];
_timeNotificationAssociations[timeKey] = infos;
}
[infos addObject:info];
}
- (void)addBlankAnimationWithDuration:(NSTimeInterval)duration {
[self addBlankAnimationWithDuration:duration
onStart:nil
onComplete:nil];
}
- (void)addBlankAnimationWithDuration:(NSTimeInterval)duration
onStart:(TimelineAnimationOnStartBlock)start {
[self addBlankAnimationWithDuration:duration
onStart:start
onComplete:nil];
}
- (void)addBlankAnimationWithDuration:(NSTimeInterval)duration
onComplete:(TimelineAnimationCompletionBlock)complete {
[self addBlankAnimationWithDuration:duration
onStart:nil
onComplete:complete];
}
- (void)addBlankAnimationWithDuration:(NSTimeInterval)duration
onStart:(nullable TimelineAnimationOnStartBlock)start
onComplete:(nullable TimelineAnimationCompletionBlock)complete {
guard (self.isNonEmpty) else { return; }
NSParameterAssert(duration >= TimelineAnimationOneFrame);
TimelineAnimationsBlankLayer *const blankLayer = [[TimelineAnimationsBlankLayer alloc] init];
CABasicAnimation *const blankAnimation = [CABasicAnimation animationWithKeyPath:TimelineAnimationsBlankLayer.keyPath];
blankAnimation.duration = duration;
__strong __kindof CALayer *const anyLayer = _animations.firstObject.layer;
NSAssert(anyLayer != nil, @"TimelineAnimations: Try to add blank animation but there is no layer to add it to.");
[anyLayer addSublayer:blankLayer];
[_blankLayers addObject:blankLayer];
[self addAnimation:blankAnimation
forLayer:blankLayer
onStart:start
onComplete:complete];
}
- (void)insertBlankAnimationAtTime:(RelativeTime)time
withDuration:(NSTimeInterval)duration {
[self insertBlankAnimationAtTime:time
onStart:nil
onComplete:nil
withDuration:duration];
}
- (void)insertBlankAnimationAtTime:(RelativeTime)time
onStart:(TimelineAnimationOnStartBlock)start
withDuration:(NSTimeInterval)duration {
[self insertBlankAnimationAtTime:time
onStart:start
onComplete:nil
withDuration:duration];
}
- (void)insertBlankAnimationAtTime:(RelativeTime)time
onComplete:(TimelineAnimationCompletionBlock)complete
withDuration:(NSTimeInterval)duration {
[self insertBlankAnimationAtTime:time
onStart:nil
onComplete:complete
withDuration:duration];
}
@end
#pragma mark - Audio
@implementation TimelineAnimation (Audio)
- (void)associateAudio:(id<TimelineAudio>)audio
usingTimeAssociation:(TimelineAudioAssociation *)association {
if (self.hasStarted) {
[self __raiseImmutableTimelineAnimationExceptionWithReason:
@"You tried to associate audio at the ongoing %@.\"%@\"",
NSStringFromClass(self.class),
self.name];
return;
}
if (self.isEmpty) {
[self __raiseEmptyTimelineAnimationWithReason:
@"Tried to associate sound in an empty %@.\"%@\"",
NSStringFromClass(self.class),
self.name];
return;
}
const RelativeTime time = [association timeInTimelineAnimation:self];
if (time < (RelativeTime)self.beginTime) {
[self __raiseTimeNotificationOutOfBoundsExceptionWithReason:
@"Tried to associate audio at %.3lf in %@.\"%@\", before its beginTime(%.3lf).",
time,
NSStringFromClass(self.class),
self.name,
self.beginTime];
return;
}
if (time >= self.endTimeWithNoRepeating) {
[self __raiseTimeNotificationOutOfBoundsExceptionWithReason:
@"Tried to associate audio at %.3lf in %@.\"%@\", after its endTime(%.3lf).",
time,
NSStringFromClass(self.class),
self.name,
self.endTimeWithNoRepeating];
return;
}
TimelineAnimationNotifyBlockInfo *const info = [TimelineAnimationNotifyBlockInfo infoWithBlock:^{
[audio play];
} isSoundNotification:YES];
info.sound = audio;
[self _appendTimelineAnimationNotifyBlockInfo:info atTime:time];
}
- (void)disassociateAllAudio {
if (self.hasStarted) {
[self __raiseImmutableTimelineAnimationExceptionWithReason:
@"You tried disassociate audio at the ongoing %@.\"%@\"",
NSStringFromClass(self.class),
self.name];
return;
}
@autoreleasepool {
NotificationAssociations *const timeNotifications = [NotificationAssociations dictionaryWithSharedKeySet:
[NotificationAssociations sharedKeySetForKeys:_timeNotificationAssociations.allKeys]];
[_timeNotificationAssociations enumerateKeysAndObjectsUsingBlock:^(RelativeTimeNumber * _Nonnull timeKey, NSMutableArray<TimelineAnimationNotifyBlockInfo *> * _Nonnull infos, BOOL * _Nonnull stop) {
// get all non-sound notifications
NSIndexSet *const indexes = [infos indexesOfObjectsPassingTest:^BOOL(TimelineAnimationNotifyBlockInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop2) {
return !info.isSoundNotification;
}];
if (indexes.count == 0) {
timeNotifications[timeKey] = nil;
}
else {
NSMutableArray<TimelineAnimationNotifyBlockInfo *> *const newInfos = [[infos objectsAtIndexes:indexes] mutableCopy];
timeNotifications[timeKey] = newInfos;
}
}];
_timeNotificationAssociations = timeNotifications;
}
}
- (void)disassociateAudio:(id<TimelineAudio>)audio {
if (self.hasStarted) {
[self __raiseImmutableTimelineAnimationExceptionWithReason:
@"You tried to disassociate audio at the ongoing %@.\"%@\"",
NSStringFromClass(self.class),
self.name];
return;
}
@autoreleasepool {
NotificationAssociations *const timeNotifications = [NotificationAssociations dictionaryWithSharedKeySet:
[NotificationAssociations sharedKeySetForKeys:_timeNotificationAssociations.allKeys]];
[_timeNotificationAssociations enumerateKeysAndObjectsUsingBlock:^(RelativeTimeNumber * _Nonnull timeKey, NSMutableArray<TimelineAnimationNotifyBlockInfo *> * _Nonnull infos, BOOL * _Nonnull stop) {
// get all notifications where the sound is different from the requested one
NSIndexSet *const indexes = [infos indexesOfObjectsPassingTest:^BOOL(TimelineAnimationNotifyBlockInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop2) {
return (info.sound != audio);
}];
if (indexes.count == 0) {
timeNotifications[timeKey] = nil;
}
else {
NSMutableArray<TimelineAnimationNotifyBlockInfo *> *const newInfos = [[infos objectsAtIndexes:indexes] mutableCopy];
timeNotifications[timeKey] = newInfos;
}
}];
_timeNotificationAssociations = timeNotifications;
}
}
- (void)disassociateAudioAtTimeAssociation:(TimelineAudioAssociation *)association {
if (self.hasStarted) {
[self __raiseImmutableTimelineAnimationExceptionWithReason:
@"You tried disassociate time association %.3lf notification at the ongoing %@.\"%@\"",
[association timeInTimelineAnimation:self],
NSStringFromClass(self.class),
self.name];
return;
}
const RelativeTime time = [association timeInTimelineAnimation:self];
RelativeTimeNumber *const timeKey = @(time);
NSMutableArray<TimelineAnimationNotifyBlockInfo *> *const infos = _timeNotificationAssociations[timeKey];
NSIndexSet *const indexes = [infos indexesOfObjectsPassingTest:^BOOL(TimelineAnimationNotifyBlockInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop) {
return !info.isSoundNotification;
}];
NSMutableArray<TimelineAnimationNotifyBlockInfo *> *const newInfos = [[infos objectsAtIndexes:indexes] mutableCopy];
_timeNotificationAssociations[timeKey] = newInfos;
}
- (NSArray<id<TimelineAudio>> *)associatedAudioBeginingAtTime:(RelativeTime)time {
if (self.hasStarted || self.hasFinished) {
[self __raiseOngoingTimelineAnimationWithReason:
@"You tried to associate audio with an ongoing %@.\"%@\".",
NSStringFromClass(self.class),
self.name];
return @[];
}
RelativeTimeNumber *const timeKey = @(time);
NSMutableArray<TimelineAnimationNotifyBlockInfo *> *infos = _timeNotificationAssociations[timeKey];
guard (infos != nil) else { return @[]; }
guard (infos.count != 0) else { return @[]; }
NSArray<TimelineAnimationNotifyBlockInfo *> *sounds = [infos _objectsPassingTest:^BOOL(TimelineAnimationNotifyBlockInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop) {
return info.isSoundNotification;
}];
guard (sounds.count != 0) else { return @[]; }
return [sounds _map:^id _Nonnull(TimelineAnimationNotifyBlockInfo * _Nonnull info) { return info.sound; }];
}
- (NSArray<id<TimelineAudio>> *)associatedOngoingAtTime:(RelativeTime)time {
if (self.hasStarted || self.hasFinished) {
[self __raiseOngoingTimelineAnimationWithReason:
@"You tried to associate audio with an ongoing %@.\"%@\".",
NSStringFromClass(self.class),
self.name];
return @[];
}
NSAssert(NO, @"Not implemented yet");
NSMutableArray<TimelineAnimationNotifyBlockInfo *> *const ongoingSounds = [[NSMutableArray alloc] init];
[_timeNotificationAssociations enumerateKeysAndObjectsUsingBlock:^(RelativeTimeNumber * _Nonnull keyTime, NSMutableArray<TimelineAnimationNotifyBlockInfo *> * _Nonnull infos, BOOL * _Nonnull stop) {
const RelativeTime beginTime = keyTime.doubleValue;
guard (time >= beginTime) else { return; }
NSArray<TimelineAnimationNotifyBlockInfo *> *_ongoingSounds =
[infos _objectsPassingTest:^BOOL(TimelineAnimationNotifyBlockInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop2) {
__strong typeof(info.sound) sound = info.sound;
guard (sound != nil) else { return NO; }
const RelativeTime endTime = beginTime + sound.duration;
return (time <= endTime);
}];
[ongoingSounds addObjectsFromArray:_ongoingSounds];
}];
return [ongoingSounds copy];
}
- (NSArray<id<TimelineAudio>> *)associatedAudios {
return [[self _audioBlockInfos] _map:^id<TimelineAudio>(TimelineAnimationNotifyBlockInfo *info) { return info.sound; } ];
}
- (NSArray<TimelineAnimationNotifyBlockInfo *> *)_audioBlockInfos {
__block NSMutableArray<TimelineAnimationNotifyBlockInfo *> *blocks = [[NSMutableArray alloc] init];
[_timeNotificationAssociations enumerateKeysAndObjectsUsingBlock:^(RelativeTimeNumber * _Nonnull timeKey, NSMutableArray<TimelineAnimationNotifyBlockInfo *> * _Nonnull infos, BOOL * _Nonnull stop) {
// get all sound notifications
NSArray<TimelineAnimationNotifyBlockInfo *> *const sounds = [infos _objectsPassingTest:^BOOL(TimelineAnimationNotifyBlockInfo * _Nonnull info, NSUInteger idx, BOOL * _Nonnull stop2) {
return info.isSoundNotification;
}];
[blocks addObjectsFromArray:sounds];
}];
return [blocks copy];
}
@end
#pragma mark - NSCopying
@implementation TimelineAnimation (Copying)
- (instancetype)initWithTimelineAnimation:(__kindof TimelineAnimation *)timeline {
self = [self initWithStart:timeline.onStart
completion:timeline.completion];
if (self) {
_preferredFramesPerSecond = timeline.preferredFramesPerSecond;
self.onUpdate = timeline.onUpdate;
_animations = [[NSMutableArray alloc] initWithArray:timeline.animations
copyItems:YES];
[_animations enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
entity.timelineAnimation = self;
}];
_paused = timeline.paused;
_finished = timeline.finished;
_speed = timeline.speed;
self.beginTime = timeline.beginTime;
self.repeatCount = timeline.repeatCount;
_repeatOnStart = [timeline.repeatOnStart copy];
_repeatCompletion = [timeline.repeatCompletion copy];
_setsModelValues = timeline.setsModelValues;
_name = timeline.name.copy;
_userInfo = timeline.userInfo.copy;
_completion = [timeline.completion copy];
_onStart = [timeline.onStart copy];
_onUpdate = [timeline.onUpdate copy];
_progress = timeline.progress;
_reversed = timeline.reversed;
_originate = timeline.originate;
_muteAssociatedSounds = timeline.muteAssociatedSounds;
_progressNotificationAssociations = timeline.progressNotificationAssociations.mutableCopy;
_timeNotificationAssociations = timeline.timeNotificationAssociations.mutableCopy;
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
return [[TimelineAnimation alloc] initWithTimelineAnimation:self];
}
@end
@implementation TimelineAnimation (Debug)
- (NSString *)summary {
NSMutableString *const summary = [[NSMutableString alloc] initWithFormat:@"\"%@\": ", self.name];
[summary appendFormat:@"duration: \"%.3lf\"; ", self.duration];
[summary appendFormat:@"animations(%@) = [\n", @(_animations.count)];
NSArray<TimelineEntity *> *const sorted = [self _sortedEntitesUsingKey:SortKey(beginTime)];
[sorted enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
__strong __kindof CALayer *const slayer = entity.layer;
[summary appendFormat:@"\t%@: time: [%.3lf,%.3lf], keypath: \"%@\", layer(%p): (%@ of %@)\n",
@(idx), entity.beginTime,
entity.endTime,
entity.animation.keyPath,
slayer,
NSStringFromClass(slayer.class),
NSStringFromClass(slayer.delegate.class)];
}];
[summary appendFormat:@"]"];
return [summary copy];
}
- (NSArray<__kindof CAPropertyAnimation *> *)animationsBeginingAtTime:(RelativeTime)time {
NSIndexSet *const indexes = [_animations indexesOfObjectsPassingTest:^BOOL(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
return (entity.beginTime == time);
}];
NSArray<TimelineEntity *> *const entities = [_animations objectsAtIndexes:indexes];
NSArray<__kindof CAPropertyAnimation *> *const animations = [[NSArray alloc] initWithArray:
[entities _map:^__kindof CAPropertyAnimation *(TimelineEntity *entity) { return [entity.animation copy]; }]
];
return animations;
}
- (NSArray<__kindof CAPropertyAnimation *> *)animationsOngoingAtTime:(RelativeTime)time {
NSIndexSet *const indexes = [_animations indexesOfObjectsPassingTest:^BOOL(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
return (time >= entity.beginTime) && (time <= entity.endTime);
}];
NSArray<TimelineEntity *> *const entities = [_animations objectsAtIndexes:indexes];
NSArray<__kindof CAPropertyAnimation *> *const animations = [[NSArray alloc] initWithArray:
[entities _map:^__kindof CAPropertyAnimation *(TimelineEntity *entity) { return [entity.animation copy]; }]
];
return animations;
}
- (NSArray<CAPropertyAnimation *> *)allPropertyAnimations {
return [_animations _map:^__kindof CAPropertyAnimation *(TimelineEntity * _Nonnull entity) {
return [entity.animation copy];
}];
}
@end
@implementation TimelineAnimation (Plumbing)
- (NSArray<TimelineAnimationDescription *> *)animationDescriptions {
if (self.hasStarted) {
[self __raiseOngoingTimelineAnimationWithReason:
@"Animation descriptions are not available on ongoing %@.\"%@\".",
NSStringFromClass(self.class),
self.name];
return @[];
}
return [_animations _map:^__kindof TimelineAnimationDescription *(TimelineEntity * _Nonnull entity) {
return [TimelineAnimationDescription descriptionWithAnimation:entity.initialAnimation
forLayer:entity.layer
onStart:entity.onStart
completion:entity.completion];
}];
}
- (void)combineAnimationDescriptions:(NSArray<TimelineAnimationDescription *> *)animationDescriptions {
if (self.hasStarted) {
[self __raiseOngoingTimelineAnimationWithReason:
@"You tried to play an non paused or finished %@.\"%@\".",
NSStringFromClass(self.class),
self.name];
return;
}
NSParameterAssert(animationDescriptions != nil);
guard (animationDescriptions != nil) else { return; }
NSParameterAssert(animationDescriptions.count > 0);
guard (animationDescriptions.count > 0) else { return; }
[animationDescriptions enumerateObjectsUsingBlock:^(TimelineAnimationDescription * _Nonnull description, NSUInteger idx, BOOL * _Nonnull stop) {
__kindof CAPropertyAnimation *animation = description.animation;
[self insertAnimation:animation
forLayer:description.layer
atTime:animation.beginTime
onStart:description.onStart
onComplete:description.completion];
}];
}
- (id)debugQuickLookObject {
return self.debugDescription;
// @autoreleasepool {
//
// NSArray<TimelineEntity *> *const entities = [self _sortedEntitesUsingKey:SortKey(beginTime)];
// const RelativeTime enArxi = entities.firstObject.beginTime;
//
// CGSize size = [[entities _reduce:[NSValue valueWithCGSize:CGSizeZero]
// transform:^NSValue *_Nonnull(NSValue *_Nonnull partial, TimelineEntity * _Nonnull entity) {
// CGSize size = partial.CGSizeValue;
// const CGSize layerSize = entity.layer.preferredFrameSize;
// size.width += layerSize.width;
// size.height = MAX(size.height, layerSize.height);
// return [NSValue valueWithCGSize:size];
// }] CGSizeValue];
//
// size.height += 50.0;
// UIGraphicsBeginImageContextWithOptions(size, NO, 0.0);
// const SEL quickLook = @selector(debugQuickLookObject);
// __block CGFloat currentX = 0.0;
// [[UIColor blackColor] setFill];
// [[UIBezierPath bezierPathWithRect:CGRectMake(0, 0, size.width, size.height)] fill];
//
// [entities enumerateObjectsUsingBlock:^(TimelineEntity * _Nonnull entity, NSUInteger idx, BOOL * _Nonnull stop) {
// __kindof CALayer *const layer = entity.layer;
//
// if ([layer respondsToSelector:quickLook]) {
// _Pragma("clang diagnostic push");
// _Pragma("clang diagnostic ignored \"-Warc-performSelector-leaks\"");
// UIImage *const image = [layer performSelector:quickLook];
// _Pragma("clang diagnostic pop");
// const CGSize imageSize = image.size;
// const CGFloat width = imageSize.width;
// const CGFloat height = imageSize.height;
// const CGRect imageRect = CGRectMake(currentX, size.height - height,
// width, height);
//
// [image drawInRect:imageRect];
//
// {
// [self _drawString:[NSString stringWithFormat:
// @"\"%@\" %ldms",
// entity.animation.keyPath,
// (long)(entity.duration * 1000.0)]
// inRect:CGRectMake(currentX, 0, width, 25.0)];
// [self _drawString:[NSString stringWithFormat:
// @"[%.3lf,%.3lf]",
// entity.beginTime - enArxi,
// entity.endTime - enArxi]
// inRect:CGRectMake(currentX, 25.0, width, 25.0)];;
// }
//
// currentX += width;
// }
// }];
// UIImage *const preview = UIGraphicsGetImageFromCurrentImageContext();
// UIGraphicsEndImageContext();
// return preview;
// }
}
- (void)_drawString:(NSString *)string inRect:(CGRect)rect {
NSDictionary<NSString *, id> *attributes =
[self _findAttributesOfString:string
toFitSize:rect.size
staringWithInitialFontSize:20];
const CGRect stringRect = CGRectMake(rect.origin.x, rect.origin.y,
rect.size.width, rect.size.height);
[string drawInRect:stringRect
withAttributes:attributes];
}
- (NSDictionary<NSString *, id> *)_findAttributesOfString:(NSString *)string
toFitSize:(CGSize)size
staringWithInitialFontSize:(CGFloat)fontSize {
NSDictionary<NSString *, id> *initialAttributes = @{
NSForegroundColorAttributeName: [UIColor whiteColor],
NSFontAttributeName: [UIFont systemFontOfSize:fontSize]
};
CGSize propertiesSize = [string boundingRectWithSize:size
options:(NSStringDrawingUsesLineFragmentOrigin)
attributes:initialAttributes
context:nil].size;
if (propertiesSize.width > size.width || propertiesSize.height > size.height) {
return [self _findAttributesOfString:string
toFitSize:size
staringWithInitialFontSize:(fontSize * (CGFloat)0.05)];
}
return initialAttributes;
}
@end
@implementation TimelineAnimation (ErrorReporting)
static const void *const __kErrorReportingKey = &__kErrorReportingKey;
+ (void)setErrorReporting:(TimelineAnimationErrorReportingBlock)errorReporting {
objc_setAssociatedObject(self,
__kErrorReportingKey,
errorReporting,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
+ (TimelineAnimationErrorReportingBlock)errorReporting {
TimelineAnimationErrorReportingBlock block = objc_getAssociatedObject(self, __kErrorReportingKey);
return [block copy];
}
@end