Skip to content

Commit 4a8d84d

Browse files
committed
Merge pull request #24 from macoscope/17-fix-reference-cycle-2
fix reference cycle
2 parents 67779c3 + c2fda0b commit 4a8d84d

4 files changed

Lines changed: 70 additions & 27 deletions

File tree

HISTORY.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
## [0.3.0][] • work in progress
22

33
- Updated to Swift 2.1 (Xcode 7.1).
4+
5+
- Made references to class-based subjects weak. This helps to remove
6+
subject-machine reference cycles, but it also means you have to keep a
7+
strong reference to a subject somewhere else (which you usually already do).
8+
When subject references become `nil`, transitions are no longer performed.
9+
Thanks to @bartekchlebek for work on this issue! :clap:
10+
411
- Added default `DOTLabel` implementation (`return "\(self)"`). You don't
512
have to change anything. If you have custom `DOTLabel` implementations,
613
you can keep using them, but if the only thing you do is `case Foo: return

README.md

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,14 @@ Enjoy improving your code by explicitly defining distinct states and
277277
transitions between them! While you do so, please keep two things in
278278
mind:
279279

280-
1) Remember that [Swift `enum`s][enums] can have associated values — you
280+
1) Your subjects are probably reference types (classes). Storing a
281+
state machine as a property of a subject normally would create a reference
282+
cycle, so SwiftyStateMachine uses weak references for class-based subjects.
283+
This means you have to keep a strong reference to a subject somewhere else,
284+
but you usually already do this. When subject references become `nil`,
285+
transitions are no longer performed.
286+
287+
2) Remember that [Swift `enum`s][enums] can have associated values — you
281288
can pass additional information with events or store data in states.
282289
For example, if you had a game with a [heads-up display][HUD], you could
283290
do something like this:
@@ -297,22 +304,6 @@ machine.handleEvent(.TakeDamage(13.37))
297304
[HUD]: http://en.wikipedia.org/wiki/HUD_%28video_gaming%29
298305

299306

300-
2) If your subjects are reference types (classes), remember to use `unowned`
301-
to break reference cycles:
302-
303-
```swift
304-
let schema = // ...
305-
306-
class MyViewController: UIViewController {
307-
lazy var stateMachine = { [unowned self] in
308-
StateMachine(schema: schema, subject: self)
309-
}()
310-
311-
// ...
312-
}
313-
```
314-
315-
316307
Development
317308
-----------
318309

StateMachine/StateMachine.swift

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ public struct StateMachineSchema<A, B, C>: StateMachineSchemaType {
5151
/// `StateMachineSchemaType` documentation for more information about schemas
5252
/// and subjects.
5353
///
54+
/// References to class-based subjects are weak. This helps to remove
55+
/// subject-machine reference cycles, but it also means you have to keep a
56+
/// strong reference to a subject somewhere else. When subject references
57+
/// become `nil`, transitions are no longer performed.
58+
///
5459
/// The state machine provides the `state` property for inspecting the current
5560
/// state and the `handleEvent` method for triggering state transitions
5661
/// defined in the schema.
@@ -73,25 +78,49 @@ public final class StateMachine<T: StateMachineSchemaType> {
7378
private let schema: T
7479

7580
/// Object associated with the state machine. Can be accessed in
76-
/// transition blocks.
77-
private let subject: T.Subject
81+
/// transition blocks. Closure used to allow for weak references.
82+
private let subject: () -> T.Subject?
7883

79-
public init(schema: T, subject: T.Subject) {
84+
private init(schema: T, subject: () -> T.Subject?) {
8085
self.state = schema.initialState
8186
self.schema = schema
8287
self.subject = subject
8388
}
8489

8590
/// A method for triggering transitions and changing the state of the
86-
/// machine. If the transition logic of the schema defines a transition
91+
/// machine. Transitions are not performed when a weak reference to the subject
92+
/// becomes `nil`. If the transition logic of the schema defines a transition
8793
/// for current state and given event, the state is changed, the optional
8894
/// transition block is executed, and `didTransitionCallback` is called.
8995
public func handleEvent(event: T.Event) {
90-
if let (newState, transition) = schema.transitionLogic(state, event) {
91-
let oldState = state
92-
state = newState
93-
transition?(subject)
94-
didTransitionCallback?(oldState, event, newState)
96+
guard let
97+
subject = subject(),
98+
(newState, transition) = schema.transitionLogic(state, event)
99+
else {
100+
return
95101
}
102+
103+
let oldState = state
104+
state = newState
105+
106+
transition?(subject)
107+
didTransitionCallback?(oldState, event, newState)
108+
}
109+
}
110+
111+
112+
public extension StateMachine where T.Subject: AnyObject {
113+
/// Creates a state machine with a weak reference to a subject. This helps
114+
/// to remove subject-machine reference cycles, but it also means you have
115+
/// to keep a strong reference to a subject somewhere else. When subject
116+
/// reference becomes `nil`, transitions are no longer performed.
117+
public convenience init(schema: T, subject: T.Subject) {
118+
self.init(schema: schema, subject: { [weak subject] in subject })
119+
}
120+
}
121+
122+
public extension StateMachine {
123+
public convenience init(schema: T, subject: T.Subject) {
124+
self.init(schema: schema, subject: { subject })
96125
}
97126
}

StateMachineTests/StateMachineSpec.swift

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ private class Subject {
5757
typealias SchemaType = StateMachineSchema<SimpleState, SimpleEvent, Subject>
5858

5959
let schema: SchemaType
60-
lazy var machine: StateMachine<SchemaType> = { [unowned self] in
60+
lazy var machine: StateMachine<SchemaType> = {
6161
StateMachine(schema: self.schema, subject: self)
6262
}()
6363

@@ -158,6 +158,22 @@ class StateMachineSpec: QuickSpec {
158158
expect(subject.machine.state) == SimpleState.S1
159159
}
160160

161+
it("doesn't cause machine-subject reference cycles") {
162+
final class MachineOwner {
163+
var machine: StateMachine<StateMachineSchema<SimpleState, SimpleEvent, MachineOwner>>!
164+
165+
init() {
166+
machine = StateMachine(
167+
schema: StateMachineSchema(initialState: .S1) { _ in nil },
168+
subject: self)
169+
}
170+
}
171+
172+
weak var reference: MachineOwner?
173+
do { reference = MachineOwner() }
174+
expect(reference).to(beNil())
175+
}
176+
161177
}
162178

163179
describe("Graphable State Machine") {

0 commit comments

Comments
 (0)