Skip to content

Commit

Permalink
add support for sync actions
Browse files Browse the repository at this point in the history
  • Loading branch information
oanhof committed Jan 5, 2024
1 parent 2e41b87 commit 73d185d
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class RepositorySearchReactor: AsyncReactor {
case onSortOptionSelected(SortOptions)
}

enum SyncAction {
case toggleHidePrivate
}

struct State {
var hidePrivate = false
var query = ""
Expand Down Expand Up @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
51 changes: 46 additions & 5 deletions Sources/AsyncReactor/AsyncReactor+SwiftUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,79 @@ extension AsyncReactor {
public func bind<T>(_ keyPath: KeyPath<State, T>, cancelId: CancelId? = nil, action: @escaping @autoclosure () -> Action) -> Binding<T> {
bind(keyPath, cancelId: cancelId) { _ in action() }
}

@MainActor
public func bind<T>(_ keyPath: KeyPath<State, T>, action: @escaping (T) -> SyncAction) -> Binding<T> {
Binding {
self.state[keyPath: keyPath]
} set: { newValue in
self.action(action(newValue))
}
}

@MainActor
public func bind<T>(_ keyPath: KeyPath<State, T>, action: @escaping @autoclosure () -> SyncAction) -> Binding<T> {
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<Reactor: AsyncReactor, Value>: DynamicProperty {
public struct ActionBinding<Reactor: AsyncReactor, Action, Value>: DynamicProperty {
let target: EnvironmentObject<Reactor>

let keyPath: KeyPath<Reactor.State, Value>
let action: (Value) -> Reactor.Action
let action: (Value) -> Action

let cancelId: CancelId?

public init(_ reactorType: Reactor.Type, keyPath: KeyPath<Reactor.State, Value>, cancelId: CancelId? = nil, action: @escaping (Value) -> Reactor.Action) {
public init(_ reactorType: Reactor.Type, keyPath: KeyPath<Reactor.State, Value>, 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<Reactor.State, Value>, cancelId: CancelId? = nil, action: @escaping @autoclosure () -> Reactor.Action) {
public init(_ reactorType: Reactor.Type, keyPath: KeyPath<Reactor.State, Value>, 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<Reactor.State, Value>, 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<Reactor.State, Value>, 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<Value> {
get { target.wrappedValue.bind(keyPath, cancelId: cancelId, action: action) }
get {
func bindAction() -> Binding<Value> {
target.wrappedValue.bind(keyPath, cancelId: cancelId, action: action as! (Value) -> Reactor.Action)
}

func bindSyncAction() -> Binding<Value> {
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 :)")
}
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/AsyncReactor/AsyncReactor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Foundation
@dynamicMemberLookup
public protocol AsyncReactor: ObservableObject {
associatedtype Action
associatedtype SyncAction = Never
associatedtype State

@MainActor
Expand All @@ -18,6 +19,9 @@ public protocol AsyncReactor: ObservableObject {
@MainActor
func action(_ action: Action) async

@MainActor
func action(_ action: SyncAction)

subscript<Value>(dynamicMember keyPath: KeyPath<State, Value>) -> Value { get }
}

Expand All @@ -28,6 +32,12 @@ extension AsyncReactor {
}
}

extension AsyncReactor where SyncAction == Never {
public func action(_ action: SyncAction) {

}
}

// MARK: - DynamicMemberLookup

extension AsyncReactor {
Expand Down

0 comments on commit 73d185d

Please sign in to comment.