Skip to content

Commit

Permalink
feat: expose helpers for static models (#311)
Browse files Browse the repository at this point in the history
Creating a static model is useful for testing, and the missing `public init` on `ActionModel` makes that difficult to do. This PR adds that init along with a couple static factory methods for that use case.
  • Loading branch information
watt authored Nov 14, 2024
1 parent a29d9d1 commit 87c8f25
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 16 deletions.
28 changes: 26 additions & 2 deletions WorkflowSwiftUI/Sources/ActionModel.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
/// An ``ObservableModel`` for workflows with a single action.
///
/// Rather than creating this model directly, you should use the
/// ``Workflow/RenderContext/makeActionModel(state:)`` method to create an instance of this model.
/// To create an accessor, use
/// ``Workflow/RenderContext/makeActionModel(state:)``. State writes and actions
/// will be sent to the workflow.
public struct ActionModel<State: ObservableState, Action>: ObservableModel, SingleActionModel {
public let accessor: StateAccessor<State>
public let sendAction: (Action) -> Void

/// Creates a new ActionModel.
///
/// Rather than creating this model directly, you should usually use the
/// ``Workflow/RenderContext/makeActionModel(state:)`` method to create an
/// instance of this model. If you need a static model for testing or
/// previews, you can use the ``constant(state:)`` method.
public init(accessor: StateAccessor<State>, sendAction: @escaping (Action) -> Void) {
self.accessor = accessor
self.sendAction = sendAction
}
}

/// An observable model with a single action.
Expand All @@ -22,3 +34,15 @@ extension ActionModel: Identifiable where State: Identifiable {
accessor.id
}
}

#if DEBUG

public extension ActionModel {
/// Creates a static model which ignores all sent values, suitable for static previews
/// or testing.
static func constant(state: State) -> ActionModel<State, Action> {
ActionModel(accessor: .constant(state: state), sendAction: { _ in })
}
}

#endif
18 changes: 18 additions & 0 deletions WorkflowSwiftUI/Sources/StateAccessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ public struct StateAccessor<State: ObservableState> {
let state: State
let sendValue: (@escaping (inout State) -> Void) -> Void

/// Creates a new state accessor.
///
/// Rather than creating this model directly, you should usually use the
/// ``Workflow/RenderContext/makeStateAccessor(state:)`` method. If you need
/// a static model for testing or previews, you can use the
/// ``constant(state:)`` method.
public init(
state: State,
sendValue: @escaping (@escaping (inout State) -> Void) -> Void
Expand All @@ -31,3 +37,15 @@ extension StateAccessor: Identifiable where State: Identifiable {
state.id
}
}

#if DEBUG

public extension StateAccessor {
/// Creates a static state accessor which ignores all sent values, suitable for static previews
/// or testing.
static func constant(state: State) -> StateAccessor<State> {
StateAccessor(state: state, sendValue: { _ in })
}
}

#endif
22 changes: 8 additions & 14 deletions WorkflowSwiftUI/Sources/Store+Preview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,17 @@ public struct StaticStorePreviewContext {
}

public func makeStateAccessor<State>(state: State) -> StateAccessor<State> {
StateAccessor(
state: state,
sendValue: { _ in }
)
.constant(state: state)
}

public func makeActionModel<State, Action>(
state: State
) -> ActionModel<State, Action> {
ActionModel(
accessor: makeStateAccessor(state: state),
sendAction: makeSink(of: Action.self).send
)
.constant(state: state)
}
}

extension Store {
public extension Store {
/// Generates a static store for previews.
///
/// Previews generated with this method are static and do not update state. To generate a
Expand All @@ -38,15 +32,15 @@ extension Store {
/// - Parameter makeModel: A closure to create the store's model. The provided `context` param
/// is a convenience to generate dummy sinks and state accessors.
/// - Returns: A store for previews.
public static func preview(
static func preview(
makeModel: (StaticStorePreviewContext) -> Model
) -> Store {
let context = StaticStorePreviewContext()
let model = makeModel(context)
let (store, _) = make(model: model)
return store
}

/// Generates a static store for previews.
///
/// Previews generated with this method are static and do not update state. To generate a
Expand All @@ -55,14 +49,14 @@ extension Store {
///
/// - Parameter state: The state of the view.
/// - Returns: A store for previews.
public static func preview<State, Action>(
static func preview<State, Action>(
state: State
) -> Store<ActionModel<State, Action>> where Model == ActionModel<State, Action> {
preview { context in
context.makeActionModel(state: state)
}
}

/// Generates a static store for previews.
///
/// Previews generated with this method are static and do not update state. To generate a
Expand All @@ -71,7 +65,7 @@ extension Store {
///
/// - Parameter state: The state of the view.
/// - Returns: A store for previews.
public static func preview<State>(
static func preview<State>(
state: State
) -> Store<StateAccessor<State>> where Model == StateAccessor<State> {
preview { context in
Expand Down

0 comments on commit 87c8f25

Please sign in to comment.