-
Notifications
You must be signed in to change notification settings - Fork 47
Equatable sinks prototype #254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: tomb/swiftui-testbed
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,16 +18,18 @@ import MarketWorkflowUI | |
import Workflow | ||
|
||
struct MainWorkflow: Workflow { | ||
let didClose: (() -> Void)? | ||
let canClose: Bool | ||
|
||
enum Output { | ||
case pushScreen | ||
case presentScreen | ||
case close | ||
} | ||
|
||
struct State { | ||
var title: String | ||
var isAllCaps: Bool | ||
let trampoline = SinkTrampoline() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like the best/only way to create a persistent identity that we can use to implement |
||
|
||
init(title: String) { | ||
self.title = title | ||
|
@@ -39,53 +41,96 @@ struct MainWorkflow: Workflow { | |
State(title: "New item") | ||
} | ||
|
||
enum Action: WorkflowAction { | ||
typealias WorkflowType = MainWorkflow | ||
|
||
case pushScreen | ||
case presentScreen | ||
case changeTitle(String) | ||
case changeAllCaps(Bool) | ||
|
||
func apply(toState state: inout WorkflowType.State) -> WorkflowType.Output? { | ||
switch self { | ||
case .pushScreen: | ||
return .pushScreen | ||
case .presentScreen: | ||
return .presentScreen | ||
case .changeTitle(let newValue): | ||
state.title = newValue | ||
state.isAllCaps = newValue.isAllCaps | ||
case .changeAllCaps(let isAllCaps): | ||
state.isAllCaps = isAllCaps | ||
state.title = isAllCaps ? state.title.uppercased() : state.title.lowercased() | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
typealias Rendering = MainScreen | ||
typealias Action = MainScreen.Action | ||
|
||
func render(state: State, context: RenderContext<Self>) -> Rendering { | ||
let sink = context.makeSink(of: Action.self) | ||
let sink = state.trampoline.makeSink(of: Action.self, with: context) | ||
|
||
return MainScreen( | ||
title: state.title, | ||
didChangeTitle: { sink.send(.changeTitle($0)) }, | ||
canClose: canClose, | ||
allCapsToggleIsOn: state.isAllCaps, | ||
allCapsToggleIsEnabled: !state.title.isEmpty, | ||
didChangeAllCapsToggle: { sink.send(.changeAllCaps($0)) }, | ||
didTapPushScreen: { sink.send(.pushScreen) }, | ||
didTapPresentScreen: { sink.send(.presentScreen) }, | ||
didTapClose: didClose | ||
sink: sink | ||
) | ||
} | ||
} | ||
|
||
extension MainScreen.Action: WorkflowAction { | ||
typealias WorkflowType = MainWorkflow | ||
|
||
func apply(toState state: inout WorkflowType.State) -> WorkflowType.Output? { | ||
switch self { | ||
case .pushScreen: | ||
return .pushScreen | ||
case .presentScreen: | ||
return .presentScreen | ||
case .changeTitle(let newValue): | ||
state.title = newValue | ||
state.isAllCaps = newValue.isAllCaps | ||
case .changeAllCaps(let isAllCaps): | ||
state.isAllCaps = isAllCaps | ||
state.title = isAllCaps ? state.title.uppercased() : state.title.lowercased() | ||
case .close: | ||
return .close | ||
} | ||
return nil | ||
} | ||
} | ||
|
||
private extension String { | ||
var isAllCaps: Bool { | ||
allSatisfy { character in | ||
character.isUppercase || !character.isCased | ||
} | ||
} | ||
} | ||
|
||
class SinkTrampoline: Equatable { | ||
private var sinks: [ObjectIdentifier: Any] = [:] | ||
|
||
func makeSink<Action, WorkflowType>( | ||
of actionType: Action.Type, | ||
with context: RenderContext<WorkflowType> | ||
) -> StableSink<Action> where Action: WorkflowAction, Action.WorkflowType == WorkflowType { | ||
let sink = context.makeSink(of: actionType) | ||
|
||
sinks[ObjectIdentifier(actionType)] = sink | ||
|
||
return StableSink(trampoline: self) | ||
Comment on lines
+97
to
+101
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we could pass Maintaining Is the need for that safety greater when the Rendering holds |
||
} | ||
|
||
func bounce<Action>(action: Action) { | ||
let sink = destination(for: Action.self) | ||
sink.send(action) | ||
} | ||
|
||
private func destination<Action>(for actionType: Action.Type) -> Sink<Action> { | ||
if let pipe = sinks[ObjectIdentifier(actionType)] { | ||
return pipe as! Sink<Action> | ||
} | ||
fatalError("bad plumbing") | ||
} | ||
|
||
static func == (lhs: SinkTrampoline, rhs: SinkTrampoline) -> Bool { | ||
lhs === rhs | ||
} | ||
} | ||
|
||
struct StableSink<Action>: Equatable { | ||
private var trampoline: SinkTrampoline | ||
|
||
init(trampoline: SinkTrampoline) { | ||
self.trampoline = trampoline | ||
} | ||
|
||
func send(_ action: Action) { | ||
trampoline.bounce(action: action) | ||
} | ||
|
||
// sugar instead of writing { sink.send($0) } | ||
func closure(_ action: Action) -> () -> Void { | ||
{ send(action) } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,9 +19,9 @@ import Workflow | |
import WorkflowUI | ||
|
||
struct RootWorkflow: Workflow { | ||
let close: (() -> Void)? | ||
|
||
typealias Output = Never | ||
enum Output { | ||
case close | ||
} | ||
Comment on lines
-22
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
struct State { | ||
var backStack: BackStack | ||
|
@@ -57,6 +57,8 @@ struct RootWorkflow: Workflow { | |
state.backStack.other.append(.main()) | ||
case .main(.presentScreen): | ||
state.isPresentingModal = true | ||
case .main(.close): | ||
return .close | ||
case .popScreen: | ||
state.backStack.other.removeLast() | ||
case .dismissScreen: | ||
|
@@ -75,7 +77,7 @@ struct RootWorkflow: Workflow { | |
func rendering(_ screen: State.Screen, isRoot: Bool) -> AnyMarketBackStackContentScreen { | ||
switch screen { | ||
case .main(let id): | ||
return MainWorkflow(didClose: isRoot ? close : nil) | ||
return MainWorkflow(canClose: isRoot) | ||
.mapOutput(Action.main) | ||
.mapRendering(AnyMarketBackStackContentScreen.init) | ||
.rendered(in: context, key: id.uuidString) | ||
|
@@ -96,7 +98,13 @@ struct RootWorkflow: Workflow { | |
base: backStack, | ||
modals: { | ||
guard state.isPresentingModal else { return [] } | ||
let screen = RootWorkflow(close: { sink.send(.dismissScreen) }) | ||
let screen = RootWorkflow() | ||
.mapOutput { output in | ||
switch output { | ||
case .close: | ||
return Action.dismissScreen | ||
} | ||
} | ||
.rendered(in: context) | ||
.asAnyScreen() | ||
let modal = Modal( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, I think I've seen this go by in POS
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just now noticing that SwiftUI's
FocusState
is not Equatable. That makes it harder forView
s to be Equatable, but I presume SwiftUI's internal comparison of non-Equatable View types handles it well.