From 25c241e0aaef2a7d0f4a7398d30cd5f08f368fad Mon Sep 17 00:00:00 2001 From: Ben Cochran Date: Wed, 4 Mar 2020 10:25:15 -0800 Subject: [PATCH 1/2] Add ViewEnvironment (#739) ViewEnvironment is a container of strongly-typed key/value pairs that can be passed down the view-side of a workflow tree. This allows containers to provide view-oriented hinting to their children as well as allows for view-side changes to view-only environment without re-rendering the workflow tree (for example, runtime theme changes). --- .../Sources/BackStackContainer.swift | 10 +- .../Sources/BackStackScreen.swift | 4 +- .../Sources/ScreenWrapperViewController.swift | 10 +- .../Sources/CrossFadeContainer.swift | 16 +-- .../SampleApp/Sources/DemoScreen.swift | 10 +- .../SampleApp/Sources/WelcomeScreen.swift | 10 +- .../DemoApp/BarScreen.swift | 12 +-- .../DemoApp/FooScreen.swift | 12 +-- ...itScreenContainerScreenSnapshotTests.swift | 15 +-- .../Sources/Environment+SplitScreen.swift | 41 ++++++++ .../Sources/SplitScreenContainerScreen.swift | 4 +- .../SplitScreenContainerViewController.swift | 30 ++++-- .../Authentication/LoadingScreen.swift | 8 +- .../Sources/Authentication/LoginScreen.swift | 2 +- .../Authentication/TwoFactorScreen.swift | 10 +- .../Sources/Game/GamePlayScreen.swift | 10 +- .../Sources/Game/NewGameScreen.swift | 10 +- .../Sources/Welcome/WelcomeScreen.swift | 10 +- .../Sources/Todo/List/TodoListScreen.swift | 10 +- .../Sources/Welcome/WelcomeScreen.swift | 10 +- .../Sources/Todo/Edit/TodoEditScreen.swift | 10 +- .../Sources/Todo/List/TodoListScreen.swift | 10 +- .../Sources/Welcome/WelcomeScreen.swift | 10 +- .../Sources/Todo/Edit/TodoEditScreen.swift | 10 +- .../Sources/Todo/List/TodoListScreen.swift | 10 +- .../Sources/Welcome/WelcomeScreen.swift | 10 +- .../Sources/Todo/Edit/TodoEditScreen.swift | 10 +- .../Sources/Todo/List/TodoListScreen.swift | 10 +- .../Sources/Welcome/WelcomeScreen.swift | 10 +- .../Container/ContainerViewController.swift | 24 +++-- .../Sources/Screen/AnyScreen/AnyScreen.swift | 11 ++- .../UntypedScreenViewController.swift | 38 -------- swift/WorkflowUI/Sources/Screen/Screen.swift | 2 +- .../Sources/Screen/ScreenViewController.swift | 31 +++--- .../ViewEnvironment/ViewEnvironment.swift | 97 +++++++++++++++++++ .../ViewEnvironment/ViewEnvironmentKey.swift | 46 +++++++++ .../DescribedViewController.swift | 8 +- .../Tests/ContainerViewControllerTests.swift | 8 +- .../ViewControllerDescriptionTests.swift | 6 +- 39 files changed, 393 insertions(+), 212 deletions(-) create mode 100644 swift/Samples/SplitScreenContainer/Sources/Environment+SplitScreen.swift delete mode 100644 swift/WorkflowUI/Sources/Screen/AnyScreen/UntypedScreenViewController.swift create mode 100644 swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironment.swift create mode 100644 swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironmentKey.swift diff --git a/swift/Samples/BackStackContainer/Sources/BackStackContainer.swift b/swift/Samples/BackStackContainer/Sources/BackStackContainer.swift index 67f25f319..3dbec5532 100644 --- a/swift/Samples/BackStackContainer/Sources/BackStackContainer.swift +++ b/swift/Samples/BackStackContainer/Sources/BackStackContainer.swift @@ -19,10 +19,10 @@ import WorkflowUI public final class BackStackContainer: ScreenViewController, UINavigationControllerDelegate { private let navController: UINavigationController - public required init(screen: BackStackScreen) { + public required init(screen: BackStackScreen, environment: ViewEnvironment) { self.navController = UINavigationController() - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -42,7 +42,7 @@ public final class BackStackContainer: ScreenViewController, UI navController.view.frame = view.bounds } - public override func screenDidChange(from previousScreen: BackStackScreen) { + public override func screenDidChange(from previousScreen: BackStackScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } @@ -63,10 +63,10 @@ public final class BackStackContainer: ScreenViewController, UI viewController.matches(item: item) }) { let existingViewController = existingViewControllers.remove(at: idx) - existingViewController.update(item: item) + existingViewController.update(item: item, environment: environment) updatedViewControllers.append(existingViewController) } else { - updatedViewControllers.append(ScreenWrapperViewController(item: item)) + updatedViewControllers.append(ScreenWrapperViewController(item: item, environment: environment)) } } diff --git a/swift/Samples/BackStackContainer/Sources/BackStackScreen.swift b/swift/Samples/BackStackContainer/Sources/BackStackScreen.swift index 95b33b13a..0c8f670a4 100644 --- a/swift/Samples/BackStackContainer/Sources/BackStackScreen.swift +++ b/swift/Samples/BackStackContainer/Sources/BackStackScreen.swift @@ -23,8 +23,8 @@ public struct BackStackScreen: Screen { self.items = items } - public var viewControllerDescription: ViewControllerDescription { - return BackStackContainer.description(for: self) + public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return BackStackContainer.description(for: self, environment: environment) } } diff --git a/swift/Samples/BackStackContainer/Sources/ScreenWrapperViewController.swift b/swift/Samples/BackStackContainer/Sources/ScreenWrapperViewController.swift index 8eaeac4ae..ef1c707da 100644 --- a/swift/Samples/BackStackContainer/Sources/ScreenWrapperViewController.swift +++ b/swift/Samples/BackStackContainer/Sources/ScreenWrapperViewController.swift @@ -22,13 +22,15 @@ import WorkflowUI final class ScreenWrapperViewController: UIViewController { let key: AnyHashable let screenType: Any.Type + let environment: ViewEnvironment let contentViewController: DescribedViewController - init(item: BackStackScreen.Item) { + init(item: BackStackScreen.Item, environment: ViewEnvironment) { self.key = item.key self.screenType = item.screenType - self.contentViewController = DescribedViewController(screen: item.screen) + self.environment = environment + self.contentViewController = DescribedViewController(screen: item.screen, environment: environment) super.init(nibName: nil, bundle: nil) @@ -51,8 +53,8 @@ final class ScreenWrapperViewController: UIViewController { contentViewController.view.frame = view.bounds } - func update(item: BackStackScreen.Item) { - contentViewController.update(screen: item.screen) + func update(item: BackStackScreen.Item, environment: ViewEnvironment) { + contentViewController.update(screen: item.screen, environment: environment) update(barVisibility: item.barVisibility) } diff --git a/swift/Samples/SampleApp/Sources/CrossFadeContainer.swift b/swift/Samples/SampleApp/Sources/CrossFadeContainer.swift index 6accd032e..b04d0f150 100644 --- a/swift/Samples/SampleApp/Sources/CrossFadeContainer.swift +++ b/swift/Samples/SampleApp/Sources/CrossFadeContainer.swift @@ -39,8 +39,8 @@ struct CrossFadeScreen: Screen { return self.key == otherScreen.key } - var viewControllerDescription: ViewControllerDescription { - return CrossFadeContainerViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return CrossFadeContainerViewController.description(for: self, environment: environment) } } @@ -48,9 +48,9 @@ struct CrossFadeScreen: Screen { fileprivate final class CrossFadeContainerViewController: ScreenViewController { var childViewController: DescribedViewController - required init(screen: CrossFadeScreen) { - childViewController = DescribedViewController(screen: screen.baseScreen) - super.init(screen: screen) + required init(screen: CrossFadeScreen, environment: ViewEnvironment) { + childViewController = DescribedViewController(screen: screen.baseScreen, environment: environment) + super.init(screen: screen, environment: environment) } override func viewDidLoad() { @@ -67,13 +67,13 @@ fileprivate final class CrossFadeContainerViewController: ScreenViewController Void - var viewControllerDescription: ViewControllerDescription { - return DemoViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return DemoViewController.description(for: self, environment: environment) } } @@ -42,12 +42,12 @@ fileprivate final class DemoViewController: ScreenViewController { private let statusLabel: UILabel private let refreshButton: UIButton - required init(screen: DemoScreen) { + required init(screen: DemoScreen, environment: ViewEnvironment) { titleButton = UIButton(frame: .zero) subscribeButton = UIButton(frame: .zero) statusLabel = UILabel(frame: .zero) refreshButton = UIButton(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -106,7 +106,7 @@ fileprivate final class DemoViewController: ScreenViewController { height: height) } - override func screenDidChange(from previousScreen: DemoScreen) { + override func screenDidChange(from previousScreen: DemoScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/SampleApp/Sources/WelcomeScreen.swift b/swift/Samples/SampleApp/Sources/WelcomeScreen.swift index 933de898b..310a017ca 100644 --- a/swift/Samples/SampleApp/Sources/WelcomeScreen.swift +++ b/swift/Samples/SampleApp/Sources/WelcomeScreen.swift @@ -22,8 +22,8 @@ struct WelcomeScreen: Screen { var onNameChanged: (String) -> Void var onLoginTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return WelcomeViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return WelcomeViewController.description(for: self, environment: environment) } } @@ -33,11 +33,11 @@ fileprivate final class WelcomeViewController: ScreenViewController Void - var viewControllerDescription: ViewControllerDescription { - return BarScreenViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return BarScreenViewController.description(for: self, environment: environment) } } @@ -34,8 +34,8 @@ fileprivate final class BarScreenViewController: ScreenViewController private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init() private var gradientLayer: CAGradientLayer? - required init(screen: BarScreen) { - super.init(screen: screen) + required init(screen: BarScreen, environment: ViewEnvironment) { + super.init(screen: screen, environment: environment) update(with: screen) } @@ -58,8 +58,8 @@ fileprivate final class BarScreenViewController: ScreenViewController updateGradient(for: view, colors: screen.backgroundColors) } - - override func screenDidChange(from previousScreen: BarScreen) { + + override func screenDidChange(from previousScreen: BarScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/SplitScreenContainer/DemoApp/FooScreen.swift b/swift/Samples/SplitScreenContainer/DemoApp/FooScreen.swift index 370378f1d..3d2e62152 100644 --- a/swift/Samples/SplitScreenContainer/DemoApp/FooScreen.swift +++ b/swift/Samples/SplitScreenContainer/DemoApp/FooScreen.swift @@ -22,8 +22,8 @@ struct FooScreen: Screen { let backgroundColor: UIColor let viewTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return FooScreenViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return FooScreenViewController.description(for: self, environment: environment) } } @@ -33,8 +33,8 @@ fileprivate final class FooScreenViewController: ScreenViewController private lazy var titleLabel: UILabel = .init() private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init() - required init(screen: FooScreen) { - super.init(screen: screen) + required init(screen: FooScreen, environment: ViewEnvironment) { + super.init(screen: screen, environment: environment) update(with: screen) } @@ -54,8 +54,8 @@ fileprivate final class FooScreenViewController: ScreenViewController titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) } - - override func screenDidChange(from previousScreen: FooScreen) { + + override func screenDidChange(from previousScreen: FooScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/SplitScreenContainer/SnapshotTests/SplitScreenContainerScreenSnapshotTests.swift b/swift/Samples/SplitScreenContainer/SnapshotTests/SplitScreenContainerScreenSnapshotTests.swift index 61d24aa97..53d0cabe7 100644 --- a/swift/Samples/SplitScreenContainer/SnapshotTests/SplitScreenContainerScreenSnapshotTests.swift +++ b/swift/Samples/SplitScreenContainer/SnapshotTests/SplitScreenContainerScreenSnapshotTests.swift @@ -29,7 +29,8 @@ class SplitScreenContainerScreenSnapshotTests: FBSnapshotTestCase { ) let viewController = SplitScreenContainerViewController( - screen: splitScreenContainerScreen + screen: splitScreenContainerScreen, + environment: .empty ) viewController.view.layoutIfNeeded() @@ -44,8 +45,8 @@ fileprivate struct FooScreen: Screen { let backgroundColor: UIColor let viewTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return FooScreenViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return FooScreenViewController.description(for: self, environment: environment) } } @@ -55,8 +56,8 @@ fileprivate final class FooScreenViewController: ScreenViewController private lazy var titleLabel: UILabel = .init() private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init() - required init(screen: FooScreen) { - super.init(screen: screen) + required init(screen: FooScreen, environment: ViewEnvironment) { + super.init(screen: screen, environment: environment) update(with: screen) } @@ -76,8 +77,8 @@ fileprivate final class FooScreenViewController: ScreenViewController titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) } - - override func screenDidChange(from previousScreen: FooScreen) { + + override func screenDidChange(from previousScreen: FooScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/SplitScreenContainer/Sources/Environment+SplitScreen.swift b/swift/Samples/SplitScreenContainer/Sources/Environment+SplitScreen.swift new file mode 100644 index 000000000..d489b840a --- /dev/null +++ b/swift/Samples/SplitScreenContainer/Sources/Environment+SplitScreen.swift @@ -0,0 +1,41 @@ +/* +* Copyright 2020 Square Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +import WorkflowUI + +public enum SplitScreenPosition { + /// Not appearing in a split screen context + case none + + /// Appearing in the leading position in a split screen + case leading + + /// Appearing in the trailing position in a split screen + case trailing +} + +extension ViewEnvironment { + + internal(set) public var splitScreenPosition: SplitScreenPosition { + get { return self[SplitScreenPositionKey.self] } + set { self[SplitScreenPositionKey.self] = newValue } + } + +} + +private enum SplitScreenPositionKey: ViewEnvironmentKey { + static var defaultValue: SplitScreenPosition = .none +} diff --git a/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerScreen.swift b/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerScreen.swift index 511f86d14..5f5b765e6 100644 --- a/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerScreen.swift +++ b/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerScreen.swift @@ -48,8 +48,8 @@ public struct SplitScreenContainerScreen ViewControllerDescription { + return SplitScreenContainerViewController.description(for: self, environment: environment) } } diff --git a/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerViewController.swift b/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerViewController.swift index d3798700e..725f98e25 100644 --- a/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerViewController.swift +++ b/swift/Samples/SplitScreenContainer/Sources/SplitScreenContainerViewController.swift @@ -29,13 +29,21 @@ internal final class SplitScreenContainerViewController ViewControllerDescription { + return LoadingScreenViewController.description(for: self, environment: environment) } } @@ -26,10 +26,10 @@ struct LoadingScreen: Screen { fileprivate final class LoadingScreenViewController: ScreenViewController { let loadingLabel: UILabel - required init(screen: LoadingScreen) { + required init(screen: LoadingScreen, environment: ViewEnvironment) { self.loadingLabel = UILabel(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) } override func viewDidLoad() { diff --git a/swift/Samples/TicTacToe/Sources/Authentication/LoginScreen.swift b/swift/Samples/TicTacToe/Sources/Authentication/LoginScreen.swift index 62cddbfd4..07c0a9cb0 100644 --- a/swift/Samples/TicTacToe/Sources/Authentication/LoginScreen.swift +++ b/swift/Samples/TicTacToe/Sources/Authentication/LoginScreen.swift @@ -25,7 +25,7 @@ struct LoginScreen: Screen { var onPasswordChanged: (String) -> Void var onLoginTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { return ViewControllerDescription( build: { LoginViewController() }, update: { $0.update(with: self) }) diff --git a/swift/Samples/TicTacToe/Sources/Authentication/TwoFactorScreen.swift b/swift/Samples/TicTacToe/Sources/Authentication/TwoFactorScreen.swift index 1156dc383..f09b9724a 100644 --- a/swift/Samples/TicTacToe/Sources/Authentication/TwoFactorScreen.swift +++ b/swift/Samples/TicTacToe/Sources/Authentication/TwoFactorScreen.swift @@ -20,8 +20,8 @@ struct TwoFactorScreen: Screen { var title: String var onLoginTapped: (String) -> Void - var viewControllerDescription: ViewControllerDescription { - return TwoFactorViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TwoFactorViewController.description(for: self, environment: environment) } } @@ -31,12 +31,12 @@ fileprivate final class TwoFactorViewController: ScreenViewController Void - var viewControllerDescription: ViewControllerDescription { - return GamePlayViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return GamePlayViewController.description(for: self, environment: environment) } } @@ -33,7 +33,7 @@ final class GamePlayViewController: ScreenViewController { let titleLabel: UILabel let cells: [[UIButton]] - required init(screen: GamePlayScreen) { + required init(screen: GamePlayScreen, environment: ViewEnvironment) { self.titleLabel = UILabel(frame: .zero) var cells: [[UIButton]] = [] @@ -46,7 +46,7 @@ final class GamePlayViewController: ScreenViewController { } self.cells = cells - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -106,7 +106,7 @@ final class GamePlayViewController: ScreenViewController { } } - override func screenDidChange(from previousScreen: GamePlayScreen) { + override func screenDidChange(from previousScreen: GamePlayScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/TicTacToe/Sources/Game/NewGameScreen.swift b/swift/Samples/TicTacToe/Sources/Game/NewGameScreen.swift index 0533dd6cf..3da552b94 100644 --- a/swift/Samples/TicTacToe/Sources/Game/NewGameScreen.swift +++ b/swift/Samples/TicTacToe/Sources/Game/NewGameScreen.swift @@ -27,8 +27,8 @@ struct NewGameScreen: Screen { case startGame } - var viewControllerDescription: ViewControllerDescription { - return NewGameViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return NewGameViewController.description(for: self, environment: environment) } } @@ -40,14 +40,14 @@ final class NewGameViewController: ScreenViewController { let playerOField: UITextField let startGameButton: UIButton - required init(screen: NewGameScreen) { + required init(screen: NewGameScreen, environment: ViewEnvironment) { self.playerXLabel = UILabel(frame: .zero) self.playerXField = UITextField(frame: .zero) self.playerOLabel = UILabel(frame: .zero) self.playerOField = UITextField(frame: .zero) self.startGameButton = UIButton(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -129,7 +129,7 @@ final class NewGameViewController: ScreenViewController { } - override func screenDidChange(from previousScreen: NewGameScreen) { + override func screenDidChange(from previousScreen: NewGameScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeScreen.swift index faf89c3d1..a57f71373 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial1Complete/Sources/Welcome/WelcomeScreen.swift @@ -26,8 +26,8 @@ struct WelcomeScreen: Screen { /// Callback when the login button is tapped. var onLoginTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return WelcomeViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return WelcomeViewController.description(for: self, environment: environment) } } @@ -36,9 +36,9 @@ final class WelcomeViewController: ScreenViewController { var welcomeView: WelcomeView - required init(screen: WelcomeScreen) { + required init(screen: WelcomeScreen, environment: ViewEnvironment) { self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -54,7 +54,7 @@ final class WelcomeViewController: ScreenViewController { welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: WelcomeScreen) { + override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListScreen.swift index a9ec59408..fc519745f 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Todo/List/TodoListScreen.swift @@ -25,8 +25,8 @@ struct TodoListScreen: Screen { // Callback when a todo is selected var onTodoSelected: (Int) -> Void - var viewControllerDescription: ViewControllerDescription { - return TodoListViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TodoListViewController.description(for: self, environment: environment) } } @@ -34,9 +34,9 @@ struct TodoListScreen: Screen { final class TodoListViewController: ScreenViewController { let todoListView: TodoListView - required init(screen: TodoListScreen) { + required init(screen: TodoListScreen, environment: ViewEnvironment) { self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -52,7 +52,7 @@ final class TodoListViewController: ScreenViewController { todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: TodoListScreen) { + override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeScreen.swift index faf89c3d1..a57f71373 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial2Complete/Sources/Welcome/WelcomeScreen.swift @@ -26,8 +26,8 @@ struct WelcomeScreen: Screen { /// Callback when the login button is tapped. var onLoginTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return WelcomeViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return WelcomeViewController.description(for: self, environment: environment) } } @@ -36,9 +36,9 @@ final class WelcomeViewController: ScreenViewController { var welcomeView: WelcomeView - required init(screen: WelcomeScreen) { + required init(screen: WelcomeScreen, environment: ViewEnvironment) { self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -54,7 +54,7 @@ final class WelcomeViewController: ScreenViewController { welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: WelcomeScreen) { + override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditScreen.swift index c36d013c2..7d5ab6cef 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/Edit/TodoEditScreen.swift @@ -28,8 +28,8 @@ struct TodoEditScreen: Screen { var onTitleChanged: (String) -> Void var onNoteChanged: (String) -> Void - var viewControllerDescription: ViewControllerDescription { - return TodoEditViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TodoEditViewController.description(for: self, environment: environment) } } @@ -38,10 +38,10 @@ final class TodoEditViewController: ScreenViewController { // The `todoEditView` has all the logic for displaying the todo and editing. let todoEditView: TodoEditView - required init(screen: TodoEditScreen) { + required init(screen: TodoEditScreen, environment: ViewEnvironment) { self.todoEditView = TodoEditView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -57,7 +57,7 @@ final class TodoEditViewController: ScreenViewController { todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: TodoEditScreen) { + override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListScreen.swift index a9ec59408..fc519745f 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Todo/List/TodoListScreen.swift @@ -25,8 +25,8 @@ struct TodoListScreen: Screen { // Callback when a todo is selected var onTodoSelected: (Int) -> Void - var viewControllerDescription: ViewControllerDescription { - return TodoListViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TodoListViewController.description(for: self, environment: environment) } } @@ -34,9 +34,9 @@ struct TodoListScreen: Screen { final class TodoListViewController: ScreenViewController { let todoListView: TodoListView - required init(screen: TodoListScreen) { + required init(screen: TodoListScreen, environment: ViewEnvironment) { self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -52,7 +52,7 @@ final class TodoListViewController: ScreenViewController { todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: TodoListScreen) { + override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeScreen.swift index faf89c3d1..a57f71373 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial3Complete/Sources/Welcome/WelcomeScreen.swift @@ -26,8 +26,8 @@ struct WelcomeScreen: Screen { /// Callback when the login button is tapped. var onLoginTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return WelcomeViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return WelcomeViewController.description(for: self, environment: environment) } } @@ -36,9 +36,9 @@ final class WelcomeViewController: ScreenViewController { var welcomeView: WelcomeView - required init(screen: WelcomeScreen) { + required init(screen: WelcomeScreen, environment: ViewEnvironment) { self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -54,7 +54,7 @@ final class WelcomeViewController: ScreenViewController { welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: WelcomeScreen) { + override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditScreen.swift index c36d013c2..7d5ab6cef 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/Edit/TodoEditScreen.swift @@ -28,8 +28,8 @@ struct TodoEditScreen: Screen { var onTitleChanged: (String) -> Void var onNoteChanged: (String) -> Void - var viewControllerDescription: ViewControllerDescription { - return TodoEditViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TodoEditViewController.description(for: self, environment: environment) } } @@ -38,10 +38,10 @@ final class TodoEditViewController: ScreenViewController { // The `todoEditView` has all the logic for displaying the todo and editing. let todoEditView: TodoEditView - required init(screen: TodoEditScreen) { + required init(screen: TodoEditScreen, environment: ViewEnvironment) { self.todoEditView = TodoEditView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -57,7 +57,7 @@ final class TodoEditViewController: ScreenViewController { todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: TodoEditScreen) { + override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListScreen.swift index a9ec59408..fc519745f 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Todo/List/TodoListScreen.swift @@ -25,8 +25,8 @@ struct TodoListScreen: Screen { // Callback when a todo is selected var onTodoSelected: (Int) -> Void - var viewControllerDescription: ViewControllerDescription { - return TodoListViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TodoListViewController.description(for: self, environment: environment) } } @@ -34,9 +34,9 @@ struct TodoListScreen: Screen { final class TodoListViewController: ScreenViewController { let todoListView: TodoListView - required init(screen: TodoListScreen) { + required init(screen: TodoListScreen, environment: ViewEnvironment) { self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -52,7 +52,7 @@ final class TodoListViewController: ScreenViewController { todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: TodoListScreen) { + override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeScreen.swift index faf89c3d1..a57f71373 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial4Complete/Sources/Welcome/WelcomeScreen.swift @@ -26,8 +26,8 @@ struct WelcomeScreen: Screen { /// Callback when the login button is tapped. var onLoginTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return WelcomeViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return WelcomeViewController.description(for: self, environment: environment) } } @@ -36,9 +36,9 @@ final class WelcomeViewController: ScreenViewController { var welcomeView: WelcomeView - required init(screen: WelcomeScreen) { + required init(screen: WelcomeScreen, environment: ViewEnvironment) { self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -54,7 +54,7 @@ final class WelcomeViewController: ScreenViewController { welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: WelcomeScreen) { + override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift index c36d013c2..7d5ab6cef 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/Edit/TodoEditScreen.swift @@ -28,8 +28,8 @@ struct TodoEditScreen: Screen { var onTitleChanged: (String) -> Void var onNoteChanged: (String) -> Void - var viewControllerDescription: ViewControllerDescription { - return TodoEditViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TodoEditViewController.description(for: self, environment: environment) } } @@ -38,10 +38,10 @@ final class TodoEditViewController: ScreenViewController { // The `todoEditView` has all the logic for displaying the todo and editing. let todoEditView: TodoEditView - required init(screen: TodoEditScreen) { + required init(screen: TodoEditScreen, environment: ViewEnvironment) { self.todoEditView = TodoEditView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -57,7 +57,7 @@ final class TodoEditViewController: ScreenViewController { todoEditView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: TodoEditScreen) { + override func screenDidChange(from previousScreen: TodoEditScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift index a9ec59408..fc519745f 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Todo/List/TodoListScreen.swift @@ -25,8 +25,8 @@ struct TodoListScreen: Screen { // Callback when a todo is selected var onTodoSelected: (Int) -> Void - var viewControllerDescription: ViewControllerDescription { - return TodoListViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TodoListViewController.description(for: self, environment: environment) } } @@ -34,9 +34,9 @@ struct TodoListScreen: Screen { final class TodoListViewController: ScreenViewController { let todoListView: TodoListView - required init(screen: TodoListScreen) { + required init(screen: TodoListScreen, environment: ViewEnvironment) { self.todoListView = TodoListView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -52,7 +52,7 @@ final class TodoListViewController: ScreenViewController { todoListView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: TodoListScreen) { + override func screenDidChange(from previousScreen: TodoListScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift index faf89c3d1..a57f71373 100644 --- a/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift +++ b/swift/Samples/Tutorial/Frameworks/Tutorial5Complete/Sources/Welcome/WelcomeScreen.swift @@ -26,8 +26,8 @@ struct WelcomeScreen: Screen { /// Callback when the login button is tapped. var onLoginTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return WelcomeViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return WelcomeViewController.description(for: self, environment: environment) } } @@ -36,9 +36,9 @@ final class WelcomeViewController: ScreenViewController { var welcomeView: WelcomeView - required init(screen: WelcomeScreen) { + required init(screen: WelcomeScreen, environment: ViewEnvironment) { self.welcomeView = WelcomeView(frame: .zero) - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(with: screen) } @@ -54,7 +54,7 @@ final class WelcomeViewController: ScreenViewController { welcomeView.frame = view.bounds.inset(by: view.safeAreaInsets) } - override func screenDidChange(from previousScreen: WelcomeScreen) { + override func screenDidChange(from previousScreen: WelcomeScreen, previousEnvironment: ViewEnvironment) { update(with: screen) } diff --git a/swift/WorkflowUI/Sources/Container/ContainerViewController.swift b/swift/WorkflowUI/Sources/Container/ContainerViewController.swift index 90e9d9e5c..73cd7d826 100644 --- a/swift/WorkflowUI/Sources/Container/ContainerViewController.swift +++ b/swift/WorkflowUI/Sources/Container/ContainerViewController.swift @@ -35,11 +35,19 @@ public final class ContainerViewController: UIViewController private let (lifetime, token) = Lifetime.make() - private init(workflowHost: Any, rendering: Property, output: Signal) { + public var rootViewEnvironment: ViewEnvironment { + didSet { + // Re-render the current rendering with the new environment + render(screen: rendering.value, environment: rootViewEnvironment) + } + } + + private init(workflowHost: Any, rendering: Property, output: Signal, rootViewEnvironment: ViewEnvironment) { self.workflowHost = workflowHost - self.rootViewController = DescribedViewController(screen: rendering.value) + self.rootViewController = DescribedViewController(screen: rendering.value, environment: rootViewEnvironment) self.rendering = rendering self.output = output + self.rootViewEnvironment = rootViewEnvironment super.init(nibName: nil, bundle: nil) @@ -47,24 +55,26 @@ public final class ContainerViewController: UIViewController .signal .take(during: lifetime) .observeValues { [weak self] screen in - self?.render(screen: screen) + guard let self = self else { return } + self.render(screen: screen, environment: self.rootViewEnvironment) } } - public convenience init(workflow: W) where W.Rendering == ScreenType, W.Output == Output { + public convenience init(workflow: W, rootViewEnvironment: ViewEnvironment = .empty) where W.Rendering == ScreenType, W.Output == Output { let host = WorkflowHost(workflow: workflow) self.init( workflowHost: host, rendering: host.rendering, - output: host.output) + output: host.output, + rootViewEnvironment: rootViewEnvironment) } required public init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } - private func render(screen: ScreenType) { - rootViewController.update(screen: screen) + private func render(screen: ScreenType, environment: ViewEnvironment) { + rootViewController.update(screen: screen, environment: environment) } override public func viewDidLoad() { diff --git a/swift/WorkflowUI/Sources/Screen/AnyScreen/AnyScreen.swift b/swift/WorkflowUI/Sources/Screen/AnyScreen/AnyScreen.swift index 3ebb99957..926a992f6 100644 --- a/swift/WorkflowUI/Sources/Screen/AnyScreen/AnyScreen.swift +++ b/swift/WorkflowUI/Sources/Screen/AnyScreen/AnyScreen.swift @@ -23,8 +23,8 @@ public struct AnyScreen: Screen { /// The original screen, retained for debugging internal let wrappedScreen: Screen - /// The wrapped screen’s view controller description is passed straight through - public let viewControllerDescription: ViewControllerDescription + /// Stored getter for the wrapped screen’s view controller description + private let _viewControllerDescription: (ViewEnvironment) -> ViewControllerDescription public init(_ screen: T) { if let anyScreen = screen as? AnyScreen { @@ -32,7 +32,12 @@ public struct AnyScreen: Screen { return } self.wrappedScreen = screen - self.viewControllerDescription = screen.viewControllerDescription + self._viewControllerDescription = screen.viewControllerDescription(environment:) + } + + public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + // Passed straight through + return _viewControllerDescription(environment) } } diff --git a/swift/WorkflowUI/Sources/Screen/AnyScreen/UntypedScreenViewController.swift b/swift/WorkflowUI/Sources/Screen/AnyScreen/UntypedScreenViewController.swift deleted file mode 100644 index 8fe20a403..000000000 --- a/swift/WorkflowUI/Sources/Screen/AnyScreen/UntypedScreenViewController.swift +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2019 Square Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#if canImport(UIKit) - -// Internal API for working with a screen view controller when the specific screen type is unknown. -protocol UntypedScreenViewController { - var screenType: Screen.Type { get } - func update(untypedScreen: Screen) -} - -extension ScreenViewController: UntypedScreenViewController { - - // `var screenType: Screen.Type` is already present in ScreenViewController - - func update(untypedScreen: Screen) { - guard let typedScreen = untypedScreen as? ScreenType else { - fatalError("Screen type mismatch: \(self) expected to receive a screen of type \(ScreenType.self), but instead received a screen of type \(type(of: screen))") - } - update(screen: typedScreen) - } - -} - -#endif diff --git a/swift/WorkflowUI/Sources/Screen/Screen.swift b/swift/WorkflowUI/Sources/Screen/Screen.swift index 0cffcdb6c..d62ecfc87 100644 --- a/swift/WorkflowUI/Sources/Screen/Screen.swift +++ b/swift/WorkflowUI/Sources/Screen/Screen.swift @@ -24,7 +24,7 @@ public protocol Screen { /// A view controller description that acts as a recipe to either build /// or update a previously-built view controller to match this screen. - var viewControllerDescription: ViewControllerDescription { get } + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription } diff --git a/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift b/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift index b2500d66a..9d565a90c 100644 --- a/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift +++ b/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift @@ -38,18 +38,17 @@ import UIKit /// ``` open class ScreenViewController: UIViewController { - private var currentScreen: ScreenType - - public final var screen: ScreenType { - return currentScreen - } + private(set) public final var screen: ScreenType public final var screenType: Screen.Type { return ScreenType.self } - public required init(screen: ScreenType) { - self.currentScreen = screen + private(set) public final var environment: ViewEnvironment + + public required init(screen: ScreenType, environment: ViewEnvironment) { + self.screen = screen + self.environment = environment super.init(nibName: nil, bundle: nil) } @@ -58,14 +57,16 @@ open class ScreenViewController: UIViewController { fatalError("init(coder:) has not been implemented") } - public final func update(screen: ScreenType) { - let previousScreen = currentScreen - currentScreen = screen - screenDidChange(from: previousScreen) + public final func update(screen: ScreenType, environment: ViewEnvironment) { + let previousScreen = self.screen + self.screen = screen + let previousEnvironment = self.environment + self.environment = environment + screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) } /// Subclasses should override this method in order to update any relevant UI bits when the screen model changes. - open func screenDidChange(from previousScreen: ScreenType) { + open func screenDidChange(from previousScreen: ScreenType, previousEnvironment: ViewEnvironment) { } @@ -76,11 +77,11 @@ extension ScreenViewController { /// Convenience to create a view controller description for the given screen /// value. See the example on the comment for ScreenViewController for /// usage. - public final class func description(for screen: ScreenType) -> ViewControllerDescription { + public final class func description(for screen: ScreenType, environment: ViewEnvironment) -> ViewControllerDescription { return ViewControllerDescription( type: self, - build: { self.init(screen: screen) }, - update: { $0.update(screen: screen) }) + build: { self.init(screen: screen, environment: environment) }, + update: { $0.update(screen: screen, environment: environment) }) } } diff --git a/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironment.swift b/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironment.swift new file mode 100644 index 000000000..74faa9c28 --- /dev/null +++ b/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironment.swift @@ -0,0 +1,97 @@ +/* +* Copyright 2012 Square Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +/// ViewEnvironment acts as a container for values to flow down the view-side +/// of a rendering tree (as opposed to being passed down through Workflows). +/// +/// This will often be used by containers to let their children know in what +/// context they’re appearing (for example, a split screen container may set +/// the environment of its two children according to which position they’re +/// appearing in). +public struct ViewEnvironment { + + /// An empty view environment. This should only be used when setting up a + /// root workflow into a root ContainerViewController or when writing tests. + /// In other scenarios, containers should pass down the ViewEnvironment + /// value they get from above. + public static let empty: ViewEnvironment = ViewEnvironment() + + /// Storage of [K.Type: K.Value] where K: ViewEnvironmentKey + private var storage: [ObjectIdentifier: Any] + + /// Private empty initializer to make the `empty` environment explicit. + private init() { + storage = [:] + } + + /// Get or set for the given ViewEnvironmentKey. + /// + /// This will typically only be used by the module that provides the + /// environment value. See documentation for ViewEnvironmentKey for a + /// usage example. + public subscript(key: Key.Type) -> Key.Value where Key: ViewEnvironmentKey { + + get { + if let value = storage[ObjectIdentifier(key)] as? Key.Value { + return value + } else { + return Key.defaultValue + } + } + + set { + storage[ObjectIdentifier(key)] = newValue + } + + } + + /// Returns a new ViewEnvironment with the given value set for the given + /// environment key. + /// + /// This is provided as a convenience for modifying the environment while + /// passing it down to children screens without the need for an intermediate + /// mutable value. It is functionally equivalent to the subscript setter. + public func setting(key: Key.Type, to value: Key.Value) -> ViewEnvironment where Key: ViewEnvironmentKey { + var newEnvironment = self + newEnvironment[key] = value + return newEnvironment + } + + /// Returns a new ViewEnvironment with the given value set for the given + /// key path. + /// + /// This is provided as a convenience for modifying the environment while + /// passing it down to children screens. + /// + /// The following are functionally equivalent: + /// ``` + /// var newEnvironment = environment + /// newEnvironment.someProperty = 42 + /// ``` + /// and + /// ``` + /// let newEnvironment = environment.setting(\.someProperty, to: 42) + /// ``` + /// + /// + public func setting(keyPath: WritableKeyPath, to value: Value) -> ViewEnvironment { + var newEnvironment = self + newEnvironment[keyPath: keyPath] = value + return newEnvironment + } + +} diff --git a/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironmentKey.swift b/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironmentKey.swift new file mode 100644 index 000000000..9917189a4 --- /dev/null +++ b/swift/WorkflowUI/Sources/Screen/ViewEnvironment/ViewEnvironmentKey.swift @@ -0,0 +1,46 @@ +/* +* Copyright 2012 Square Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +/// A key into the ViewEnvironment. +/// +/// Environment keys are associated with a specific type of value (`Value`) and +/// must declare a default value. +/// +/// Typically the key conforming to `ViewEnvironmentKey` will be private, and +/// you are encouraged to provide a convenience accessor on `ViewEnvironment` +/// as in the following example: +/// +/// ``` +/// private enum ThemeKey: ViewEnvironmentKey { +/// typealias Value = Theme +/// var defaultValue: Theme +/// } +/// +/// extension ViewEnvironment { +/// public var theme: Theme { +/// get { self[ThemeKey.self] } +/// set { self[ThemeKey.self] = newValue } +/// } +/// } +/// ``` +public protocol ViewEnvironmentKey { + + associatedtype Value + + static var defaultValue: Value { get } + +} diff --git a/swift/WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift b/swift/WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift index b60016786..6224475ed 100644 --- a/swift/WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift +++ b/swift/WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift @@ -28,8 +28,8 @@ public final class DescribedViewController: UIViewController { super.init(nibName: nil, bundle: nil) } - public convenience init(screen: S) { - self.init(description: screen.viewControllerDescription) + public convenience init(screen: S, environment: ViewEnvironment) { + self.init(description: screen.viewControllerDescription(environment: environment)) } @available(*, unavailable) @@ -56,8 +56,8 @@ public final class DescribedViewController: UIViewController { } } - public func update(screen: S) { - update(description: screen.viewControllerDescription) + public func update(screen: S, environment: ViewEnvironment) { + update(description: screen.viewControllerDescription(environment: environment)) } public override func viewDidLoad() { diff --git a/swift/WorkflowUI/Tests/ContainerViewControllerTests.swift b/swift/WorkflowUI/Tests/ContainerViewControllerTests.swift index 07b7bdaca..af7cb373e 100644 --- a/swift/WorkflowUI/Tests/ContainerViewControllerTests.swift +++ b/swift/WorkflowUI/Tests/ContainerViewControllerTests.swift @@ -26,16 +26,16 @@ import Workflow fileprivate struct TestScreen: Screen { var string: String - var viewControllerDescription: ViewControllerDescription { - return TestScreenViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return TestScreenViewController.description(for: self, environment: environment) } } fileprivate final class TestScreenViewController: ScreenViewController { var onScreenChange: (() -> Void)? = nil - override func screenDidChange(from previousScreen: TestScreen) { - super.screenDidChange(from: previousScreen) + override func screenDidChange(from previousScreen: TestScreen, previousEnvironment: ViewEnvironment) { + super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) onScreenChange?() } } diff --git a/swift/WorkflowUI/Tests/ViewControllerDescriptionTests.swift b/swift/WorkflowUI/Tests/ViewControllerDescriptionTests.swift index b1d19ba00..7147daacc 100644 --- a/swift/WorkflowUI/Tests/ViewControllerDescriptionTests.swift +++ b/swift/WorkflowUI/Tests/ViewControllerDescriptionTests.swift @@ -90,8 +90,8 @@ class ViewControllerDescriptionTests: XCTestCase { // description struct MyScreen: Screen { - var viewControllerDescription: ViewControllerDescription { - return MyScreenViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return MyScreenViewController.description(for: self, environment: environment) } } @@ -100,7 +100,7 @@ class ViewControllerDescriptionTests: XCTestCase { let screen = MyScreen() - let description = screen.viewControllerDescription + let description = screen.viewControllerDescription(environment: .empty) let viewController = description.buildViewController() XCTAssertTrue(type(of: viewController) == MyScreenViewController.self) From 6a46f0e7245b692758aec579853cc221bebde5cc Mon Sep 17 00:00:00 2001 From: Ben Cochran Date: Wed, 4 Mar 2020 15:15:00 -0800 Subject: [PATCH 2/2] Update docs and template for ViewEnvironment --- .../building-a-view-controller-from-screen.md | 16 ++++++++++------ swift/Samples/Tutorial/Tutorial1.md | 4 ++-- swift/Samples/Tutorial/Tutorial3.md | 4 ++-- .../___FILEBASENAME___Screen.swift | 16 ++++++++-------- .../Sources/Screen/ScreenViewController.swift | 4 ++-- 5 files changed, 24 insertions(+), 20 deletions(-) diff --git a/docs/tutorial/building-a-view-controller-from-screen.md b/docs/tutorial/building-a-view-controller-from-screen.md index 766a5b601..a3d35258e 100644 --- a/docs/tutorial/building-a-view-controller-from-screen.md +++ b/docs/tutorial/building-a-view-controller-from-screen.md @@ -11,6 +11,10 @@ controller from a view model update. struct DemoScreen: Screen { let title: String let onTap: () -> Void + + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return DemoScreenViewController.description(for: self, environment: environment) + } } @@ -18,9 +22,9 @@ class DemoScreenViewController: ScreenViewController { private let button: UIButton - required init(screen: DemoScreen) { + required init(screen: DemoScreen, environment: ViewEnvironment) { button = UIButton() - super.init(screen: screen) + super.init(screen: screen, environment: environment) update(screen: screen) } @@ -39,8 +43,8 @@ class DemoScreenViewController: ScreenViewController { button.frame = view.bounds } - override func screenDidChange(from previousScreen: DemoScreen) { - super.screenDidChange(from: previousScreen) + override func screenDidChange(from previousScreen: DemoScreen, previousEnvironment: ViewEnvironment) { + super.screenDidChange(from: previousScreen, previousEnvironment: previousEnvironment) update(screen: screen) } @@ -64,5 +68,5 @@ class DemoScreenViewController: ScreenViewController { 1. The button is tapped. When the callback is called, we call the `onTap` closure passed into the screen. The workflow will handle this event, update its state, and a new screen will be rendered. 1. The updated screen is passed to the view controller via the - `screenDidChange(from previousScreen:)` method. Again, the view controller updates the title of - the button based on what was passed in the screen. + `screenDidChange(from previousScreen: previousEnvironment: previousEnvironment:)` method. Again, + the view controller updates the title of the button based on what was passed in the screen. diff --git a/swift/Samples/Tutorial/Tutorial1.md b/swift/Samples/Tutorial/Tutorial1.md index b88cb75a8..b5a32284a 100644 --- a/swift/Samples/Tutorial/Tutorial1.md +++ b/swift/Samples/Tutorial/Tutorial1.md @@ -44,8 +44,8 @@ struct WelcomeScreen: Screen { /// Callback when the login button is tapped. var onLoginTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return WelcomeViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return WelcomeViewController.description(for: self, environment: environment) } } ``` diff --git a/swift/Samples/Tutorial/Tutorial3.md b/swift/Samples/Tutorial/Tutorial3.md index 8d0207113..33f5284a9 100644 --- a/swift/Samples/Tutorial/Tutorial3.md +++ b/swift/Samples/Tutorial/Tutorial3.md @@ -89,8 +89,8 @@ The `Screen` protocol also requires a `viewControllerDescription` property. This ```swift extension TodoEditScreen { - var viewControllerDescription: ViewControllerDescription { - TodoEditViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + TodoEditViewController.description(for: self, environment: environment) } } ``` diff --git a/swift/Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift b/swift/Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift index 51be7c9b0..6ff8a4182 100755 --- a/swift/Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift +++ b/swift/Tooling/Templates/Screen (View Controller).xctemplate/___FILEBASENAME___Screen.swift @@ -10,24 +10,24 @@ struct ___VARIABLE_productName___Screen: Screen { // It should also contain callbacks for any UI events, for example: // var onButtonTapped: () -> Void - var viewControllerDescription: ViewControllerDescription { - return ___VARIABLE_productName___ViewController.description(for: self) + func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { + return ___VARIABLE_productName___ViewController.description(for: self, environment: environment) } } final class ___VARIABLE_productName___ViewController: ScreenViewController<___VARIABLE_productName___Screen> { - required init(screen: ___VARIABLE_productName___Screen) { - super.init(screen: screen) - update(with: screen) + required init(screen: ___VARIABLE_productName___Screen, environment: ViewEnvironment) { + super.init(screen: screen, environment: environment) + update(with: screen, environment: environment) } - override func screenDidChange(from previousScreen: ___VARIABLE_productName___Screen) { - update(with: screen) + override func screenDidChange(from previousScreen: ___VARIABLE_productName___Screen, previousEnvironment: ViewEnvironment) { + update(with: screen, environment: environment) } - private func update(with screen: ___VARIABLE_productName___Screen) { + private func update(with screen: ___VARIABLE_productName___Screen, environment: ViewEnvironment) { /// Update UI } diff --git a/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift b/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift index 9d565a90c..356753bef 100644 --- a/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift +++ b/swift/WorkflowUI/Sources/Screen/ScreenViewController.swift @@ -25,13 +25,13 @@ import UIKit /// Using this base class, a screen can be implemented as: /// ``` /// struct MyScreen: Screen { -/// var viewControllerDescription: ViewControllerDescription { +/// func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription { /// return MyScreenViewController.description(for: self) /// } /// } /// /// private class MyScreenViewController: ScreenViewController { -/// override func screenDidChange(from previousScreen: MyScreen) { +/// override func screenDidChange(from previousScreen: MyScreen, previousEnvironment: ViewEnvironment) { /// // … update views as necessary /// } /// }