Skip to content

Instantly share code, notes, and snippets.

@mayoff
Last active December 7, 2021 14:25
Show Gist options
  • Save mayoff/4659e5f950a9158e80cadd63e7fec218 to your computer and use it in GitHub Desktop.
Save mayoff/4659e5f950a9158e80cadd63e7fec218 to your computer and use it in GitHub Desktop.
An implementation of Point-Free's Composable Architecture, with the addition of subscriptions from The Elm Architecture (TEA). See https://github.com/pointfreeco/episode-code-samples/issues/51
#if canImport(Combine)
import Combine
import CasePaths
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public typealias Effect<Output> = AnyPublisher<Output, Never>
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension Effect {
public static var noEffects: Effect<Output> {
return Empty(completeImmediately: true)
.eraseToAnyPublisher()
}
public static func fireAndForget(_ body: @escaping () -> Void) -> Effect<Output> {
let empty = Empty<Output, Never>(completeImmediately: true)
return Deferred { (body(), empty).1 }
.eraseToAnyPublisher()
}
public static func sync(_ body: @escaping () -> Output) -> Effect<Output> {
return Deferred { Just(body()) }
.eraseToAnyPublisher()
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public struct Reducer<Model, Action> {
public init(_ apply: @escaping (inout Model, Action) -> [Effect<Action>]) {
_apply = apply
}
public func apply(_ action: Action, to model: inout Model) -> [Effect<Action>] {
return _apply(&model, action)
}
public func pullback<OuterModel, OuterAction>(
model: WritableKeyPath<OuterModel, Model>,
action: CasePath<OuterAction, Action>
) -> Reducer<OuterModel, OuterAction> {
return .init { (outerModel, outerAction) -> [AnyPublisher<OuterAction, Never>] in
guard let innerAction = action.extract(from: outerAction) else { return [] }
return self
.apply(innerAction, to: &outerModel[keyPath: model])
.map { $0.map(action.embed).eraseToAnyPublisher() }
}
}
private let _apply: (inout Model, Action) -> [Effect<Action>]
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol Extrinsic: Hashable {
associatedtype Action
func publisher() -> AnyPublisher<Action, Never>
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
fileprivate class ExtrinsicCollectionBase<Model, Action> {
func update(with store: RootStore<Model, Action>) {
fatalError("subclass responsibility")
}
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
fileprivate class ExtrinsicCollection<Model, Ex: Extrinsic>: ExtrinsicCollectionBase<Model, Ex.Action> {
init(_ make: @escaping (Model) -> Set<Ex>) {
self.make = make
}
override func update(with store: RootStore<Model, Ex.Action>) {
let exs = make(store.model)
for ex in ticketForEx.keys.filter({ !exs.contains($0) }) {
ticketForEx.removeValue(forKey: ex)?.cancel()
}
for ex in exs.subtracting(ticketForEx.keys) {
ticketForEx[ex] = ex.publisher()
.sink(receiveValue: { [weak store] in store?.send($0) })
}
}
private let make: (Model) -> Set<Ex>
private var ticketForEx: [Ex: AnyCancellable] = [:]
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@dynamicMemberLookup
public class Store<Model, Action>: ObservableObject {
public static func root(
withModel model: Model,
reducer: Reducer<Model, Action>
) -> Store<Model, Action> {
return RootStore(model: model, reducer: reducer, extrinsicCollection: nil)
}
public static func root<Ex: Extrinsic>(
withModel model: Model,
reducer: Reducer<Model, Action>,
extrinsics: @escaping (Model) -> Set<Ex>
) -> Store<Model, Action> where Ex.Action == Action {
return RootStore(model: model, reducer: reducer, extrinsicCollection: ExtrinsicCollection(extrinsics))
}
public var model: Model { fatalError("subclass responsibility") }
public func send(_ action: Action) { fatalError("subclass responsibility") }
public var objectWillChange: AnyPublisher<Void, Never> { fatalError("subclass responsibility") }
public final func view<InnerModel, InnerAction>(
toInner: @escaping (Model) -> InnerModel,
toOuter: @escaping (InnerAction) -> Action
) -> Store<InnerModel, InnerAction> {
return InnerStore(outer: self, toInner: toInner, toOuter: toOuter)
}
public subscript<Value>(dynamicMember keyPath: KeyPath<Model, Value>) -> Value { model[keyPath: keyPath] }
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
fileprivate final class RootStore<Model, Action>: Store<Model, Action> {
override var model: Model { _model }
override func send(_ action: Action) {
let effects = reducer.apply(action, to: &_model)
for effect in effects {
var ticket: AnyCancellable? = nil
ticket = effect.sink(
receiveCompletion: { _ in ticket = nil },
receiveValue: { self.send($0) })
withExtendedLifetime(ticket) { }
}
extrinsicCollection?.update(with: self)
subject.send()
}
override var objectWillChange: AnyPublisher<Void, Never> { _objectWillChange }
init(model: Model, reducer: Reducer<Model, Action>, extrinsicCollection: ExtrinsicCollectionBase<Model, Action>?) {
_model = model
self.reducer = reducer
self.extrinsicCollection = extrinsicCollection
_objectWillChange = subject.eraseToAnyPublisher()
super.init()
extrinsicCollection?.update(with: self)
}
private var _model: Model
private let reducer: Reducer<Model, Action>
private let subject = PassthroughSubject<Void, Never>()
private let _objectWillChange: AnyPublisher<Void, Never>
private let extrinsicCollection: ExtrinsicCollectionBase<Model, Action>?
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
fileprivate final class InnerStore<OuterModel, OuterAction, InnerModel, InnerAction>: Store<InnerModel, InnerAction> {
override var model: InnerModel { toInner(outer.model) }
override func send(_ action: InnerAction) { outer.send(toOuter(action)) }
override var objectWillChange: AnyPublisher<Void, Never> { outer.objectWillChange }
init(
outer: Store<OuterModel, OuterAction>,
toInner: @escaping (OuterModel) -> InnerModel,
toOuter: @escaping (InnerAction) -> OuterAction
) {
self.outer = outer
self.toInner = toInner
self.toOuter = toOuter
}
private let outer: Store<OuterModel, OuterAction>
private let toInner: (OuterModel) -> InnerModel
private let toOuter: (InnerAction) -> OuterAction
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment