summaryrefslogtreecommitdiff
path: root/SmartDeviceLink/SDLStateMachine.m
blob: ab5c546898796473749cdfcf31864386e841fa4d (plain)
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
//
//  SDLStateMachine.m
//  SmartDeviceLink-iOS
//
//  Created by Joel Fischer on 10/30/15.
//  Copyright © 2015 smartdevicelink. All rights reserved.
//

#import "SDLStateMachine.h"

#import "SDLError.h"


NS_ASSUME_NONNULL_BEGIN

NSString *const SDLStateMachineNotificationFormat = @"com.sdl.statemachine.%@";

SDLStateMachineNotificationInfoKey const SDLStateMachineNotificationInfoKeyOldState = @"oldState";
SDLStateMachineNotificationInfoKey const SDLStateMachineNotificationInfoKeyNewState = @"newState";

SDLStateMachineExceptionInfoKey const SDLStateMachineExceptionInfoKeyTargetClass = @"targetClass";
SDLStateMachineExceptionInfoKey const SDLStateMachineExceptionInfoKeyFromState = @"fromState";
SDLStateMachineExceptionInfoKey const SDLStateMachineExceptionInfoKeyToClass = @"toState";

SDLStateMachineTransitionFormat const SDLStateMachineTransitionFormatWillLeave = @"willLeaveState%@";
SDLStateMachineTransitionFormat const SDLStateMachineTransitionFormatWillTransition = @"willTransitionFromState%@ToState%@";
SDLStateMachineTransitionFormat const SDLStateMachineTransitionFormatDidTransition = @"didTransitionFromState%@ToState%@";
SDLStateMachineTransitionFormat const SDLStateMachineTransitionFormatDidEnter = @"didEnterState%@";


@interface SDLStateMachine ()

@property (copy, nonatomic, readwrite) SDLState *currentState;

@end


@implementation SDLStateMachine

- (instancetype)initWithTarget:(id)target initialState:(SDLState *)initialState states:(NSDictionary<SDLState *, SDLAllowableStateTransitions *> *)states {
    self = [super init];

    if (!self) {
        return nil;
    }

    if (states[initialState] == nil) {
        @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Attempted to start with an SDLState that is not in the states dictionary" userInfo:nil];
    }

    _target = target;
    _states = states;
    _currentState = initialState;

    return self;
}

- (void)transitionToState:(SDLState *)state {
    NSString *oldState = [self.currentState copy];
    if ([self isCurrentState:state]) {
        return;
    }

    if (![self sdl_canState:self.currentState transitionToState:state]) {
        @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                       reason:@"Invalid state machine transition occurred"
                                     userInfo:@{SDLStateMachineExceptionInfoKeyTargetClass: NSStringFromClass([self.target class]),
                                                SDLStateMachineExceptionInfoKeyFromState: self.currentState,
                                                SDLStateMachineExceptionInfoKeyToClass: state}];
    }

    SEL willLeave = NSSelectorFromString([NSString stringWithFormat:SDLStateMachineTransitionFormatWillLeave, oldState]);
    SEL willTransition = NSSelectorFromString([NSString stringWithFormat:SDLStateMachineTransitionFormatWillTransition, oldState, state]);
    SEL didTransition = NSSelectorFromString([NSString stringWithFormat:SDLStateMachineTransitionFormatDidTransition, oldState, state]);
    SEL didEnter = NSSelectorFromString([NSString stringWithFormat:SDLStateMachineTransitionFormatDidEnter, state]);

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

    // Pre state transition calls
    if ([self.target respondsToSelector:willLeave]) {
        [self.target performSelector:willLeave];
    }

    if ([self.target respondsToSelector:willTransition]) {
        [self.target performSelector:willTransition];
    }

    // Transition the state
    self.currentState = state;

    // Post state transition calls
    [[NSNotificationCenter defaultCenter] postNotificationName:self.transitionNotificationName object:self userInfo:@{SDLStateMachineNotificationInfoKeyOldState: oldState, SDLStateMachineNotificationInfoKeyNewState: state}];
    if ([self.target respondsToSelector:didTransition]) {
        [self.target performSelector:didTransition];
    }

    if ([self.target respondsToSelector:didEnter]) {
        [self.target performSelector:didEnter];
    }

#pragma clang diagnostic pop
}

- (BOOL)isCurrentState:(SDLState *)state {
    return [self.currentState isEqualToString:state];
}


#pragma mark - Helpers

- (void)setToState:(SDLState *)state fromOldState:(nullable SDLState *)oldState callEnterTransition:(BOOL)shouldCall {
    if (![self.states.allKeys containsObject:state]) {
        return;
    }

    if (oldState != nil && shouldCall) {
        self.currentState = oldState;
        [self transitionToState:state];
    } else if (shouldCall) {
        SEL didEnter = NSSelectorFromString([NSString stringWithFormat:SDLStateMachineTransitionFormatDidEnter, state]);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        if ([self.target respondsToSelector:didEnter]) {
            [self.target performSelector:didEnter];
#pragma clang diagnostic pop
        }
    } else {
        self.currentState = state;
    }
}

/**
 *  Determine if a state transition is valid. Returns YES if the state transition dictionary's fromState key contains toState in its value array, or if fromState and toState are the same state.
 *
 *  @param fromState The former state
 *  @param toState   The new state
 *
 *  @return Whether or not the state transition is valid
 */
- (BOOL)sdl_canState:(SDLState *)fromState transitionToState:(SDLState *)toState {
    if ([self.states[fromState] containsObject:toState] || [fromState isEqualToString:toState]) {
        return YES;
    }

    return NO;
}

- (NSString *)transitionNotificationName {
    return [NSString stringWithFormat:SDLStateMachineNotificationFormat, [self.target class]];
}

@end

NS_ASSUME_NONNULL_END