-
Notifications
You must be signed in to change notification settings - Fork 44
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Observable State for WorkflowSwiftUI (#283)
This is a complete implementation of Workflow-powered SwiftUI views using fine-grained observability to minimize render impact. It's based on the prototype in #276, and @square-tomb's previous prototype #260. Squares can learn more at go/workflow-swiftui. # Observation We're depending on Perception, Point-Free's backport of Observation. The approach to observable state is adapted from TCA's approach, with a custom `@ObservableState` macro that endows struct types with a concept of identity. In workflows, you must annotate your state with `@ObservableState`, and render a type conforming to `ObservableModel`, which wraps your state and sends mutations into the unidirectional flow. There are several built-in conveniences for rendering common cases, or you can create a custom type. On the view side, your `View` will consume a `Store<Model>` wrapper, which provides access to state, sinks, and any child stores from nested workflows. To wire things up, you can implement a trivial type conforming to `ObservableScreen` and map your rendering. It's *strongly* recommended to keep these layers separate: render a model, implement a view, and create a screen only where needed. This allows for compositions of workflows and views, which is difficult or impossible if rendering a screen directly. Check out the new ObservableScreen sample app for complete examples of most concepts being introduced here. I'll also write up an adoption guide that expands on each requirement. # SwiftPM The `@ObservableState` macro means we cannot ship `WorkflowSwiftUI` with CocoaPods. For now, this PR only removes WorkflowSwiftUI podspec, but it may be preferable to remove all podpsecs and migrate everything to SwiftPM to reduce the maintenance burden. To work on WorkflowSwiftUI locally, you can use [xcodegen](https://github.com/yonaskolb/XcodeGen) to create a project, similarly to how `pod gen` works.
- Loading branch information
Showing
44 changed files
with
5,344 additions
and
255 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
Part of the distributed code is also derived in part from | ||
https://github.com/pointfreeco/swift-composable-architecture, licensed under MIT | ||
(https://github.com/pointfreeco/swift-composable-architecture/blob/main/LICENSE). | ||
Copyright (c) 2020 Point-Free, Inc. | ||
|
||
Part of the distributed code is also derived in part from | ||
https://github.com/apple/swift licensed under Apache | ||
(https://github.com/apple/swift/blob/main/LICENSE.txt). Copyright 2024 Apple, | ||
Inc. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import UIKit | ||
import Workflow | ||
import WorkflowUI | ||
|
||
@main | ||
class AppDelegate: UIResponder, UIApplicationDelegate { | ||
var window: UIWindow? | ||
|
||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { | ||
let root = WorkflowHostingController( | ||
workflow: MultiCounterWorkflow().mapRendering(MultiCounterScreen.init) | ||
) | ||
root.view.backgroundColor = .systemBackground | ||
|
||
window = UIWindow(frame: UIScreen.main.bounds) | ||
window?.rootViewController = root | ||
window?.makeKeyAndVisible() | ||
|
||
return true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
import SwiftUI | ||
import ViewEnvironment | ||
import WorkflowSwiftUI | ||
|
||
struct CounterView: View { | ||
typealias Model = CounterModel | ||
|
||
let store: Store<Model> | ||
let key: String | ||
|
||
var body: some View { | ||
let _ = Self._printChanges() | ||
WithPerceptionTracking { | ||
let _ = print("Evaluated CounterView[\(key)] body") | ||
HStack { | ||
Text(store.info.name) | ||
|
||
Spacer() | ||
|
||
Button { | ||
store.send(.decrement) | ||
} label: { | ||
Image(systemName: "minus") | ||
} | ||
|
||
Text("\(store.count)") | ||
.monospacedDigit() | ||
|
||
Button { | ||
store.send(.increment) | ||
} label: { | ||
Image(systemName: "plus") | ||
} | ||
|
||
if let maxValue = store.maxValue { | ||
Text("(max \(maxValue))") | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
#if DEBUG | ||
|
||
#Preview { | ||
CounterView( | ||
store: .preview( | ||
state: .init( | ||
count: 0, | ||
info: .init( | ||
name: "Preview counter", | ||
stepSize: 1 | ||
) | ||
) | ||
), | ||
key: "preview" | ||
) | ||
.padding() | ||
} | ||
|
||
#endif |
Oops, something went wrong.