@@ -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,24 @@ 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+ /// - Parameter connectable: the view to connect
136+ /// - Returns: an identifier that can be used to disconnect the view
137+ /// - Attention: fails via `MobiusHooks.errorHandler` if the loop is running
138+ @discardableResult
141139 public func connectView< ViewConnectable: Connectable > (
142140 _ connectable: ViewConnectable
143- ) where ViewConnectable. Input == Model , ViewConnectable. Output == Event {
141+ ) -> UUID where ViewConnectable. Input == Model , ViewConnectable. Output == Event {
144142 do {
143+ let id = UUID ( )
145144 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 " )
148- }
149-
150- stoppedState. viewConnectable = AsyncDispatchQueueConnectable ( connectable, acceptQueue: viewQueue)
145+ stoppedState. viewConnectables [ id] = AsyncDispatchQueueConnectable ( connectable, acceptQueue: viewQueue)
151146 }
147+
148+ return id
152149 } catch {
153150 MobiusHooks . errorHandler (
154151 errorMessage ( error, default: " \( Self . debugTag) : cannot connect a view while running " ) ,
@@ -162,16 +159,44 @@ public final class MobiusController<Model, Event, Effect> {
162159 ///
163160 /// May not be called directly from an effect handler running on the controller’s loop queue.
164161 ///
165- /// - Attention: fails via `MobiusHooks.errorHandler` if the loop is running or if there isn't anything to
166- /// disconnect
162+ /// - Attention: fails via `MobiusHooks.errorHandler` if the loop is running, if there is more than 1 connection,
163+ /// or if there isn't anything to disconnect
167164 public func disconnectView( ) {
168165 do {
169166 try state. mutate { stoppedState in
170- guard stoppedState. viewConnectable != nil else {
167+ guard stoppedState. viewConnectables. count <= 1 else {
168+ throw ErrorMessage ( message: " \( Self . debugTag) : missing view connection id, cannot disconnect " )
169+ }
170+
171+ guard let id = stoppedState. viewConnectables. keys. first else {
171172 throw ErrorMessage ( message: " \( Self . debugTag) : no view connected, cannot disconnect " )
172173 }
173174
174- stoppedState. viewConnectable = nil
175+ stoppedState. viewConnectables [ id] = nil
176+ }
177+ } catch {
178+ MobiusHooks . errorHandler (
179+ errorMessage ( error, default: " \( Self . debugTag) : cannot disconnect view while running; call stop first " ) ,
180+ #file,
181+ #line
182+ )
183+ }
184+ }
185+
186+ /// Disconnect a connected view from this controller.
187+ ///
188+ /// May not be called directly from an effect handler running on the controller’s loop queue.
189+ ///
190+ /// - Parameter id: the identifier received from calling `connectView(_:)`
191+ /// - Attention: fails via `MobiusHooks.errorHandler` if the loop is running or if the id is not connected
192+ public func disconnectView( id: UUID ) {
193+ do {
194+ try state. mutate { stoppedState in
195+ guard stoppedState. viewConnectables [ id] != nil else {
196+ throw ErrorMessage ( message: " \( Self . debugTag) : invalid view connection, cannot disconnect " )
197+ }
198+
199+ stoppedState. viewConnectables [ id] = nil
175200 }
176201 } catch {
177202 MobiusHooks . errorHandler (
@@ -191,11 +216,8 @@ public final class MobiusController<Model, Event, Effect> {
191216 do {
192217 try state. transitionToRunning { stoppedState in
193218 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
219+ let disposables : [ Disposable ] = [ loop] + stoppedState. viewConnectables. values. map { connectable in
220+ let connection = connectable. connect { [ weak loop] event in
199221 guard let loop = loop else {
200222 // This failure should not be reached under normal circumstances because it is handled by
201223 // AsyncDispatchQueueConnectable. Stopping here means that the viewConnectable called its
@@ -205,13 +227,14 @@ public final class MobiusController<Model, Event, Effect> {
205227
206228 loop. unguardedDispatchEvent ( event)
207229 }
208- loop. addObserver ( viewConnection. accept)
209- disposables. append ( viewConnection)
230+ loop. addObserver ( connection. accept)
231+
232+ return connection
210233 }
211234
212235 return RunningState (
213236 loop: loop,
214- viewConnectable : stoppedState. viewConnectable ,
237+ viewConnectables : stoppedState. viewConnectables ,
215238 disposables: CompositeDisposable ( disposables: disposables)
216239 )
217240 }
@@ -236,11 +259,12 @@ public final class MobiusController<Model, Event, Effect> {
236259 public func stop( ) {
237260 do {
238261 try state. transitionToStopped { runningState in
239- let model = runningState. loop. latestModel
240-
241262 runningState. disposables. dispose ( )
242263
243- return StoppedState ( modelToStartFrom: model, viewConnectable: runningState. viewConnectable)
264+ return StoppedState (
265+ modelToStartFrom: runningState. loop. latestModel,
266+ viewConnectables: runningState. viewConnectables
267+ )
244268 }
245269 } catch {
246270 MobiusHooks . errorHandler (
0 commit comments