Skip to content

Commit

Permalink
send actions to store
Browse files Browse the repository at this point in the history
  • Loading branch information
square-tomb committed Dec 21, 2023
1 parent a5c285e commit 32668e4
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 66 deletions.
22 changes: 13 additions & 9 deletions Samples/SwiftUITestbed/Sources/MainScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ import WorkflowUI
// conformance of type 'ViewModel<State, Action>' to protocol 'SwiftUIScreen'
// does not imply conformance to inherited protocol 'Screen'"
extension MainWorkflow.Rendering: SwiftUIScreen, Screen {
static func makeView(store: Store<State>, sendAction: @escaping (Action) -> Void) -> some View {
MainScreenView(model: store)
var model: Model {
self
}

public static func makeView(store: Store<MainWorkflow.State, MainWorkflow.Action>) -> some View {
MainView(store: store)
}
}

private struct MainScreenView: View {
@BindableStore var model: Store<MainWorkflow.State>
private struct MainView: View {
@BindableStore var store: Store<MainWorkflow.State, MainWorkflow.Action>

@Environment(\.viewEnvironment.marketStylesheet) private var styles: MarketStylesheet
@Environment(\.viewEnvironment.marketContext) private var context: MarketContext
Expand All @@ -49,16 +53,16 @@ private struct MainScreenView: View {

TextField(
"Text",
text: $model.title
text: $store.title
)
.focused($focusedField, equals: .title)
.onAppear { focusedField = .title }

ToggleRow(
style: context.stylesheets.testbed.toggleRow,
label: "All Caps",
isEnabled: model.allCapsToggleIsEnabled,
isOn: $model.allCapsToggleIsOn
isEnabled: store.allCapsToggleIsEnabled,
isOn: $store.allCapsToggleIsOn
)

Spacer(minLength: styles.spacings.spacing50)
Expand All @@ -68,12 +72,12 @@ private struct MainScreenView: View {

Button(
"Push Screen",
action: { fatalError("TODO") }
action: store.action(.pushScreen)
)

Button(
"Present Screen",
action: { fatalError("TODO") }
action: store.action(.presentScreen)
)

Button(
Expand Down
12 changes: 6 additions & 6 deletions Samples/SwiftUITestbed/Sources/Observation/BindableStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,18 @@ import SwiftUI
@available(watchOS, deprecated: 10, renamed: "Bindable")
@propertyWrapper
@dynamicMemberLookup
public struct BindableStore<State: ObservableState> {
public var wrappedValue: Store<State>
public init(wrappedValue: Store<State>) {
struct BindableStore<State: ObservableState, Action> {
var wrappedValue: Store<State, Action>
init(wrappedValue: Store<State, Action>) {
self.wrappedValue = wrappedValue
}

public var projectedValue: BindableStore<State> {
var projectedValue: BindableStore<State, Action> {
self
}

public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<Store<State>, Subject>
subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<Store<State, Action>, Subject>
) -> Binding<Subject> {
Binding(
get: { self.wrappedValue[keyPath: keyPath] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import SwiftUI
// NB: These overloads ensure runtime warnings aren't emitted for errant SwiftUI bindings.
#if DEBUG
extension Binding {
public subscript<State: ObservableState, Member: Equatable>(
subscript<
State: ObservableState,
Action,
Member: Equatable
>(
dynamicMember keyPath: WritableKeyPath<State, Member>
) -> Binding<Member>
where Value == Store<State> {
where Value == Store<State, Action> {
Binding<Member>(
get: { self.wrappedValue.state[keyPath: keyPath] },
set: { _ in fatalError("TODO") }
Expand All @@ -19,10 +23,10 @@ extension Binding {

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
extension Bindable {
public subscript<State: ObservableState, Member: Equatable>(
subscript<State: ObservableState, Action, Member: Equatable>(
dynamicMember keyPath: WritableKeyPath<State, Member>
) -> Binding<Member>
where Value == Store<State> {
where Value == Store<State, Action> {
Binding<Member>(
get: { self.wrappedValue.state[keyPath: keyPath] },
set: { _ in fatalError("TODO") }
Expand Down
58 changes: 33 additions & 25 deletions Samples/SwiftUITestbed/Sources/Observation/Store.swift
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
import ComposableArchitecture // for ObservableState and Perception

@dynamicMemberLookup
public final class Store<State: ObservableState>: Perceptible {
private var _state: State
let _$observationRegistrar = PerceptionRegistrar()

fileprivate(set) var state: State {
get {
_$observationRegistrar.access(self, keyPath: \.state)
return _state
}
set {
if !_$isIdentityEqual(state, newValue) {
_$observationRegistrar.withMutation(of: self, keyPath: \.state) {
_state = newValue
}
} else {
_state = newValue
}
}
final class Store<State: ObservableState, Action>: Perceptible {
typealias Model = ViewModel<State, Action>

private var model: Model
private let _$observationRegistrar = PerceptionRegistrar()

var state: State {
_$observationRegistrar.access(self, keyPath: \.state)
return model.state
}

func send(_ action: Action) {
model.sendAction(action)
}

private init(state: State) {
self._state = state
fileprivate init(_ model: Model) {
self.model = model
}

fileprivate func setModel(_ newValue: Model) {
if !_$isIdentityEqual(model.state, newValue.state) {
_$observationRegistrar.withMutation(of: self, keyPath: \.state) {
model = newValue
}
} else {
model = newValue
}
}
}

public extension Store {
static func make(initialState: State) -> (Store, (State) -> Void) {
let store = Store(state: initialState)
let setState = { store.state = $0 }
return (store, setState)
extension Store {
static func make(model: Model) -> (Store, (Model) -> Void) {
let store = Store(model)
return (store, store.setModel)
}

subscript<T>(dynamicMember keyPath: KeyPath<State, T>) -> T {
state[keyPath: keyPath]
}

func action(_ action: Action) -> () -> Void {
{ self.send(action) }
}
}

extension Store: Equatable {
Expand Down
35 changes: 13 additions & 22 deletions Samples/SwiftUITestbed/Sources/Observation/SwiftUIScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,36 @@ import SwiftUI
import Workflow
import WorkflowUI

struct ViewModel<State: ObservableState, Action> {
let state: State
let sendAction: (Action) -> Void
}

protocol SwiftUIScreen: Screen {
associatedtype State: ObservableState
associatedtype Action
associatedtype Content: View

var state: State { get }
var sendAction: (Action) -> Void { get }
typealias Model = ViewModel<State, Action>

var model: Model { get }

@ViewBuilder
static func makeView(store: Store<State>, sendAction: @escaping (Action) -> Void) -> Content
static func makeView(store: Store<State, Action>) -> Content
}

extension SwiftUIScreen {
func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
ViewControllerDescription(
type: ModeledHostingController<Self.State, EnvironmentInjectingView<Content>>.self,
type: ModeledHostingController<Model, EnvironmentInjectingView<Content>>.self,
environment: environment,
build: {
let (store, setState) = Store.make(initialState: state)
let (store, setModel) = Store.make(model: model)
return ModeledHostingController(
setState: setState,
setModel: setModel,
rootView: EnvironmentInjectingView(
environment: environment,
content: Self.makeView(
store: store,
sendAction: { _ in
fatalError("TODO")
}
)
content: Self.makeView(store: store)
)
)
},
update: { hostingController in
hostingController.setState(state)
hostingController.setModel(model)
// TODO: update viewEnvironment
}
)
Expand All @@ -60,11 +51,11 @@ private struct EnvironmentInjectingView<Content: View>: View {
}
}

private final class ModeledHostingController<State, Content: View>: UIHostingController<Content> {
let setState: (State) -> Void
private final class ModeledHostingController<Model, Content: View>: UIHostingController<Content> {
let setModel: (Model) -> Void

init(setState: @escaping (State) -> Void, rootView: Content) {
self.setState = setState
init(setModel: @escaping (Model) -> Void, rootView: Content) {
self.setModel = setModel
super.init(rootView: rootView)
}

Expand Down
6 changes: 6 additions & 0 deletions Samples/SwiftUITestbed/Sources/Observation/ViewModel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import ComposableArchitecture

struct ViewModel<State: ObservableState, Action> {
let state: State
let sendAction: (Action) -> Void
}

0 comments on commit 32668e4

Please sign in to comment.