Skip to content

Instantly share code, notes, and snippets.

@Verdier
Last active March 27, 2024 02:26
Show Gist options
  • Save Verdier/746cb771f8ce4146d14c519934a51a2c to your computer and use it in GitHub Desktop.
Save Verdier/746cb771f8ce4146d14c519934a51a2c to your computer and use it in GitHub Desktop.
Redux SwiftUI
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: ... */
)
/* ... */
}
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)
}
}
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)
}
}
//
// 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