@@ -10,17 +10,15 @@ import Foundation
1010public final class MobiusController < Model, Event, Effect> {
1111 typealias Loop = MobiusLoop < Model , Event , Effect >
1212 typealias ViewConnectable = AsyncDispatchQueueConnectable < Model , Event >
13- typealias ViewConnection = Connection < Model >
1413
1514 private struct StoppedState {
1615 var modelToStartFrom : Model
17- var viewConnectable : ViewConnectable ?
18-
16+ var viewConnectables : [ UUID : ViewConnectable ]
1917 }
2018
2119 private struct RunningState {
2220 var loop : Loop
23- var viewConnectable : ViewConnectable ?
21+ var viewConnectables : [ UUID : ViewConnectable ]
2422 var disposables : CompositeDisposable
2523 }
2624
@@ -78,7 +76,7 @@ public final class MobiusController<Model, Event, Effect> {
7876 self . viewQueue = viewQueue
7977
8078 let state = State (
81- state: StoppedState ( modelToStartFrom: initialModel, viewConnectable : nil ) ,
79+ state: StoppedState ( modelToStartFrom: initialModel, viewConnectables : [ : ] ) ,
8280 queue: loopQueue
8381 )
8482 self . state = state
@@ -130,25 +128,28 @@ public final class MobiusController<Model, Event, Effect> {
130128
131129 /// Connect a view to this controller.
132130 ///
133- /// May not be called while the loop is running.
134- ///
135131 /// The `Connectable` will be given an event consumer, which the view should use to send events to the `MobiusLoop`.
136132 /// The view should also return a `Connection` that accepts models and renders them. Disposing the connection should
137133 /// make the view stop emitting events.
138134 ///
139- /// - Attention: fails via `MobiusHooks.errorHandler` if the loop is running or if the controller already is
140- /// connected
135+ /// - Returns: An identifier that can be used to disconnect the view.
136+ /// - Attention: Fails via `MobiusHooks.errorHandler` if the loop is running.
137+ @discardableResult
141138 public func connectView< ViewConnectable: Connectable > (
142139 _ connectable: ViewConnectable
143- ) where ViewConnectable. Input == Model , ViewConnectable. Output == Event {
140+ ) -> UUID where ViewConnectable. Input == Model , ViewConnectable. Output == Event {
144141 do {
142+ var uuid = Self . viewConnectableID
145143 try state. mutate { stoppedState in
146- guard stoppedState. viewConnectable == nil else {
147- throw ErrorMessage ( message: " \( Self . debugTag) : only one view may be connected at a time " )
144+ if stoppedState. viewConnectables [ uuid] != nil {
145+ // For backwards compatibility with id-less `disconnectView()` usage, we default
146+ // to the reserved connection id and generate a new one when needed.
147+ uuid = UUID ( )
148148 }
149-
150- stoppedState. viewConnectable = AsyncDispatchQueueConnectable ( connectable, acceptQueue: viewQueue)
149+ stoppedState. viewConnectables [ uuid] = AsyncDispatchQueueConnectable ( connectable, acceptQueue: viewQueue)
151150 }
151+
152+ return uuid
152153 } catch {
153154 MobiusHooks . errorHandler (
154155 errorMessage ( error, default: " \( Self . debugTag) : cannot connect a view while running " ) ,
@@ -158,20 +159,22 @@ public final class MobiusController<Model, Event, Effect> {
158159 }
159160 }
160161
161- /// Disconnect the connected view from this controller.
162+ /// Disconnect a connected view from this controller.
162163 ///
163164 /// May not be called directly from an effect handler running on the controller’s loop queue.
164165 ///
166+ /// - Parameter id: The identifier received from calling `connectView(_:)`.
165167 /// - Attention: fails via `MobiusHooks.errorHandler` if the loop is running or if there isn't anything to
166168 /// disconnect
167- public func disconnectView( ) {
169+ public func disconnectView( id : UUID ? = nil ) {
168170 do {
171+ let uuid = id ?? Self . viewConnectableID
169172 try state. mutate { stoppedState in
170- guard stoppedState. viewConnectable != nil else {
171- throw ErrorMessage ( message: " \( Self . debugTag) : no view connected , cannot disconnect " )
173+ guard stoppedState. viewConnectables [ uuid ] != nil else {
174+ throw ErrorMessage ( message: " \( Self . debugTag) : invalid view connection id , cannot disconnect " )
172175 }
173176
174- stoppedState. viewConnectable = nil
177+ stoppedState. viewConnectables [ uuid ] = nil
175178 }
176179 } catch {
177180 MobiusHooks . errorHandler (
@@ -191,11 +194,8 @@ public final class MobiusController<Model, Event, Effect> {
191194 do {
192195 try state. transitionToRunning { stoppedState in
193196 let loop = loopFactory ( stoppedState. modelToStartFrom)
194-
195- var disposables : [ Disposable ] = [ loop]
196-
197- if let viewConnectable = stoppedState. viewConnectable {
198- let viewConnection = viewConnectable. connect { [ weak loop] event in
197+ let disposables : [ Disposable ] = [ loop] + stoppedState. viewConnectables. values. map { connectable in
198+ let connection = connectable. connect { [ weak loop] event in
199199 guard let loop = loop else {
200200 // This failure should not be reached under normal circumstances because it is handled by
201201 // AsyncDispatchQueueConnectable. Stopping here means that the viewConnectable called its
@@ -205,13 +205,14 @@ public final class MobiusController<Model, Event, Effect> {
205205
206206 loop. unguardedDispatchEvent ( event)
207207 }
208- loop. addObserver ( viewConnection. accept)
209- disposables. append ( viewConnection)
208+ loop. addObserver ( connection. accept)
209+
210+ return connection
210211 }
211212
212213 return RunningState (
213214 loop: loop,
214- viewConnectable : stoppedState. viewConnectable ,
215+ viewConnectables : stoppedState. viewConnectables ,
215216 disposables: CompositeDisposable ( disposables: disposables)
216217 )
217218 }
@@ -236,11 +237,12 @@ public final class MobiusController<Model, Event, Effect> {
236237 public func stop( ) {
237238 do {
238239 try state. transitionToStopped { runningState in
239- let model = runningState. loop. latestModel
240-
241240 runningState. disposables. dispose ( )
242241
243- return StoppedState ( modelToStartFrom: model, viewConnectable: runningState. viewConnectable)
242+ return StoppedState (
243+ modelToStartFrom: runningState. loop. latestModel,
244+ viewConnectables: runningState. viewConnectables
245+ )
244246 }
245247 } catch {
246248 MobiusHooks . errorHandler (
@@ -287,6 +289,11 @@ public final class MobiusController<Model, Event, Effect> {
287289 }
288290 }
289291
292+ /// Reserves a special identifier for the primary view connectable
293+ private static var viewConnectableID : UUID {
294+ return UUID ( uuid: ( 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ) )
295+ }
296+
290297 /// Simple error that just carries an error message out of a closure for us
291298 private struct ErrorMessage : Error {
292299 let message : String
0 commit comments