Swift UI API interface for SwiftCurrent #61
-
Upon working on the first implementation of SwiftUI with our AnyView wrapping inside a presenter, we ended up hitting a fair number of issues, that mostly stemmed from the way we interfaced with our Workflow. We decided to take a huge step back, and work through what kind of API we would want in a SwiftUI world. For code snippets and where we're at right now, see the branch: swiftUI-again-spike and see the SampleView.swift file. We currently have an API like this: var workflow: Workflow = Workflow(FR1.self)
.thenProceed(with: FR2.self)
var body: some View {
SwiftUIResponder2(workflow: workflow) { _ in
}
} The above does not seem very SwiftUI-like and has been causing some issues with our sample apps as we have been implementing it. var body: some View {
// Option 1
SwiftUIResponder2(workflow: workflow) { _ in
}.present(workflow)
// Option 2
present(workflow)
// Option 3
workflow.present()
} The above were deemed rejected as they too did not feel like SwiftUI. Leaving us with the realization that the API for SwiftCurrent really does need to be seamless with the environment it is in. What we did for UIKit is not exactly what we will do for SwiftUI. The closes we have gotten so far are these two options with more to come: // Option 1
WorkflowGroup {
FirstView() // but even if it takes parameters just use the () initializer
SecondView()
.background(shiftLeading ? .red : .blue)
.transition(shiftLeading ? .slide : .fade)
...
OneBeforeNthView()
.show(when: .proceeding) //remove from stack when proceeding
NthView()
.show(when: .backingUp) //don't display unless backing up
}
// Option 2
WorkflowGroup {
ThenPresent(FirstView())
ReplaceWith(SecondView())
.background(shiftLeading ? .red : .blue)
} The first does not make it clear that each view will be shown sequentially, and the second is working through different options to make that clear. At this point we have realized that we should open up this thought process to a Discussion topic. Mostly because this is a lot to put into an EOD on an issue, and second, because this issue is going to take quite a bit of deliberating on. Ultimately we may end up where we started, but we should definitely take the time to make sure the experience as a Developer, is the experience we would all want. |
Beta Was this translation helpful? Give feedback.
Replies: 13 comments 17 replies
-
Something we could take for inspiration on how to create many views is the LazyVStack: LazyVStack(alignment: .center, spacing: nil, pinnedViews: [], content: {
ForEach(1...10, id: \.self) { count in
Text("Placeholder \(count)")
}
}) It's something that takes in a few parameters and then creates numerous views, when needed. |
Beta Was this translation helpful? Give feedback.
-
My $0.02 don't have an initializer for things that take arguments...that doesn't take arguments. You'll break all kinds of compiler safety and Swift expectations. You can use types, and make those types accept view modifiers like other views do, or you can even wrap things to fit better: FlowRepresentableView(FR1.self)
.background(.red) You can theoretically lose the fluent API in favor of a composable SwiftUI type API, but I wouldn't worry about peripheral words like WorkflowView {
WorkflowItem(FR1.self)
WorkflowItem(FR2.self)
.persistence(.removedAfterProceeding)
.launchStyle(.modal(.fullScreen))
WorkflowItem(FR3.self)
.padding(10)
}.launchStyle(.navigationStack) This also has a big advantage for other types of patterns, if you're a big believer in composable architecture's state exposure then you can easily do it using this method. Whereas with some of the others I see I feel like you give up a lot of flexibility and walk away some really unclear expectations of a thing that initializes but... doesn't? |
Beta Was this translation helpful? Give feedback.
-
A bit of details that didn't get documented above. We know what experience we want when it comes to the WorkflowGroup thing itself. When you have a parent view (ContentView) and it has its own state and its own layout that changes on state, it should be able to update that as necessary WITHOUT impacting the state of the presented view. So if you were on FR3 and it had its own state of "input received", that state should be preserved, EVEN THOUGH ContentView relaid out. @wiemerm threw together a quick picture if people need a visual representation, but hopefully this is clear enough. |
Beta Was this translation helpful? Give feedback.
-
Starting a new thread for visibility. We had another riff on the latest suggestion: WorkflowView {
Workflow(WorkflowItem(FR1.self))
.thenProceed(with: WorfklowItem(FR2.self).padding(10))
} The thought being that because these are views we are swapping out, it will be really important to maintain the ability to modify the views. I will care A LOT about how my views are transitioning within a single page, unlike in UIKit where the paradigm is that the entire view is being replaced/covered. |
Beta Was this translation helpful? Give feedback.
-
As we discussed some of these options, where we are currently leaning is that a readable API is more important than for sure compile time safety in the data types of your items. One of the arguments being, that everyone will be impacted by the API and only some will be impacted by the compiler not flagging the data types as invalid in the flow. Additionally, we are not saying we couldn't, just that we are valuing readable API over compile time safety. |
Beta Was this translation helpful? Give feedback.
-
Here are some scenarios we should be considering to verify the API we are leaning towards:
|
Beta Was this translation helpful? Give feedback.
-
SwiftUIResponder2Option 1var body: some View {
SwiftUIResponder2(workflow: workflow) { _ in
}.present(workflow)
} Pros
Cons
Option 2var body: some View {
present(workflow)
} Presents itself as modular but is not in actuality Option 3var body: some View {
workflow.present()
} Cons
Applies to All OptionsPros
Cons
Captured Thoughts
|
Beta Was this translation helpful? Give feedback.
-
WorkflowGroupOption 1// Option 1
WorkflowGroup {
FirstView() // but even if it takes parameters just use the () initializer
SecondView()
.background(shiftLeading ? .red : .blue)
.transition(shiftLeading ? .slide : .fade)
...
OneBeforeNthView()
.show(when: .proceeding) //remove from stack when proceeding
NthView()
.show(when: .backingUp) //don't display unless backing up
} Pros
Cons
Option 2// Option 2
WorkflowGroup {
ThenPresent(FirstView())
ReplaceWith(SecondView())
.background(shiftLeading ? .red : .blue)
} Pros
All OptionsPros
Cons
|
Beta Was this translation helpful? Give feedback.
-
WorkflowViewTyler'sWorkflowView {
WorkflowItem(FR1.self)
WorkflowItem(FR2.self)
.persistence(.removedAfterProceeding)
.launchStyle(.modal(.fullScreen))
WorkflowItem(FR3.self)
.padding(10)
}.launchStyle(.navigationStack) Pros
Cons
AlternateWorkflowView {
WorkflowItem<FR1>()
WorkflowItem<FR2>()
.persistence(.removedAfterProceeding)
.launchStyle(.modal(.fullScreen))
WorkflowItem<FR3>()
.padding(10)
}.launchStyle(.navigationStack) Comments
|
Beta Was this translation helpful? Give feedback.
-
Enumpublic enum FooView<Content>: View where Content : View {
public var body: some View {
switch self {
default: EmptyView()
}
}
init(@ViewBuilder content: () -> Content) {
}
case foo: Button = Button()
case bar: Text = Text()
func foo() { }
var bar: Bool { true }
}
enum WorkflowViewy<Content>: View where Content : View { // The view that you are placing on your screen
case workflow // The sequence of views you want to show
case workflowItems(Content) // The specific wrapper around the view being presented at this point.
init(@ViewBuilder content: () -> Content) {
self = .workflowItems(content())
}
var body: some View {
EmptyView()
}
}
/*
WorkflowViewy {
WorkflowViewy.init {
SecondView()
.padding()
.foregroundColor(.blue)
.transition(.slide)
}
}
*/ Comments
WorkflowView {
Workflow {
WorkflowItem(FR1.self)
WorkflowItem(FR2.self)
}
}
|
Beta Was this translation helpful? Give feedback.
-
View Modifier/Presentation RouteVStack {
Button("Launch!") {
isPresented = true
}
}.workflow(isPresented: $isPresented,
launchStyle: .modal,
transition: .slide) {
WorkflowItem(FR1.self)
.padding()
.background(Color.blue)
} Comments
|
Beta Was this translation helpful? Give feedback.
-
Present FlowRepresentables as modifiers on WorkflowViewOption 1var body: some View {
WorkflowView()
.thenPresent(FR1.self)
.thenPresent(FR2.self)
} Pros
Cons
Option 2var body: some View {
WorkflowView(launchStyle: .inline)
.thenPresent(FR1.self, launchStyle: .navigation)
.thenPresent(FR2.self, modifiers: [.padding(16), .transition(.slide)])
} Pros
Cons
Option 3var body: some View {
WorkflowView()
.launchStyle(.inline)
.thenPresent(FR1.self)
.launchStyle(.navigation)
.thenPresent(FR2.self)
.padding()
.transition(.slide)
.background(Color.red)
// As seen in the wild
Text("1")
.background(Image(systemName: "circle"))
.frame(width: 200, height: 150)
.background(Color.blue)
} Pros
Cons
Comments Would we get a performance gain going this route? |
Beta Was this translation helpful? Give feedback.
-
Chosen direction We have decided on an API to move forward with. Ultimately the API will look like this: var body: some View {
// example 1: Always presented, 2 item workflow, without arguments
WorkflowView {
WorkflowItem(FR1.self)
WorkflowItem(FR2.self)
}
// example 2: Always presented, 2 item workflow, with arguments
WorkflowView(launchingWith: "MY name is!") {
WorkflowItem(FR1.self)
WorkflowItem(FR2.self)
}
// example 3: Fully customized workflow with animation on abandon.
WorkflowView(isLaunched: $isLaunched.animation(), launchingWith: "String in") {
WorkflowItem(FR1.self)
WorkflowItem(FR2.self)
.presentationStyle(.modal)
.persistence(.removedAfterProceeding)
.applyModifiers {
$0.background(Color.gray)
.transition(.slide)
.animation(.spring())
}
}
.onAbandon { print("presentingWorkflowView is now false") }
.onFinish { args in print("Finished 1: \(args)") }
.onFinish { print("Finished 2: \($0)") }
.background(Color.green)
// example 4: Conditionally built workflow
WorkflowView {
WorkflowItem(FR1.self)
if somePrecondition {
WorkflowItem(FR2.self)
}
WorkflowItem(FR3.self)
} This API offers several benefits:
In addition to these benefits, we fulfilled the scenarios listed here.
This is the direction we are heading. Not all modifiers will be implemented immediately nor will all names necessarily remain exactly this, but the general premise seems to be correct. If there are names that seem confusing or that you have suggestions to change to, please leave those as replies to this answer. Any and all feedback is welcome as well, so drop those in response to this answer as well. |
Beta Was this translation helpful? Give feedback.
Chosen direction
We have decided on an API to move forward with. Ultimately the API will look like this: