Skip to content

Commit c450501

Browse files
committed
Refactored Connectable API
1 parent 3a0332f commit c450501

File tree

8 files changed

+83
-79
lines changed

8 files changed

+83
-79
lines changed

Sources/SwiftDux/UI/ActionBinding.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public struct ActionBinding<Value> {
1919

2020
@inlinable internal init(value: Value, isEqual: @escaping (Value) -> Bool, set: @escaping (Value) -> Void) {
2121
self.isEqual = isEqual
22-
projectedValue = Binding(get: { value }, set: set)
22+
self.projectedValue = Binding(get: { value }, set: set)
2323
}
2424

2525
@inlinable static internal func constant<T>(value: T) -> ActionBinding<T> {
@@ -32,14 +32,14 @@ public struct ActionBinding<Value> {
3232

3333
/// Returns a regular binding.
3434
/// - Returns: The binding.
35-
public func toBinding() -> Binding<Value> {
35+
@inlinable public func toBinding() -> Binding<Value> {
3636
projectedValue
3737
}
3838
}
3939

4040
extension ActionBinding: Equatable {
4141

42-
public static func == (lhs: ActionBinding<Value>, rhs: ActionBinding<Value>) -> Bool {
42+
@inlinable public static func == (lhs: ActionBinding<Value>, rhs: ActionBinding<Value>) -> Bool {
4343
lhs.isEqual(rhs.wrappedValue) && rhs.isEqual(lhs.wrappedValue)
4444
}
4545
}

Sources/SwiftDux/UI/Connectable.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import SwiftUI
66
/// to a possible bug in Swift that throws an invalid assocated type if Props isn't explicitly typealiased.
77
public protocol Connectable {
88

9-
associatedtype Superstate
9+
associatedtype State
1010
associatedtype Props: Equatable
1111

1212
/// Map a superstate to the state needed by the view using the provided parameter.
@@ -15,7 +15,7 @@ public protocol Connectable {
1515
/// will not be rendered.
1616
/// - Parameter state: The superstate provided to the view from a superview.
1717
/// - Returns: The state if possible.
18-
func map(state: Superstate) -> Props?
18+
func map(state: State) -> Props?
1919

2020
/// Map a superstate to the state needed by the view using the provided parameter.
2121
///
@@ -25,18 +25,18 @@ public protocol Connectable {
2525
/// - state: The superstate provided to the view from a superview.
2626
/// - binder: Helper that creates Binding types beteen the state and a dispatcable action
2727
/// - Returns: The state if possible.
28-
func map(state: Superstate, binder: ActionBinder) -> Props?
28+
func map(state: State, binder: ActionBinder) -> Props?
2929
}
3030

3131
extension Connectable {
3232

3333
/// Default implementation. Returns nil.
34-
@inlinable public func map(state: Superstate) -> Props? {
34+
@inlinable public func map(state: State) -> Props? {
3535
nil
3636
}
3737

3838
/// Default implementation. Calls the other map function.
39-
@inlinable public func map(state: Superstate, binder: ActionBinder) -> Props? {
39+
@inlinable public func map(state: State, binder: ActionBinder) -> Props? {
4040
map(state: state)
4141
}
4242
}

Sources/SwiftDux/UI/ConnectableView.swift

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import SwiftUI
33
/// A view that connects to the application state.
44
public protocol ConnectableView: View, Connectable {
55
associatedtype Content: View
6-
associatedtype Body = Connector<Content, Superstate, Props>
6+
associatedtype Body = Connector<Content, State, Props>
77

88
/// Return the body of the view using the provided props object.
99
/// - Parameter props: A mapping of the application to the props used by the view.
@@ -13,13 +13,7 @@ public protocol ConnectableView: View, Connectable {
1313

1414
extension ConnectableView {
1515

16-
/// Concrete return type is nessarry to avoid segment fault in release builds of apps.
17-
public var body: Connector<Content, Superstate, Props> {
18-
Connector<Content, Superstate, Props>(
19-
content: { props in
20-
self.body(props: props)
21-
},
22-
mapProps: map
23-
)
16+
public var body: Connector<Content, State, Props> {
17+
Connector(mapState: map, content: body)
2418
}
2519
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import SwiftUI
2+
3+
/// A view modifier that connects to the application state.
4+
public protocol ConnectableViewModifier: ViewModifier, Connectable {
5+
associatedtype InnerBody: View
6+
associatedtype Body = Connector<InnerBody, State, Props>
7+
8+
/// Return the body of the view modifier using the provided props object.
9+
/// - Parameters:
10+
/// - props: A mapping of the application to the props used by the view.
11+
/// - content: The content of the view modifier.
12+
/// - Returns: The connected view.
13+
func body(props: Props, content: Content) -> InnerBody
14+
}
15+
16+
extension ConnectableViewModifier {
17+
18+
public func body(content: Content) -> Connector<InnerBody, State, Props> {
19+
Connector(mapState: map) { props in
20+
body(props: props, content: content)
21+
}
22+
}
23+
}
Lines changed: 15 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,40 @@
11
import Combine
22
import SwiftUI
33

4-
public struct Connector<Content, Superstate, Props>: View where Props: Equatable, Content: View {
4+
public struct Connector<Content, State, Props>: View where Props: Equatable, Content: View {
55
@Environment(\.store) private var anyStore
6-
@Environment(\.actionDispatcher) private var actionDispatcher
6+
@Environment(\.actionDispatcher) private var dispatch
77

8+
private var mapState: (State, ActionBinder) -> Props?
89
private var content: (Props) -> Content
9-
private var mapProps: (Superstate, ActionBinder) -> Props?
10+
@SwiftUI.State private var props: Props?
1011

11-
private var store: StoreProxy<Superstate>? {
12+
private var store: StoreProxy<State>? {
1213
if anyStore is NoopAnyStore {
1314
return nil
14-
} else if let store = anyStore.unwrap(as: Superstate.self) {
15+
} else if let store = anyStore.unwrap(as: State.self) {
1516
return store
1617
}
17-
fatalError("Tried mapping the state to a view, but the Store<_> doesn't conform to '\(Superstate.self)'")
18+
fatalError("Tried mapping the state to a view, but the Store<_> doesn't conform to '\(State.self)'")
1819
}
1920

2021
public init(
21-
content: @escaping (Props) -> Content,
22-
mapProps: @escaping (Superstate, ActionBinder) -> Props?
22+
mapState: @escaping (State, ActionBinder) -> Props?,
23+
@ViewBuilder content: @escaping (Props) -> Content
2324
) {
2425
self.content = content
25-
self.mapProps = mapProps
26+
self.mapState = mapState
2627
}
2728

2829
public var body: some View {
2930
store.map { store in
30-
ConnectorInner(
31-
content: content,
32-
initialProps: getProps(),
33-
propsPublisher: store.didChange
34-
.compactMap { _ in getProps() }
35-
.removeDuplicates()
36-
)
31+
Group {
32+
props.map { content($0) }
33+
}.onReceive(store.publish(mapState)) { self.props = $0 }
3734
}
3835
}
3936

40-
private func getProps() -> Props? {
41-
guard let store = store else { return nil }
42-
return mapProps(store.state, ActionBinder(actionDispatcher: self.actionDispatcher))
43-
}
44-
}
45-
46-
internal struct ConnectorInner<Props, PropsPublisher, Content>: View
47-
where Props: Equatable, PropsPublisher: Publisher, PropsPublisher.Output == Props, PropsPublisher.Failure == Never, Content: View {
48-
private var content: (Props) -> Content
49-
private var propsPublisher: PropsPublisher
50-
@State private var props: Props?
51-
52-
internal init(content: @escaping (Props) -> Content, initialProps: Props?, propsPublisher: PropsPublisher) {
53-
self.content = content
54-
self.propsPublisher = propsPublisher
55-
self._props = State(initialValue: initialProps)
56-
}
57-
58-
var body: some View {
59-
return props.map { content($0).onReceive(propsPublisher) { self.props = $0 } }
37+
private func mapState(state: State) -> Props? {
38+
mapState(state, ActionBinder(actionDispatcher: dispatch))
6039
}
6140
}

Sources/SwiftDux/UI/Extensions/Environment+ActionDispatcher.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension EnvironmentValues {
1818

1919
/// Environment value to supply an actionDispatcher. This is used by the MappedDispatch to retrieve
2020
/// an action dispatcher from the environment.
21-
internal var actionDispatcher: ActionDispatcher {
21+
public var actionDispatcher: ActionDispatcher {
2222
get {
2323
self[ActionDispatcherKey.self]
2424
}

Sources/SwiftDux/UI/ViewModifiers/OnAppearDispatchViewModifier.swift

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,41 @@
11
import Combine
2+
import Dispatch
23
import SwiftUI
34

45
public struct OnAppearDispatchActionViewModifier: ViewModifier {
5-
@MappedDispatch() private var dispatch
6-
7-
var action: Action
8-
6+
@Environment(\.actionDispatcher) private var dispatch
97
@State private var cancellable: Cancellable? = nil
8+
private var action: Action
109

1110
@usableFromInline internal init(action: Action) {
1211
self.action = action
1312
}
1413

1514
public func body(content: Content) -> some View {
16-
content.onAppear { [action, dispatch] in dispatch(action) }
15+
content.onAppear { dispatch(action) }
1716
}
1817
}
1918

20-
public struct OnAppearDispatchActionPlanViewModifier<T>: ViewModifier {
21-
@MappedDispatch() private var dispatch
19+
public struct OnAppearDispatchActionPlanViewModifier: ViewModifier {
20+
@Environment(\.actionDispatcher) private var dispatch
2221

23-
var actionPlan: ActionPlan<T>
24-
var cancelOnDisappear: Bool
22+
private var action: RunnableAction
23+
private var cancelOnDisappear: Bool
2524

2625
@State private var cancellable: Cancellable? = nil
2726

28-
@usableFromInline internal init(actionPlan: ActionPlan<T>, cancelOnDisappear: Bool) {
29-
self.actionPlan = actionPlan
27+
@usableFromInline internal init(action: RunnableAction, cancelOnDisappear: Bool) {
28+
self.action = action
3029
self.cancelOnDisappear = cancelOnDisappear
3130
}
3231

3332
public func body(content: Content) -> some View {
3433
content
35-
.onAppear { [actionPlan, dispatch] in
36-
guard self.cancellable == nil else { return }
37-
self.cancellable = actionPlan.sendAsCancellable(dispatch)
34+
.onAppear {
35+
guard cancellable == nil else { return }
36+
self.cancellable = action.sendAsCancellable(dispatch)
3837
}
39-
.onDisappear { [cancelOnDisappear] in
38+
.onDisappear {
4039
if cancelOnDisappear {
4140
self.cancellable?.cancel()
4241
self.cancellable = nil
@@ -52,7 +51,12 @@ extension View {
5251
/// - Parameter action: An action to dispatch every time the view appears.
5352
/// - Returns: The modified view.
5453
@inlinable public func onAppear(dispatch action: Action) -> some View {
55-
modifier(OnAppearDispatchActionViewModifier(action: action))
54+
Group {
55+
if let action = action as? RunnableAction {
56+
modifier(OnAppearDispatchActionPlanViewModifier(action: action, cancelOnDisappear: true))
57+
}
58+
modifier(OnAppearDispatchActionViewModifier(action: action))
59+
}
5660
}
5761

5862
/// Sends the provided action plan when the view appears.
@@ -99,11 +103,11 @@ extension View {
99103
/// ```
100104
///
101105
/// - Parameters:
102-
/// - actionPlan: An action to dispatch every time the view appears.
106+
/// - action: An action to dispatch every time the view appears.
103107
/// - cancelOnDisappear: It will cancel any subscription from the action when the view disappears. If false, it keeps
104108
/// the subscription alive and reppearances of the view will not re-call the action.
105109
/// - Returns: The modified view.
106-
@inlinable public func onAppear<T>(dispatch actionPlan: ActionPlan<T>, cancelOnDisappear: Bool = true) -> some View {
107-
modifier(OnAppearDispatchActionPlanViewModifier(actionPlan: actionPlan, cancelOnDisappear: cancelOnDisappear))
110+
@inlinable public func onAppear<T>(dispatch action: RunnableAction, cancelOnDisappear: Bool) -> some View {
111+
modifier(OnAppearDispatchActionPlanViewModifier(action: action, cancelOnDisappear: cancelOnDisappear))
108112
}
109113
}

Sources/SwiftDux/UI/ViewModifiers/StoreProviderViewModifier.swift

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ import SwiftUI
33

44
/// A view modifier that injects a store into the environment.
55
internal struct StoreProviderViewModifier: ViewModifier {
6-
var anyStore: AnyStore
6+
private var store: AnyStore
77

8-
func body(content: Content) -> some View {
8+
init(store: AnyStore) {
9+
self.store = store
10+
}
11+
12+
public func body(content: Content) -> some View {
913
content
10-
.environment(\.store, anyStore)
11-
.environment(\.actionDispatcher, anyStore)
14+
.environment(\.store, store)
15+
.environment(\.actionDispatcher, store)
1216
}
1317
}
1418

@@ -34,10 +38,10 @@ extension View {
3438
/// - Parameter store: The store object to inject.
3539
/// - Returns: The modified view.
3640
public func provideStore<State>(_ store: Store<State>) -> some View where State: Equatable {
37-
return modifier(StoreProviderViewModifier(anyStore: AnyStoreWrapper(store: store)))
41+
modifier(StoreProviderViewModifier(store: AnyStoreWrapper(store: store)))
3842
}
3943

4044
public func provideStore(_ store: AnyStore) -> some View {
41-
return modifier(StoreProviderViewModifier(anyStore: store))
45+
modifier(StoreProviderViewModifier(store: store))
4246
}
4347
}

0 commit comments

Comments
 (0)