From 73d185df4c56ad944bed78c4b98909e02c92a25e Mon Sep 17 00:00:00 2001 From: Dominik Arnhof Date: Fri, 5 Jan 2024 13:40:54 +0100 Subject: [PATCH] add support for sync actions --- .../Search/RepositorySearchReactor.swift | 14 ++++- .../Search/RepositorySearchView.swift | 2 +- .../AsyncReactor/AsyncReactor+SwiftUI.swift | 51 +++++++++++++++++-- Sources/AsyncReactor/AsyncReactor.swift | 10 ++++ 4 files changed, 70 insertions(+), 7 deletions(-) diff --git a/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchReactor.swift b/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchReactor.swift index efbdefd..38ade6b 100644 --- a/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchReactor.swift +++ b/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchReactor.swift @@ -36,6 +36,10 @@ class RepositorySearchReactor: AsyncReactor { case onSortOptionSelected(SortOptions) } + enum SyncAction { + case toggleHidePrivate + } + struct State { var hidePrivate = false var query = "" @@ -92,9 +96,17 @@ class RepositorySearchReactor: AsyncReactor { catch { logger.error("error while searching repositories: \(error)") } + case .onSortOptionSelected(let option): state.sortBy = option - UserDefaults.standard.set(state.sortBy.rawValue, forKey: "sortBy") + UserDefaults.standard.set(option.rawValue, forKey: "sortBy") + } + } + + func action(_ action: SyncAction) { + switch action { + case .toggleHidePrivate: + state.hidePrivate.toggle() } } diff --git a/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchView.swift b/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchView.swift index a8312b6..e979d5e 100644 --- a/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchView.swift +++ b/Example/AsyncReactorExample/Features/Repository/Search/RepositorySearchView.swift @@ -13,7 +13,7 @@ struct RepositorySearchView: View { @EnvironmentObject private var reactor: RepositorySearchReactor - @ActionBinding(RepositorySearchReactor.self, keyPath: \.hidePrivate, action: RepositorySearchReactor.Action.onHidePrivateToggle) + @ActionBinding(RepositorySearchReactor.self, keyPath: \.hidePrivate, action: RepositorySearchReactor.SyncAction.toggleHidePrivate) private var hidePrivate: Bool @ActionBinding(RepositorySearchReactor.self, keyPath: \.query, cancelId: .init(id: "enterQuery", mode: .inFlight), action: RepositorySearchReactor.Action.enterQuery) diff --git a/Sources/AsyncReactor/AsyncReactor+SwiftUI.swift b/Sources/AsyncReactor/AsyncReactor+SwiftUI.swift index 283c45f..6eff5aa 100644 --- a/Sources/AsyncReactor/AsyncReactor+SwiftUI.swift +++ b/Sources/AsyncReactor/AsyncReactor+SwiftUI.swift @@ -28,38 +28,79 @@ extension AsyncReactor { public func bind(_ keyPath: KeyPath, cancelId: CancelId? = nil, action: @escaping @autoclosure () -> Action) -> Binding { bind(keyPath, cancelId: cancelId) { _ in action() } } + + @MainActor + public func bind(_ keyPath: KeyPath, action: @escaping (T) -> SyncAction) -> Binding { + Binding { + self.state[keyPath: keyPath] + } set: { newValue in + self.action(action(newValue)) + } + } + + @MainActor + public func bind(_ keyPath: KeyPath, action: @escaping @autoclosure () -> SyncAction) -> Binding { + bind(keyPath) { _ in action() } + } } /// Property wrapper to get a binding to a state keyPath and a associated Action /// Can be used and behaves like the `@State` property wrapper @MainActor @propertyWrapper -public struct ActionBinding: DynamicProperty { +public struct ActionBinding: DynamicProperty { let target: EnvironmentObject let keyPath: KeyPath - let action: (Value) -> Reactor.Action + let action: (Value) -> Action let cancelId: CancelId? - public init(_ reactorType: Reactor.Type, keyPath: KeyPath, cancelId: CancelId? = nil, action: @escaping (Value) -> Reactor.Action) { + public init(_ reactorType: Reactor.Type, keyPath: KeyPath, cancelId: CancelId? = nil, action: @escaping (Value) -> Reactor.Action) where Action == Reactor.Action { target = EnvironmentObject() self.keyPath = keyPath self.action = action self.cancelId = cancelId } - public init(_ reactorType: Reactor.Type, keyPath: KeyPath, cancelId: CancelId? = nil, action: @escaping @autoclosure () -> Reactor.Action) { + public init(_ reactorType: Reactor.Type, keyPath: KeyPath, cancelId: CancelId? = nil, action: @escaping @autoclosure () -> Reactor.Action) where Action == Reactor.Action { self.init(reactorType, keyPath: keyPath, cancelId: cancelId, action: { _ in action() }) } + public init(_ reactorType: Reactor.Type, keyPath: KeyPath, action: @escaping (Value) -> Reactor.SyncAction) where Action == Reactor.SyncAction { + target = EnvironmentObject() + self.keyPath = keyPath + self.action = action + cancelId = nil + } + + public init(_ reactorType: Reactor.Type, keyPath: KeyPath, action: @escaping @autoclosure () -> Reactor.SyncAction) where Action == Reactor.SyncAction { + self.init(reactorType, keyPath: keyPath, action: { _ in action() }) + } + public var wrappedValue: Value { get { projectedValue.wrappedValue } nonmutating set { projectedValue.wrappedValue = newValue } } public var projectedValue: Binding { - get { target.wrappedValue.bind(keyPath, cancelId: cancelId, action: action) } + get { + func bindAction() -> Binding { + target.wrappedValue.bind(keyPath, cancelId: cancelId, action: action as! (Value) -> Reactor.Action) + } + + func bindSyncAction() -> Binding { + target.wrappedValue.bind(keyPath, action: action as! (Value) -> Reactor.SyncAction) + } + + if Action.self == Reactor.Action.self { + return bindAction() + } else if Action.self == Reactor.SyncAction.self { + return bindSyncAction() + } else { + fatalError("this should never happen :)") + } + } } } diff --git a/Sources/AsyncReactor/AsyncReactor.swift b/Sources/AsyncReactor/AsyncReactor.swift index dfc70b8..cce75fd 100644 --- a/Sources/AsyncReactor/AsyncReactor.swift +++ b/Sources/AsyncReactor/AsyncReactor.swift @@ -10,6 +10,7 @@ import Foundation @dynamicMemberLookup public protocol AsyncReactor: ObservableObject { associatedtype Action + associatedtype SyncAction = Never associatedtype State @MainActor @@ -18,6 +19,9 @@ public protocol AsyncReactor: ObservableObject { @MainActor func action(_ action: Action) async + @MainActor + func action(_ action: SyncAction) + subscript(dynamicMember keyPath: KeyPath) -> Value { get } } @@ -28,6 +32,12 @@ extension AsyncReactor { } } +extension AsyncReactor where SyncAction == Never { + public func action(_ action: SyncAction) { + + } +} + // MARK: - DynamicMemberLookup extension AsyncReactor {