-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathMCCStateMachine.m
204 lines (161 loc) · 7.29 KB
/
MCCStateMachine.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
//
// MCCStateMachine.h
// MailCommon
//
// Created by Scott Little on 11/01/15.
// Copyright (c) 2015 Little Known Software. All rights reserved.
//
// Copied from Apple's example code "AdvancedUserInterfacesUsingCollectionView" project, thus:
// Copyright (C) 2014 Apple Inc. All Rights Reserved.
//
#import "MCCStateMachine.h"
#import <objc/message.h>
#import <libkern/OSAtomic.h>
static NSString * const MCC_PREFIXED_CONSTANT(StateNil) = @"Nil";
@implementation MCC_PREFIXED_NAME(StateMachine) {
OSSpinLock _lock;
}
@synthesize currentState = _currentState;
- (instancetype)init {
self = [super init];
if (!self)
return nil;
_lock = OS_SPINLOCK_INIT;
return self;
}
- (id)target {
id<MCC_PREFIXED_NAME(StateMachineDelegate)> delegate = self.delegate;
if (delegate) {
return delegate;
}
return self;
}
- (NSString *)currentState {
__block NSString *currentState;
// for atomic-safety, _currentState must not be released between the load of _currentState and the retain invocation
OSSpinLockLock(&_lock);
currentState = _currentState;
OSSpinLockUnlock(&_lock);
return currentState;
}
- (BOOL)applyState:(NSString *)toState {
return [self _setCurrentState:toState];
}
- (void)setCurrentState:(NSString *)toState {
[self _setCurrentState:toState];
}
- (BOOL)_setCurrentState:(NSString *)toState {
NSString *fromState = self.currentState;
if (self.shouldLogStateTransitions)
NSLog(@" ••• request state change from %@ to %@", fromState, toState);
NSString *appliedToState = [self _validateTransitionFromState:fromState toState:toState];
if (!appliedToState)
return NO;
// ...send will-change message for downstream KVO support...
id target = [self target];
SEL genericWillChangeAction = @selector(stateWillChange);
if ([target respondsToSelector:genericWillChangeAction]) {
typedef void (*ObjCMsgSendReturnVoid)(id, SEL);
ObjCMsgSendReturnVoid sendMsgReturnVoid = (ObjCMsgSendReturnVoid)objc_msgSend;
sendMsgReturnVoid(target, genericWillChangeAction);
}
OSSpinLockLock(&_lock);
_currentState = [appliedToState copy];
OSSpinLockUnlock(&_lock);
// ... send messages
[self _performTransitionFromState:fromState toState:appliedToState];
return [toState isEqual:appliedToState];
}
- (NSString *)_missingTransitionFromState:(NSString *)fromState toState:(NSString *)toState {
if ([_delegate respondsToSelector:@selector(missingTransitionFromState:toState:)])
return [_delegate missingTransitionFromState:fromState toState:toState];
return [self missingTransitionFromState:fromState toState:toState];
}
- (NSString *)missingTransitionFromState:(NSString *)fromState toState:(NSString *)toState {
[NSException raise:@"IllegalStateTransition" format:@"cannot transition from %@ to %@", fromState, toState];
return nil;
}
- (NSString *)_validateTransitionFromState:(NSString *)fromState toState:(NSString *)toState {
// Transitioning to the same state (fromState == toState) is always allowed. If it's explicitly included in its own validTransitions, the standard method calls below will be invoked. This allows us to avoid creating states that exist only to reexecute transition code for the current state.
// Raise exception if attempting to transition to nil -- you can only transition *from* nil
if (!toState) {
NSLog(@" ••• %@ cannot transition to <nil> state", self);
toState = [self _missingTransitionFromState:fromState toState:toState];
if (!toState) {
return nil;
}
}
// Raise exception if this is an illegal transition (toState must be a validTransition on fromState)
if (fromState) {
id validTransitions = self.validTransitions[fromState];
BOOL transitionSpecified = YES;
// Multiple valid transitions
if ([validTransitions isKindOfClass:[NSArray class]]) {
if (![validTransitions containsObject:toState]) {
transitionSpecified = NO;
}
}
// Otherwise, single valid transition object
else if (![validTransitions isEqual:toState]) {
transitionSpecified = NO;
}
if (!transitionSpecified) {
// Silently fail if implict transition to the same state
if ([fromState isEqualToString:toState]) {
if (self.shouldLogStateTransitions)
NSLog(@" ••• %@ ignoring reentry to %@", self, toState);
return nil;
}
if (self.shouldLogStateTransitions)
NSLog(@" ••• %@ cannot transition to %@ from %@", self, toState, fromState);
toState = [self _missingTransitionFromState:fromState toState:toState];
if (!toState)
return nil;
}
}
// Allow target to opt out of this transition (preconditions)
id target = [self target];
typedef BOOL (*ObjCMsgSendReturnBool)(id, SEL);
ObjCMsgSendReturnBool sendMsgReturnBool = (ObjCMsgSendReturnBool)objc_msgSend;
SEL enterStateAction = NSSelectorFromString([@"shouldEnter" stringByAppendingString:toState]);
if ([target respondsToSelector:enterStateAction] && !sendMsgReturnBool(target, enterStateAction)) {
NSLog(@" ••• %@ transition disallowed to %@ from %@ (via %@)", self, toState, fromState, NSStringFromSelector(enterStateAction));
toState = [self _missingTransitionFromState:fromState toState:toState];
}
return toState;
}
- (void)_performTransitionFromState:(NSString *)fromState toState:(NSString *)toState {
// Subclasses may implement several different selectors to handle state transitions:
//
// did enter state (didEnterPaused)
// did exit state (didExitPaused)
// transition between states (stateDidChangeFromPausedToPlaying)
// generic transition handler (stateDidChange), for common tasks
//
// Any and all of these that are implemented will be invoked.
if (self.shouldLogStateTransitions)
NSLog(@" ••• %@ state change from %@ to %@", self, fromState, toState);
id target = [self target];
typedef void (*ObjCMsgSendReturnVoid)(id, SEL);
ObjCMsgSendReturnVoid sendMsgReturnVoid = (ObjCMsgSendReturnVoid)objc_msgSend;
if (fromState) {
SEL exitStateAction = NSSelectorFromString([@"didExit" stringByAppendingString:fromState]);
if ([target respondsToSelector:exitStateAction]) {
sendMsgReturnVoid(target, exitStateAction);
}
}
SEL enterStateAction = NSSelectorFromString([@"didEnter" stringByAppendingString:toState]);
if ([target respondsToSelector:enterStateAction]) {
sendMsgReturnVoid(target, enterStateAction);
}
NSString *fromStateNotNil = fromState ? fromState : MCC_PREFIXED_CONSTANT(StateNil);
SEL transitionAction = NSSelectorFromString([NSString stringWithFormat:@"stateDidChangeFrom%@To%@", fromStateNotNil, toState]);
if ([target respondsToSelector:transitionAction]) {
sendMsgReturnVoid(target, transitionAction);
}
SEL genericDidChangeAction = @selector(stateDidChange);
if ([target respondsToSelector:genericDidChangeAction]) {
sendMsgReturnVoid(target, genericDidChangeAction);
}
}
@end