-
Notifications
You must be signed in to change notification settings - Fork 901
/
Router.swift
227 lines (181 loc) · 7.83 KB
/
Router.swift
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
//
// Copyright (c) 2017. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import RxSwift
/// The lifecycle stages of a router scope.
public enum RouterLifecycle {
/// Router did load.
case didLoad
}
/// The scope of a `Router`, defining various lifecycles of a `Router`.
public protocol RouterScope: AnyObject {
/// An observable that emits values when the router scope reaches its corresponding life-cycle stages. This
/// observable completes when the router scope is deallocated.
var lifecycle: Observable<RouterLifecycle> { get }
}
/// The base protocol for all routers.
public protocol Routing: RouterScope {
// The following methods must be declared in the base protocol, since `Router` internally invokes these methods.
// In order to unit test router with a mock child router, the mocked child router first needs to conform to the
// custom subclass routing protocol, and also this base protocol to allow the `Router` implementation to execute
// base class logic without error.
/// The base interactable associated with this `Router`.
var interactable: Interactable { get }
/// The list of children routers of this `Router`.
var children: [Routing] { get }
/// Loads the `Router`.
///
/// - note: This method is internally used by the framework. Application code should never
/// invoke this method explicitly.
func load()
// We cannot declare the attach/detach child methods to take in concrete `Router` instances,
// since during unit testing, we need to use mocked child routers.
/// Attaches the given router as a child.
///
/// - parameter child: The child router to attach.
func attachChild(_ child: Routing)
/// Detaches the given router from the tree.
///
/// - parameter child: The child router to detach.
func detachChild(_ child: Routing)
}
/// The base class of all routers that does not own view controllers, representing application states.
///
/// A router acts on inputs from its corresponding interactor, to manipulate application state, forming a tree of
/// routers. A router may obtain a view controller through constructor injection to manipulate view controller tree.
/// The DI structure guarantees that the injected view controller must be from one of this router's ancestors.
/// Router drives the lifecycle of its owned `Interactor`.
///
/// Routers should always use helper builders to instantiate children routers.
open class Router<InteractorType>: Routing {
/// The corresponding `Interactor` owned by this `Router`.
public let interactor: InteractorType
/// The base `Interactable` associated with this `Router`.
public let interactable: Interactable
/// The list of children `Router`s of this `Router`.
public final var children: [Routing] = []
/// The observable that emits values when the router scope reaches its corresponding life-cycle stages.
///
/// This observable completes when the router scope is deallocated.
public final var lifecycle: Observable<RouterLifecycle> {
return lifecycleSubject.asObservable()
}
/// Initializer.
///
/// - parameter interactor: The corresponding `Interactor` of this `Router`.
public init(interactor: InteractorType) {
self.interactor = interactor
guard let interactable = interactor as? Interactable else {
fatalError("\(interactor) should conform to \(Interactable.self)")
}
self.interactable = interactable
}
/// Loads the `Router`.
///
/// - note: This method is internally used by the framework. Application code should never invoke this method
/// explicitly.
public final func load() {
guard !didLoadFlag else {
return
}
didLoadFlag = true
internalDidLoad()
didLoad()
}
/// Called when the router has finished loading.
///
/// This method is invoked only once. Subclasses should override this method to perform one time setup logic,
/// such as attaching immutable children. The default implementation does nothing.
open func didLoad() {
// No-op
}
// We cannot declare the attach/detach child methods to take in concrete `Router` instances,
// since during unit testing, we need to use mocked child routers.
/// Attaches the given router as a child.
///
/// - parameter child: The child `Router` to attach.
public final func attachChild(_ child: Routing) {
assert(!(children.contains { $0 === child }), "Attempt to attach child: \(child), which is already attached to \(self).")
children.append(child)
// Activate child first before loading. Router usually attaches immutable children in didLoad.
// We need to make sure the RIB is activated before letting it attach immutable children.
child.interactable.activate()
child.load()
}
/// Detaches the given `Router` from the tree.
///
/// - parameter child: The child `Router` to detach.
public final func detachChild(_ child: Routing) {
child.interactable.deactivate()
children.removeElementByReference(child)
}
// MARK: - Internal
let deinitDisposable = CompositeDisposable()
func internalDidLoad() {
bindSubtreeActiveState()
lifecycleSubject.onNext(.didLoad)
}
// MARK: - Private
private let lifecycleSubject = PublishSubject<RouterLifecycle>()
private var didLoadFlag: Bool = false
private func bindSubtreeActiveState() {
let disposable = interactable.isActiveStream
// Do not retain self here to guarantee execution. Retaining self will cause the dispose bag
// to never be disposed, thus self is never deallocated. Also cannot just store the disposable
// and call dispose(), since we want to keep the subscription alive until deallocation, in
// case the router is re-attached. Using weak does require the router to be retained until its
// interactor is deactivated.
.subscribe(onNext: { [weak self] (isActive: Bool) in
// When interactor becomes active, we are attached to parent, otherwise we are detached.
self?.setSubtreeActive(isActive)
})
_ = deinitDisposable.insert(disposable)
}
private func setSubtreeActive(_ active: Bool) {
if active {
iterateSubtree(self) { router in
if !router.interactable.isActive {
router.interactable.activate()
}
}
} else {
iterateSubtree(self) { router in
if router.interactable.isActive {
router.interactable.deactivate()
}
}
}
}
private func iterateSubtree(_ root: Routing, closure: (_ node: Routing) -> ()) {
closure(root)
for child in root.children {
iterateSubtree(child, closure: closure)
}
}
private func detachAllChildren() {
for child in children {
detachChild(child)
}
}
deinit {
interactable.deactivate()
if !children.isEmpty {
detachAllChildren()
}
lifecycleSubject.onCompleted()
deinitDisposable.dispose()
LeakDetector.instance.expectDeallocate(object: interactable)
}
}