Last active
March 27, 2024 02:26
-
-
Save Verdier/746cb771f8ce4146d14c519934a51a2c to your computer and use it in GitHub Desktop.
Redux SwiftUI
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct MyView: View { | |
@StateObject var myValue = store.subscribe() | |
.map { state in state.myValue } | |
.toObservableObject(animation: .interactiveSpring()) // optional animation | |
@StateObject var myDerivedValue = store.subscribe() | |
.map { state in state.myValue } | |
.removeDuplicates() | |
.map { value in complexTransformation(value) } // triggered only on new value | |
.toObservableObject() | |
@StateObject var myValue = store.subscribe() | |
.map { state in state.myValue } | |
.throttle(millis: 500) | |
.toObservableObject(animation: .interactiveSpring()) // optional animation | |
// MARK: Helpers | |
/* Shorcut for map with removeDuplicates if myValue is Equatable */ | |
@StateObject var myValue = store.select { state in state.myValue } /* , animation: */ | |
/* Shorcut for map -> removeDuplicates if myValue is Equatable -> map */ | |
@StateObject var myDerivedValue = store.select( | |
{ state in state.myValue }, | |
transform: { value in complexTransformation(value) } | |
/* , animation: ... */ | |
) | |
/* ... */ | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
public protocol Action {} | |
public struct StoreInit: Action {} | |
public typealias Reducer<State> = (_ action: Action, _ state: State) -> State | |
public typealias DispatchFunction = (Action) -> Void | |
public typealias Middleware<State> = (@escaping DispatchFunction, @escaping () -> State?) | |
-> (@escaping DispatchFunction) -> DispatchFunction | |
class Store<State> { | |
public let stateChanged = PassthroughSubject<State, Never>() | |
public private(set) var state: State! { | |
didSet { stateChanged.send(state) } | |
} | |
public lazy var dispatchFunction: DispatchFunction! = createDispatchFunction() | |
private var reducer: Reducer<State> | |
private var isDispatching = Synchronized<Bool>(false) | |
public var middleware: [Middleware<State>] { | |
didSet { | |
dispatchFunction = createDispatchFunction() | |
} | |
} | |
public required init( | |
reducer: @escaping Reducer<State>, | |
state: State?, | |
middleware: [Middleware<State>] = [] | |
) { | |
self.reducer = reducer | |
self.middleware = middleware | |
if let state = state { | |
self.state = state | |
} else { | |
dispatch(StoreInit()) | |
} | |
} | |
private func createDispatchFunction() -> DispatchFunction! { | |
return middleware | |
.reversed() | |
.reduce( | |
{ [unowned self] action in | |
self._defaultDispatch(action: action) | |
}, | |
{ dispatchFunction, middleware in | |
// If the store get's deinitialized before the middleware is complete; drop | |
// the action without dispatching. | |
let dispatch: (Action) -> Void = { [weak self] in self?.dispatch($0) } | |
let getState: () -> State? = { [weak self] in self?.state } | |
return middleware(dispatch, getState)(dispatchFunction) | |
} | |
) | |
} | |
func _defaultDispatch(action: Action) { | |
guard !isDispatching.value else { | |
fatalError( | |
"Redux:ConcurrentMutationError- Action has been dispatched while" + | |
" a previous action is being processed. A reducer" + | |
" is dispatching an action, or Redux is used in a concurrent context" + | |
" (e.g. from multiple threads). Action: \(action)" | |
) | |
} | |
isDispatching.value { $0 = true } | |
let newState = reducer(action, state) | |
isDispatching.value { $0 = false } | |
state = newState | |
} | |
func dispatch(_ action: Action) { | |
dispatchFunction(action) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
import SwiftUI | |
class ObservableValue<T>: ObservableObject { | |
@Published fileprivate(set) var current: T | |
public let objectDidChange = PassthroughSubject<ChangeSubject<T>, Never>() | |
fileprivate init(current: T) { | |
self.current = current | |
} | |
static func constant<T>(_ value: T) -> ObservableValue<T> { | |
return ObservableValue<T>(current: value) | |
} | |
struct ChangeSubject<T> { | |
let old: T | |
let new: T | |
} | |
} | |
private class ObservablePublisher<T>: ObservableValue<T> { | |
private var animation: SwiftUI.Animation? | |
private var cancellable: AnyCancellable? | |
fileprivate init(publisher: AnyPublisher<T, Never>, current: T, animation: SwiftUI.Animation? = nil) { | |
super.init(current: current) | |
self.animation = animation | |
self.cancellable = publisher | |
.sink { [weak self] newValue in | |
DispatchQueue.main.async { | |
self?.setValue(newValue) | |
} | |
} | |
} | |
private func setValue(_ newValue: T) { | |
let oldValue = current | |
if let animation = animation { | |
withAnimation(animation) { | |
current = newValue | |
} | |
} else { | |
current = newValue | |
} | |
objectDidChange.send(ChangeSubject(old: oldValue, new: newValue)) | |
} | |
} | |
class StoreSubscriptionBuilder<State, T> { | |
private var store: Store<State> | |
private var mapper: (State) -> T | |
private var publisher: AnyPublisher<T, Never> | |
init( | |
store: Store<State>, | |
mapper: @escaping (State) -> T, | |
publisher: AnyPublisher<T, Never> | |
) { | |
self.store = store | |
self.mapper = mapper | |
self.publisher = publisher | |
} | |
func toObservableObject(animation: SwiftUI.Animation? = nil) -> ObservableValue<T> { | |
return ObservablePublisher( | |
publisher: publisher, | |
current: mapper(store.state), | |
animation: animation | |
) | |
} | |
} | |
extension StoreSubscriptionBuilder { | |
func map<U>(_ transform: @escaping (T) -> U) -> StoreSubscriptionBuilder<State, U> { | |
return .init( | |
store: store, | |
mapper: { transform(self.mapper($0)) }, | |
publisher: publisher.map(transform).eraseToAnyPublisher() | |
) | |
} | |
func removeDuplicates() -> StoreSubscriptionBuilder<State, T> where T: Equatable { | |
return .init( | |
store: store, | |
mapper: mapper, | |
publisher: publisher.removeDuplicates().eraseToAnyPublisher() | |
) | |
} | |
func throttle(millis: Int) -> StoreSubscriptionBuilder<State, T> where T: Equatable { | |
return .init( | |
store: store, | |
mapper: mapper, | |
publisher: publisher | |
.throttle(for: .milliseconds(millis), scheduler: DispatchQueue.main, latest: true) | |
.eraseToAnyPublisher() | |
) | |
} | |
} | |
extension Store { | |
func subscribe() -> StoreSubscriptionBuilder<State, State> { | |
return .init( | |
store: self, | |
mapper: { $0 }, | |
publisher: stateChanged.eraseToAnyPublisher() | |
) | |
} | |
//MARK: Helpers | |
func select<T>( | |
_ select: @escaping (_ state: State) -> T, | |
animation: SwiftUI.Animation? = nil | |
) -> ObservableValue<T> { | |
return subscribe().map(select).toObservableObject(animation: animation) | |
} | |
func select<T: Equatable>( | |
_ select: @escaping (_ state: State) -> T, | |
animation: SwiftUI.Animation? = nil | |
) -> ObservableValue<T> { | |
return subscribe() | |
.map(select) | |
.removeDuplicates() | |
.toObservableObject(animation: animation) | |
} | |
func select<T: Equatable, D>( | |
_ select: @escaping (_ state: State) -> T, | |
transform: @escaping (_ value: T) -> D, | |
animation: SwiftUI.Animation? = nil | |
) -> ObservableValue<D> { | |
return subscribe() | |
.map(select) | |
.removeDuplicates() | |
.map(transform) | |
.toObservableObject(animation: animation) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Synchronized.swift | |
// ReSwift | |
// | |
// Created by Basem Emara on 2020-08-18. | |
// https://basememara.com/creating-thread-safe-generic-values-in-swift/ | |
// | |
// Copyright © 2020 ReSwift Community. All rights reserved. | |
// | |
import Foundation | |
/// An object that manages the execution of tasks atomically. | |
struct Synchronized<Value> { | |
private let mutex = DispatchQueue(label: "studio.remarkable.Synchronized", attributes: .concurrent) | |
private var _value: Value | |
init(_ value: Value) { | |
self._value = value | |
} | |
/// Returns or modify the thread-safe value. | |
var value: Value { return mutex.sync { return _value } } | |
/// Submits a block for synchronous, thread-safe execution. | |
mutating func value<T>(execute task: (inout Value) throws -> T) rethrows -> T { | |
return try mutex.sync(flags: .barrier) { return try task(&_value) } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment