MobiusCore/Source/Mobius.swift (122 lines of code) (raw):

// Copyright Spotify AB. // SPDX-License-Identifier: Apache-2.0 import Foundation /// A wrapper around an update function. /// /// The [update function] is the core of a Mobius loop. It takes a model and an event, and produces an updated model and /// a list of effects. /// /// The Update function is declarative, in the sense that it declares what should happen, but doesn’t actually do /// anything itself. It returns a `Next` that describes the desired changes – possibly a new `Model`, and possibly some /// `Effect`s that should be executed – but actually changing the `Model` and executing the `Effect`s happens elsewhere. /// /// The `Update` struct is intended to simplify compositional construction of update functions, but Mobius itself /// doesn’t currently provide any utilities for this kind of workflow. It is possible to pass a plain function (of type /// `(Model, Event) -> Next<Model, Effect>` to the Mobius loop instead of explicitly creating an `Update` struct. /// /// [update function]: https://github.com/spotify/Mobius.swift/wiki/Concepts#update-function public struct Update<Model, Event, Effect> { @usableFromInline let updateClosure: (Model, Event) -> Next<Model, Effect> /// Creates an `Update` struct wrapping the provided function. public init(_ update: @escaping (Model, Event) -> Next<Model, Effect>) { self.updateClosure = update } /// Invokes the update function. @inlinable public func update(model: Model, event: Event) -> Next<Model, Effect> { return updateClosure(model, event) } /// Invokes the update function. @inlinable public func callAsFunction(model: Model, event: Event) -> Next<Model, Effect> { return update(model: model, event: event) } } /// A function used to normalize the initial model of a loop and optionally issue effects when the loop is started. public typealias Initiate<Model, Effect> = (Model) -> First<Model, Effect> // MARK: - Building a Mobius Loop public enum Mobius { /// Create a `Builder` to help you configure a `MobiusLoop` before starting it. /// /// The builder is immutable. When setting various properties, a new instance of a builder will be returned. /// It is therefore recommended to chain the loop configuration functions /// /// Once done configuring the loop you can start the loop using `start(from:)`. /// /// - Parameters: /// - update: the `Update` function of the loop /// - effectHandler: an instance conforming to `Connectable`. Will be used to handle effects by the loop. /// - Returns: a `Builder` instance that you can further configure before starting the loop public static func loop<Model, Event, Effect, EffectHandler: Connectable>( update: Update<Model, Event, Effect>, effectHandler: EffectHandler ) -> Builder<Model, Event, Effect> where EffectHandler.Input == Effect, EffectHandler.Output == Event { return Builder( update: update, effectHandler: effectHandler, eventSource: AnyEventSource({ _ in AnonymousDisposable(disposer: {}) }), eventConsumerTransformer: { $0 }, logger: AnyMobiusLogger(NoopLogger()) ) } /// A convenience version of `loop` that takes an unwrapped update function. /// /// - Parameters: /// - update: the update function of the loop /// - effectHandler: an instance conforming to `Connectable`. Will be used to handle effects by the loop. /// - Returns: a `Builder` instance that you can further configure before starting the loop public static func loop<Model, Event, Effect, EffectHandler: Connectable>( update: @escaping (Model, Event) -> Next<Model, Effect>, effectHandler: EffectHandler ) -> Builder<Model, Event, Effect> where EffectHandler.Input == Effect, EffectHandler.Output == Event { return self.loop( update: Update(update), effectHandler: effectHandler ) } /// A `Builder` represents a set of options for a Mobius loop. /// /// Create a builder using `Mobius.loop`, then optionally configure it with the various `with...` methods. Finally, /// call `start` to create a `MobiusLoop` (single-threaded), or `makeController` to create a `MobiusController` /// (runs on a background queue, can be stopped and resumed). public struct Builder<Model, Event, Effect> { private let update: Update<Model, Event, Effect> private let effectHandler: AnyConnectable<Effect, Event> private let eventSource: AnyEventSource<Event> private let logger: AnyMobiusLogger<Model, Event, Effect> private let eventConsumerTransformer: ConsumerTransformer<Event> fileprivate init<EffectHandler: Connectable>( update: Update<Model, Event, Effect>, effectHandler: EffectHandler, eventSource: AnyEventSource<Event>, eventConsumerTransformer: @escaping ConsumerTransformer<Event>, logger: AnyMobiusLogger<Model, Event, Effect> ) where EffectHandler.Input == Effect, EffectHandler.Output == Event { self.update = update self.effectHandler = AnyConnectable(effectHandler) self.eventSource = eventSource self.logger = logger self.eventConsumerTransformer = eventConsumerTransformer } /// Return a copy of this builder with a new [event source]. /// /// If a `MobiusLoop` is created from the builder by calling `start`, the event source will be subscribed to /// immediately, and the subscription will be disposed when the loop is disposed. /// /// If a `MobiusController` is created by calling `makeController`, the controller will subscribe to the event /// source each time `start` is called on the controller, and dispose the subscription when `stop` is called. /// /// - Note: The event source will replace any existing event source. /// /// - Parameter eventSource: The event source to set on the new builder. /// - Returns: An updated Builder. /// /// [event source]: https://github.com/spotify/Mobius.swift/wiki/Event-Source public func withEventSource<Source: EventSource>(_ eventSource: Source) -> Builder where Source.Event == Event { return Builder( update: update, effectHandler: effectHandler, eventSource: AnyEventSource(eventSource), eventConsumerTransformer: eventConsumerTransformer, logger: logger ) } /// Return a copy of this builder with a new logger. /// /// - Note: The logger will replace any existing logger. /// /// - Parameter logger: The logger to set on the new builder. /// - Returns: An updated Builder. public func withLogger<Logger: MobiusLogger>( _ logger: Logger ) -> Builder where Logger.Model == Model, Logger.Event == Event, Logger.Effect == Effect { return Builder( update: update, effectHandler: effectHandler, eventSource: eventSource, eventConsumerTransformer: eventConsumerTransformer, logger: AnyMobiusLogger(logger) ) } /// Add a function to transform the event consumers, i.e. functions that take an event and pass it to the /// loop’s processing logic. If multiple transformers are supplied, they will be applied in the order they /// were specified. /// /// Note that this is a map over `Consumer<Event>`, not over `Event`. /// /// - Note: The event consumer transformer can be used to implement custom scheduling, such as marshalling /// events to a particular queue or thread. However, correctly managing the logic around this while also /// handling loop teardown is tricky; it is recommended that you use `MobiusController` for this purpose, or /// at least refer to its implementation. /// /// - Note: The transformer will replace any existing event consumer transformer. /// /// - Parameter transformer: The transformation to apply to event consumers. /// - Returns: An updated Builder. public func withEventConsumerTransformer(_ transformer: @escaping ConsumerTransformer<Event>) -> Builder { let oldTransfomer = self.eventConsumerTransformer return Builder( update: update, effectHandler: effectHandler, eventSource: eventSource, eventConsumerTransformer: { consumer in transformer(oldTransfomer(consumer)) }, logger: logger ) } /// Create a `MobiusLoop` from the builder, and optionally dispatch one or more effects. /// /// - Parameters: /// - initialModel: The model the loop should start with. /// - effects: Zero or more effects to execute immediately. public func start(from initialModel: Model, effects: [Effect] = []) -> MobiusLoop<Model, Event, Effect> { return MobiusLoop( model: initialModel, update: update, eventSource: eventSource, eventConsumerTransformer: eventConsumerTransformer, effectHandler: effectHandler, effects: effects, logger: logger ) } /// Create a `MobiusController` from the builder. /// /// - Parameters: /// - initialModel: The initial default model of the `MobiusController` /// - qos: The Quality of Service class for the controller’s work queue. Default: `.userInitiated` public func makeController( from initialModel: Model, initiate: Initiate<Model, Effect>? = nil, qos: DispatchQoS.QoSClass = .userInitiated ) -> MobiusController<Model, Event, Effect> { return makeController(from: initialModel, initiate: initiate, loopQueue: .global(qos: qos)) } /// Create a `MobiusController` from the builder. /// /// - Parameters: /// - initialModel: The initial default model of the `MobiusController` /// - initiate: An optional initiator function to invoke each time the controller’s loop is started. /// - loopQueue: The target queue for the `MobiusController`’s work queue. The controller will dispatch events /// and effects on a serial queue that targets this queue. /// - viewQueue: The queue to use to post to the `MobiusController`’s view connection. /// Default: the main queue. public func makeController( from initialModel: Model, initiate: Initiate<Model, Effect>? = nil, loopQueue: DispatchQueue, viewQueue: DispatchQueue = .main ) -> MobiusController<Model, Event, Effect> { return MobiusController( builder: self, initialModel: initialModel, initiate: initiate, logger: logger, loopQueue: loopQueue, viewQueue: viewQueue ) } } }