message: Animate ratio changes.

diff:
@@ -20,6 +20,7 @@ import WorkflowUI
struct BaseScreen: Screen {
let title: String
let backgroundColor: UIColor
+ let viewTapped: () -> Void
}
@@ -35,6 +36,7 @@ extension ViewRegistry {
fileprivate final class BaseScreenViewController: ScreenViewController<BaseScreen> {
private lazy var titleLabel: UILabel = .init()
+ private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init()
required init(screen: BaseScreen, viewRegistry: ViewRegistry) {
super.init(screen: screen, viewRegistry: viewRegistry)
@@ -45,6 +47,9 @@ fileprivate final class BaseScreenViewController: ScreenViewController<BaseScree
override func viewDidLoad() {
super.viewDidLoad()
+ tapGestureRecognizer.addTarget(self, action: #selector(viewTapped))
+ view.addGestureRecognizer(tapGestureRecognizer)
+
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.textAlignment = .center
view.addSubview(titleLabel)
@@ -63,5 +68,10 @@ fileprivate final class BaseScreenViewController: ScreenViewController<BaseScree
view.backgroundColor = screen.backgroundColor
titleLabel.text = screen.title
}
+
+ @objc
+ private func viewTapped() {
+ screen.viewTapped()
+ }
}

diff:
@@ -31,10 +31,10 @@ struct DemoWorkflow: Workflow {
extension DemoWorkflow {
- typealias State = Void
+ typealias State = Int
func makeInitialState() -> State {
- return ()
+ return 1
}
func workflowDidChange(from previousWorkflow: DemoWorkflow, state: inout State) {
@@ -42,6 +42,25 @@ extension DemoWorkflow {
}
+// MARK: Actions
+
+extension DemoWorkflow {
+ enum Action: WorkflowAction {
+ typealias WorkflowType = DemoWorkflow
+
+ case viewTapped
+
+ func apply(toState state: inout DemoWorkflow.State) -> Never? {
+ switch self {
+ case .viewTapped:
+ state += 1
+ }
+
+ return nil
+ }
+ }
+}
+
// MARK: Rendering
@@ -49,12 +68,15 @@ extension DemoWorkflow {
typealias Rendering = SplitScreenContainerScreen
+ private static let sizes: [CGFloat] = [.quarter, .third, .half, 0.75]
+
func render(state: State, context: RenderContext<DemoWorkflow>) -> Rendering {
+ let sink = context.makeSink(of: Action.self)
return SplitScreenContainerScreen(
- leftScreen: BaseScreen(title: "Left screen", backgroundColor: .red),
- rightScreen: BaseScreen(title: "Right screen", backgroundColor: .green),
- ratio: .third
+ leftScreen: BaseScreen(title: "Left screen", backgroundColor: .red, viewTapped: { sink.send(.viewTapped) }),
+ rightScreen: BaseScreen(title: "Right screen", backgroundColor: .green, viewTapped: { sink.send(.viewTapped) }),
+ ratio: DemoWorkflow.sizes[state % DemoWorkflow.sizes.count]
)
}

diff:
@@ -29,6 +29,7 @@ final class SplitScreenContainerViewController: ScreenViewController<SplitScreen
private var leftContentViewController: ScreenViewController<AnyScreen>? = nil
private lazy var leftContainerView: UIView = .init()
+ private var leftContainerViewWidthConstraint: NSLayoutConstraint?
private lazy var separatorView: UIView = .init()
@@ -75,6 +76,10 @@ final class SplitScreenContainerViewController: ScreenViewController<SplitScreen
} else {
self.rightContentViewController = embed(screen.rightScreen, in: rightContainerView)
}
+
+ if (leftContainerViewWidthConstraint?.multiplier != screen.ratio) {
+ updateWidthConstraints(ratio: screen.ratio, animated: true)
+ }
}
private func embed(_ screen: AnyScreen, in containerView: UIView) -> ScreenViewController<AnyScreen> {
@@ -113,7 +118,6 @@ final class SplitScreenContainerViewController: ScreenViewController<SplitScreen
leftContainerView.leftAnchor.constraint(equalTo: view.leftAnchor),
leftContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
leftContainerView.rightAnchor.constraint(equalTo: separatorView.leftAnchor),
- leftContainerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: screen.ratio),
separatorView.topAnchor.constraint(equalTo: view.topAnchor),
separatorView.widthAnchor.constraint(equalToConstant: 1.0),
@@ -124,5 +128,25 @@ final class SplitScreenContainerViewController: ScreenViewController<SplitScreen
rightContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
rightContainerView.rightAnchor.constraint(equalTo: view.rightAnchor),
])
+
+ updateWidthConstraints(ratio: screen.ratio, animated: false)
+ }
+
+ private func updateWidthConstraints(ratio: CGFloat, animated: Bool) {
+ guard animated else {
+ self.leftContainerViewWidthConstraint = self.leftContainerView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: ratio)
+ self.leftContainerViewWidthConstraint?.isActive = true
+ return
+ }
+
+ view.layoutIfNeeded()
+ UIView.animate(withDuration: 0.2) {
+ self.leftContainerViewWidthConstraint?.isActive = false
+
+ self.leftContainerViewWidthConstraint = self.leftContainerView.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: ratio)
+ self.leftContainerViewWidthConstraint?.isActive = true
+
+ self.view.layoutIfNeeded()
+ }
}
}

original_message: Animate ratio changes.


message: Make SplitScreenContainerViewController generic

change_type: MODIFY

diff:
import UIKit
import Workflow
import WorkflowUI
+import SplitScreenContainer
@UIApplicationMain
@@ -42,3 +43,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
}
}
+
+extension ViewRegistry {
+
+ public mutating func registerSplitScreenContainer() {
+ self.register(screenViewControllerType: SplitScreenContainerViewController<AnyScreen, BaseScreen>.self)
+ }
+
+}

diff:
@@ -66,7 +66,7 @@ extension DemoWorkflow {
extension DemoWorkflow {
- typealias Rendering = SplitScreenContainerScreen
+ typealias Rendering = SplitScreenContainerScreen<AnyScreen, BaseScreen>
private static let sizes: [CGFloat] = [.quarter, .third, .half, 0.75]
@@ -74,7 +74,7 @@ extension DemoWorkflow {
let sink = context.makeSink(of: Action.self)
return SplitScreenContainerScreen(
- leftScreen: BaseScreen(title: "Left screen", backgroundColor: .red, viewTapped: { sink.send(.viewTapped) }),
+ leftScreen: AnyScreen(BaseScreen(title: "Left screen", backgroundColor: .red, viewTapped: { sink.send(.viewTapped) })),
rightScreen: BaseScreen(title: "Right screen", backgroundColor: .green, viewTapped: { sink.send(.viewTapped) }),
ratio: DemoWorkflow.sizes[state % DemoWorkflow.sizes.count]
)

diff:
@@ -17,13 +17,13 @@ import WorkflowUI
/// A `SplitScreenContainerScreen` displays two screens side by side with a separator in between.
-public struct SplitScreenContainerScreen: Screen {
+public struct SplitScreenContainerScreen<LeftScreenType: Screen, RightScreenType: Screen>: Screen {
/// The screen displayed to the left of the separator.
- public let leftScreen: AnyScreen
+ public let leftScreen: LeftScreenType
/// The screen displayed to the right of the separator.
- public let rightScreen: AnyScreen
+ public let rightScreen: RightScreenType
/// The ratio of `leftScreen`'s width relative to that of `rightScreen`. Defaults to `.third`.
public let ratio: CGFloat
@@ -31,14 +31,14 @@ public struct SplitScreenContainerScreen: Screen {
/// The color of the `separatorView` displayed between `leftScreen`'s and `rightScreen`'s views.
public let separatorColor: UIColor
- public init<LeftScreenType: Screen, RightScreenType: Screen>(
+ public init(
leftScreen: LeftScreenType,
rightScreen: RightScreenType,
ratio: CGFloat = .third,
separatorColor: UIColor = .black
) {
- self.leftScreen = AnyScreen(leftScreen)
- self.rightScreen = AnyScreen(rightScreen)
+ self.leftScreen = leftScreen
+ self.rightScreen = rightScreen
self.ratio = ratio
self.separatorColor = separatorColor
}

diff:
import Workflow
import WorkflowUI
-extension ViewRegistry {
+public final class SplitScreenContainerViewController<LeftScreenType: Screen, RightScreenType: Screen>: ScreenViewController<SplitScreenContainerScreen<LeftScreenType, RightScreenType>> {
- public mutating func registerSplitScreenContainer() {
- self.register(screenViewControllerType: SplitScreenContainerViewController.self)
- }
-
-}
-
-
-final class SplitScreenContainerViewController: ScreenViewController<SplitScreenContainerScreen> {
-
- private var leftContentViewController: ScreenViewController<AnyScreen>? = nil
+ private var leftContentViewController: ScreenViewController<LeftScreenType>? = nil
private lazy var leftContainerView: UIView = .init()
private var leftContainerViewWidthConstraint: NSLayoutConstraint?
private lazy var separatorView: UIView = .init()
- private var rightContentViewController: ScreenViewController<AnyScreen>? = nil
+ private var rightContentViewController: ScreenViewController<RightScreenType>? = nil
private lazy var rightContainerView: UIView = .init()
- required init(screen: SplitScreenContainerScreen, viewRegistry: ViewRegistry) {
+ required init(screen: SplitScreenContainerScreen<LeftScreenType, RightScreenType>, viewRegistry: ViewRegistry) {
super.init(screen: screen, viewRegistry: viewRegistry)
update(with: screen)
}
- override func screenDidChange(from previousScreen: SplitScreenContainerScreen) {
+ override public func screenDidChange(from previousScreen: SplitScreenContainerScreen<LeftScreenType, RightScreenType>) {
update(with: screen)
}
- private func update(with screen: SplitScreenContainerScreen) {
+ private func update(with screen: SplitScreenContainerScreen<LeftScreenType, RightScreenType>) {
separatorView.backgroundColor = screen.separatorColor
- if let leftContentViewController = leftContentViewController {
- if leftContentViewController.screenType == type(of: screen.leftScreen) {
- leftContentViewController.update(screen: screen.leftScreen)
- } else {
- leftContentViewController.willMove(toParent: nil)
- leftContentViewController.view.removeFromSuperview()
- leftContentViewController.removeFromParent()
-
- self.leftContentViewController = embed(screen.leftScreen, in: leftContainerView)
- }
- } else {
- self.leftContentViewController = embed(screen.leftScreen, in: leftContainerView)
- }
-
- if let rightContentViewController = rightContentViewController {
- if rightContentViewController.screenType == type(of: screen.rightScreen) {
- rightContentViewController.update(screen: screen.rightScreen)
- } else {
- rightContentViewController.willMove(toParent: nil)
- rightContentViewController.view.removeFromSuperview()
- rightContentViewController.removeFromParent()
-
- self.rightContentViewController = embed(screen.rightScreen, in: rightContainerView)
- }
- } else {
- self.rightContentViewController = embed(screen.rightScreen, in: rightContainerView)
- }
+ leftContentViewController?.update(screen: screen.leftScreen)
+ rightContentViewController?.update(screen: screen.rightScreen)
if (leftContainerViewWidthConstraint?.multiplier != screen.ratio) {
updateWidthConstraints(ratio: screen.ratio, animated: true)
}
}
- private func embed(_ screen: AnyScreen, in containerView: UIView) -> ScreenViewController<AnyScreen> {
- let viewController = viewRegistry.provideView(for: screen)
-
- addChild(viewController)
-
- viewController.view.translatesAutoresizingMaskIntoConstraints = false
- containerView.addSubview(viewController.view)
-
- NSLayoutConstraint.activate([
- viewController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
- viewController.view.rightAnchor.constraint(equalTo: containerView.rightAnchor),
- viewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
- viewController.view.leftAnchor.constraint(equalTo: containerView.leftAnchor),
- ])
-
- viewController.didMove(toParent: self)
-
- return viewController
- }
-
- override func viewDidLoad() {
+ override public func viewDidLoad() {
super.viewDidLoad()
leftContainerView.translatesAutoresizingMaskIntoConstraints = false
@@ -130,6 +76,29 @@ final class SplitScreenContainerViewController: ScreenViewController<SplitScreen
])
updateWidthConstraints(ratio: screen.ratio, animated: false)
+
+ self.leftContentViewController = embed(screen.leftScreen, in: leftContainerView)
+ self.rightContentViewController = embed(screen.rightScreen, in: rightContainerView)
+ }
+
+ private func embed<ScreenType: Screen>(_ screen: ScreenType, in containerView: UIView) -> ScreenViewController<ScreenType> {
+ let viewController = viewRegistry.provideView(for: screen)
+
+ addChild(viewController)
+
+ viewController.view.translatesAutoresizingMaskIntoConstraints = false
+ containerView.addSubview(viewController.view)
+
+ NSLayoutConstraint.activate([
+ viewController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
+ viewController.view.rightAnchor.constraint(equalTo: containerView.rightAnchor),
+ viewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
+ viewController.view.leftAnchor.constraint(equalTo: containerView.leftAnchor),
+ ])
+
+ viewController.didMove(toParent: self)
+
+ return viewController
}
private func updateWidthConstraints(ratio: CGFloat, animated: Bool) {

license: Apache License 2.0

repo: square/workflow-swift


message: Add example of changing screen at run-time

diff:
@@ -29,7 +29,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var viewRegistry = ViewRegistry()
viewRegistry.registerSplitScreenContainer()
- viewRegistry.registerBaseScreen()
+ viewRegistry.registerFooScreen()
+ viewRegistry.registerBarScreen()
let container = ContainerViewController(
workflow: DemoWorkflow(),
@@ -47,7 +48,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
extension ViewRegistry {
public mutating func registerSplitScreenContainer() {
- self.register(screenViewControllerType: SplitScreenContainerViewController<AnyScreen, BaseScreen>.self)
+ self.register(screenViewControllerType: SplitScreenContainerViewController<AnyScreen, FooScreen>.self)
}
}

change_type: ADD

diff:
+/*
+ * 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.
+ */
+import Workflow
+import WorkflowUI
+
+
+struct BarScreen: Screen {
+ let title: String
+ let backgroundColors: [UIColor]
+ let viewTapped: () -> Void
+}
+
+
+extension ViewRegistry {
+
+ public mutating func registerBarScreen() {
+ self.register(screenViewControllerType: BarScreenViewController.self)
+ }
+
+}
+
+
+fileprivate final class BarScreenViewController: ScreenViewController<BarScreen> {
+
+ private lazy var titleLabel: UILabel = .init()
+ private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init()
+ private var gradientLayer: CAGradientLayer?
+
+ required init(screen: BarScreen, viewRegistry: ViewRegistry) {
+ super.init(screen: screen, viewRegistry: viewRegistry)
+
+ update(with: screen)
+ }
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ tapGestureRecognizer.addTarget(self, action: #selector(viewTapped))
+ view.addGestureRecognizer(tapGestureRecognizer)
+
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
+ titleLabel.textAlignment = .center
+ titleLabel.textColor = .white
+ view.addSubview(titleLabel)
+
+ NSLayoutConstraint.activate([
+ titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
+ ])
+
+ updateGradient(for: view, colors: screen.backgroundColors)
+ }
+
+ override func screenDidChange(from previousScreen: BarScreen) {
+ update(with: screen)
+ }
+
+ private func update(with screen: BarScreen) {
+ titleLabel.text = screen.title
+
+ updateGradient(for: view, colors: screen.backgroundColors)
+ }
+
+ private func updateGradient(for targetView: UIView, colors: [UIColor]) {
+ let newGradientLayer = CAGradientLayer()
+
+ newGradientLayer.frame = targetView.bounds
+ newGradientLayer.colors = colors.map({ $0.cgColor })
+
+ targetView.layer.insertSublayer(newGradientLayer, at: 0)
+
+ gradientLayer?.removeFromSuperlayer()
+
+ gradientLayer = newGradientLayer
+ }
+
+ @objc
+ private func viewTapped() {
+ screen.viewTapped()
+ }
+}
+

diff:
@@ -66,19 +66,48 @@ extension DemoWorkflow {
extension DemoWorkflow {
- typealias Rendering = SplitScreenContainerScreen<AnyScreen, BaseScreen>
+ typealias Rendering = SplitScreenContainerScreen<AnyScreen, FooScreen>
private static let sizes: [CGFloat] = [.quarter, .third, .half, 0.75]
+ private static let colors: [UIColor] = [.red, .blue, .green, .yellow]
+ private static let complimentaryColors: [UIColor] = [.blue, .green, .yellow, .purple,]
func render(state: State, context: RenderContext<DemoWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)
return SplitScreenContainerScreen(
- leftScreen: AnyScreen(BaseScreen(title: "Left screen", backgroundColor: .red, viewTapped: { sink.send(.viewTapped) })),
- rightScreen: BaseScreen(title: "Right screen", backgroundColor: .green, viewTapped: { sink.send(.viewTapped) }),
+ leftScreen: leftScreenFor(state: state, context: context),
+ rightScreen: FooScreen(title: "Right screen", backgroundColor: .green, viewTapped: { sink.send(.viewTapped) }),
ratio: DemoWorkflow.sizes[state % DemoWorkflow.sizes.count]
)
}
+ private func leftScreenFor(state: State, context: RenderContext<DemoWorkflow>) -> AnyScreen {
+ let sink = context.makeSink(of: Action.self)
+
+ let color = DemoWorkflow.colors[state % DemoWorkflow.colors.count]
+
+ if state % 2 == 0 {
+ return AnyScreen(
+ FooScreen(
+ title: "Left Foo screen",
+ backgroundColor: color,
+ viewTapped: { sink.send(.viewTapped) }
+ )
+ )
+ } else {
+ let complimentaryColor = DemoWorkflow.complimentaryColors[state % DemoWorkflow.complimentaryColors.count]
+
+ return AnyScreen(
+ BarScreen(
+ title: "Left Bar screen",
+ backgroundColors: [color, complimentaryColor],
+ viewTapped: { sink.send(.viewTapped) }
+ )
+ )
+ }
+
+ }
+
}

change_type: RENAME

diff:
@@ -17,7 +17,7 @@ import Workflow
import WorkflowUI
-struct BaseScreen: Screen {
+struct FooScreen: Screen {
let title: String
let backgroundColor: UIColor
let viewTapped: () -> Void
@@ -26,19 +26,19 @@ struct BaseScreen: Screen {
extension ViewRegistry {
- public mutating func registerBaseScreen() {
- self.register(screenViewControllerType: BaseScreenViewController.self)
+ public mutating func registerFooScreen() {
+ self.register(screenViewControllerType: FooScreenViewController.self)
}
}
-fileprivate final class BaseScreenViewController: ScreenViewController<BaseScreen> {
+fileprivate final class FooScreenViewController: ScreenViewController<FooScreen> {
private lazy var titleLabel: UILabel = .init()
private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init()
- required init(screen: BaseScreen, viewRegistry: ViewRegistry) {
+ required init(screen: FooScreen, viewRegistry: ViewRegistry) {
super.init(screen: screen, viewRegistry: viewRegistry)
update(with: screen)
@@ -60,11 +60,11 @@ fileprivate final class BaseScreenViewController: ScreenViewController<BaseScree
])
}
- override func screenDidChange(from previousScreen: BaseScreen) {
+ override func screenDidChange(from previousScreen: FooScreen) {
update(with: screen)
}
- private func update(with screen: BaseScreen) {
+ private func update(with screen: FooScreen) {
view.backgroundColor = screen.backgroundColor
titleLabel.text = screen.title
}


message: Use frame based layout

diff:
import Workflow
import WorkflowUI
-public final class SplitScreenContainerViewController<LeftScreenType: Screen, RightScreenType: Screen>: ScreenViewController<SplitScreenContainerScreen<LeftScreenType, RightScreenType>> {
+public final class SplitScreenContainerViewController<LeftScreenType: Screen, RightScreenType: Screen>: ScreenViewController<SplitScreenContainerViewController.ContainerScreen> {
+ public typealias ContainerScreen = SplitScreenContainerScreen<LeftScreenType, RightScreenType>
private var leftContentViewController: ScreenViewController<LeftScreenType>? = nil
private lazy var leftContainerView: UIView = .init()
- private var leftContainerViewWidthConstraint: NSLayoutConstraint?
private lazy var separatorView: UIView = .init()
private var rightContentViewController: ScreenViewController<RightScreenType>? = nil
private lazy var rightContainerView: UIView = .init()
- required init(screen: SplitScreenContainerScreen<LeftScreenType, RightScreenType>, viewRegistry: ViewRegistry) {
+ private var currentRatio: CGFloat {
+ didSet {
+ guard oldValue != currentRatio else {
+ return
+ }
+
+ UIView.animate(withDuration: 0.25) {
+ self.view.setNeedsLayout()
+ self.view.layoutIfNeeded()
+ }
+ }
+ }
+
+ required init(screen: ContainerScreen, viewRegistry: ViewRegistry) {
+ currentRatio = screen.ratio
+
super.init(screen: screen, viewRegistry: viewRegistry)
}
- override public func screenDidChange(from previousScreen: SplitScreenContainerScreen<LeftScreenType, RightScreenType>) {
+ override public func screenDidChange(from previousScreen: ContainerScreen) {
update(with: screen)
}
- private func update(with screen: SplitScreenContainerScreen<LeftScreenType, RightScreenType>) {
- if (separatorView.backgroundColor != screen.separatorColor) {
+ private func update(with screen: ContainerScreen) {
separatorView.backgroundColor = screen.separatorColor
- }
leftContentViewController?.update(screen: screen.leftScreen)
rightContentViewController?.update(screen: screen.rightScreen)
- if (leftContainerViewWidthConstraint?.multiplier != screen.ratio) {
- updateWidthConstraints(ratio: screen.ratio, animated: true)
- }
+ //Intentional force of layout pass after updating the child view controllers
+ view.layoutIfNeeded()
+
+ //Update the current ratio in an animated fashion if needed
+ currentRatio = screen.ratio
}
override public func viewDidLoad() {
super.viewDidLoad()
- leftContainerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(leftContainerView)
-
- separatorView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(separatorView)
-
- rightContainerView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(rightContainerView)
- NSLayoutConstraint.activate([
- leftContainerView.topAnchor.constraint(equalTo: view.topAnchor),
- leftContainerView.leftAnchor.constraint(equalTo: view.leftAnchor),
- leftContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- leftContainerView.rightAnchor.constraint(equalTo: separatorView.leftAnchor),
+ self.leftContentViewController = embed(screen.leftScreen, in: leftContainerView)
+ self.rightContentViewController = embed(screen.rightScreen, in: rightContainerView)
+ update(with: screen)
+ }
- separatorView.topAnchor.constraint(equalTo: view.topAnchor),
- separatorView.widthAnchor.constraint(equalToConstant: 1.0),
- separatorView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ public override func viewDidLayoutSubviews() {
+ super.viewDidLayoutSubviews()
- rightContainerView.topAnchor.constraint(equalTo: view.topAnchor),
- rightContainerView.leftAnchor.constraint(equalTo: separatorView.rightAnchor),
- rightContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- rightContainerView.rightAnchor.constraint(equalTo: view.rightAnchor),
- ])
+ let distance = view.bounds.width * currentRatio
- updateWidthConstraints(ratio: screen.ratio, animated: false)
+ let (firstSlice, rightRect) = view.bounds.divided(atDistance: distance, from: .minXEdge)
- self.leftContentViewController = embed(screen.leftScreen, in: leftContainerView)
- self.rightContentViewController = embed(screen.rightScreen, in: rightContainerView)
+ let (leftRect, separatorRect) = firstSlice.divided(atDistance: distance - 1, from: .minXEdge)
- update(with: screen)
+ leftContainerView.frame = leftRect
+ separatorView.frame = separatorRect
+ rightContainerView.frame = rightRect
+ leftContentViewController?.view.frame = leftRect
+ rightContentViewController?.view.frame = CGRect(x: 0, y: 0, width: rightRect.width, height: rightRect.height)
}
private func embed<ScreenType: Screen>(_ screen: ScreenType, in containerView: UIView) -> ScreenViewController<ScreenType> {
let viewController = viewRegistry.provideView(for: screen)
addChild(viewController)
-
- viewController.view.translatesAutoresizingMaskIntoConstraints = false
containerView.addSubview(viewController.view)
-
- NSLayoutConstraint.activate([
- viewController.view.topAnchor.constraint(equalTo: containerView.topAnchor),
- viewController.view.rightAnchor.constraint(equalTo: containerView.rightAnchor),
- viewController.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
- viewController.view.leftAnchor.constraint(equalTo: containerView.leftAnchor),
- ])
-
viewController.didMove(toParent: self)
return viewController
}
-
- private func updateWidthConstraints(ratio: CGFloat, animated: Bool) {
- func updateConstraints() {
- leftContainerViewWidthConstraint?.isActive = false
-
- leftContainerViewWidthConstraint = leftContainerView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: ratio)
- leftContainerViewWidthConstraint?.isActive = true
- }
-
- if animated {
- view.layoutIfNeeded()
- UIView.animate(withDuration: 0.2) {
- updateConstraints()
-
- self.view.layoutIfNeeded()
- }
- } else {
- updateConstraints()
- }
- }
}

original_message: Use frame based layout


message: Take separatorWidth as part of the `SplitScreenContainerScreen` model

diff:
@@ -78,7 +78,9 @@ extension DemoWorkflow {
return SplitScreenContainerScreen(
leftScreen: leftScreenFor(state: state, context: context),
rightScreen: FooScreen(title: "Right screen", backgroundColor: .green, viewTapped: { sink.send(.viewTapped) }),
- ratio: DemoWorkflow.sizes[state % DemoWorkflow.sizes.count]
+ ratio: DemoWorkflow.sizes[state % DemoWorkflow.sizes.count],
+ separatorColor: .black,
+ separatorWidth: 1.0 * CGFloat(state)
)
}

diff:
@@ -31,16 +31,21 @@ public struct SplitScreenContainerScreen<LeftScreenType: Screen, RightScreenType
/// The color of the `separatorView` displayed between `leftScreen`'s and `rightScreen`'s views.
public let separatorColor: UIColor
+ /// The width of the `separatorView` displayed between `leftScreen`'s and `rightScreen`'s views.
+ public let separatorWidth: CGFloat
+
public init(
leftScreen: LeftScreenType,
rightScreen: RightScreenType,
ratio: CGFloat = .third,
- separatorColor: UIColor = .black
+ separatorColor: UIColor = .black,
+ separatorWidth: CGFloat = 1.0
) {
self.leftScreen = leftScreen
self.rightScreen = rightScreen
self.ratio = ratio
self.separatorColor = separatorColor
+ self.separatorWidth = separatorWidth
}
}

diff:
@@ -33,15 +33,25 @@ public final class SplitScreenContainerViewController<LeftScreenType: Screen, Ri
return
}
- UIView.animate(withDuration: 0.25) {
- self.view.setNeedsLayout()
- self.view.layoutIfNeeded()
+ needsAnimatedLayout = true
}
}
+
+ private var currentSeparatorWidth: CGFloat {
+ didSet {
+ guard oldValue != currentSeparatorWidth else {
+ return
+ }
+
+ needsAnimatedLayout = true
+ }
}
+ private var needsAnimatedLayout = false
+
required init(screen: ContainerScreen, viewRegistry: ViewRegistry) {
currentRatio = screen.ratio
+ currentSeparatorWidth = screen.separatorWidth
super.init(screen: screen, viewRegistry: viewRegistry)
}
@@ -59,8 +69,17 @@ public final class SplitScreenContainerViewController<LeftScreenType: Screen, Ri
//Intentional force of layout pass after updating the child view controllers
view.layoutIfNeeded()
- //Update the current ratio in an animated fashion if needed
currentRatio = screen.ratio
+ currentSeparatorWidth = screen.separatorWidth
+
+ if needsAnimatedLayout {
+ needsAnimatedLayout = false
+
+ UIView.animate(withDuration: 0.25) {
+ self.view.setNeedsLayout()
+ self.view.layoutIfNeeded()
+ }
+ }
}
override public func viewDidLoad() {
@@ -82,7 +101,7 @@ public final class SplitScreenContainerViewController<LeftScreenType: Screen, Ri
let (firstSlice, rightRect) = view.bounds.divided(atDistance: distance, from: .minXEdge)
- let (leftRect, separatorRect) = firstSlice.divided(atDistance: distance - 1, from: .minXEdge)
+ let (leftRect, separatorRect) = firstSlice.divided(atDistance: distance - currentSeparatorWidth, from: .minXEdge)
leftContainerView.frame = leftRect
separatorView.frame = separatorRect


message: Rename `left` and `right` to `leading` and `trailing` and respect layout direction.

diff:
@@ -76,8 +76,8 @@ extension DemoWorkflow {
let sink = context.makeSink(of: Action.self)
return SplitScreenContainerScreen(
- leftScreen: leftScreenFor(state: state, context: context),
- rightScreen: FooScreen(title: "Right screen", backgroundColor: .green, viewTapped: { sink.send(.viewTapped) }),
+ leadingScreen: leadingScreenFor(state: state, context: context),
+ trailingScreen: FooScreen(title: "Trailing screen", backgroundColor: .green, viewTapped: { sink.send(.viewTapped) }),
ratio: DemoWorkflow.sizes[state % DemoWorkflow.sizes.count],
separatorColor: .black,
separatorWidth: 1.0 * CGFloat(state)
@@ -85,7 +85,7 @@ extension DemoWorkflow {
}
- private func leftScreenFor(state: State, context: RenderContext<DemoWorkflow>) -> AnyScreen {
+ private func leadingScreenFor(state: State, context: RenderContext<DemoWorkflow>) -> AnyScreen {
let sink = context.makeSink(of: Action.self)
let color = DemoWorkflow.colors[state % DemoWorkflow.colors.count]
@@ -93,7 +93,7 @@ extension DemoWorkflow {
if state % 2 == 0 {
return AnyScreen(
FooScreen(
- title: "Left Foo screen",
+ title: "Leading Foo screen",
backgroundColor: color,
viewTapped: { sink.send(.viewTapped) }
)
@@ -103,7 +103,7 @@ extension DemoWorkflow {
return AnyScreen(
BarScreen(
- title: "Left Bar screen",
+ title: "Leading Bar screen",
backgroundColors: [color, complimentaryColor],
viewTapped: { sink.send(.viewTapped) }
)

diff:
@@ -13,7 +13,7 @@ class SplitScreenContainerScreenSnapshotTests: FBSnapshotTestCase {
func test_splitRatio() {
var viewRegistry = ViewRegistry()
- viewRegistry.register(screenViewControllerType: BaseScreenViewController.self)
+ viewRegistry.register(screenViewControllerType: FooScreenViewController.self)
let ratios: [String : CGFloat] = [
"third" : .third,
@@ -24,8 +24,8 @@ class SplitScreenContainerScreenSnapshotTests: FBSnapshotTestCase {
for (name, ratio) in ratios {
let splitScreenContainerScreen = SplitScreenContainerScreen(
- leftScreen: BaseScreen(title: "Left screen", backgroundColor: .green),
- rightScreen: BaseScreen(title: "Right screen", backgroundColor: .red),
+ leadingScreen: FooScreen(title: "Leading screen", backgroundColor: .green, viewTapped: {}),
+ trailingScreen: FooScreen(title: "Trailing screen", backgroundColor: .red, viewTapped: {}),
ratio: ratio
)
@@ -41,20 +41,19 @@ class SplitScreenContainerScreenSnapshotTests: FBSnapshotTestCase {
}
-struct BaseScreen: Screen {
- var title: String
- var backgroundColor: UIColor
+fileprivate struct FooScreen: Screen {
+ let title: String
+ let backgroundColor: UIColor
+ let viewTapped: () -> Void
}
-fileprivate final class BaseScreenViewController: ScreenViewController<BaseScreen> {
+fileprivate final class FooScreenViewController: ScreenViewController<FooScreen> {
- private let titleLabel: UILabel
-
- required init(screen: BaseScreen, viewRegistry: ViewRegistry) {
- titleLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 100, height: 20))
- titleLabel.textAlignment = .center
+ private lazy var titleLabel: UILabel = .init()
+ private lazy var tapGestureRecognizer: UITapGestureRecognizer = .init()
+ required init(screen: FooScreen, viewRegistry: ViewRegistry) {
super.init(screen: screen, viewRegistry: viewRegistry)
update(with: screen)
@@ -63,22 +62,31 @@ fileprivate final class BaseScreenViewController: ScreenViewController<BaseScree
override func viewDidLoad() {
super.viewDidLoad()
- view.addSubview(titleLabel)
- }
+ tapGestureRecognizer.addTarget(self, action: #selector(viewTapped))
+ view.addGestureRecognizer(tapGestureRecognizer)
- override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
+ titleLabel.textAlignment = .center
+ view.addSubview(titleLabel)
- titleLabel.center = view.center
+ NSLayoutConstraint.activate([
+ titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
+ titleLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
+ ])
}
- override func screenDidChange(from previousScreen: BaseScreen) {
+ override func screenDidChange(from previousScreen: FooScreen) {
update(with: screen)
}
- private func update(with screen: BaseScreen) {
+ private func update(with screen: FooScreen) {
view.backgroundColor = screen.backgroundColor
titleLabel.text = screen.title
}
+
+ @objc
+ private func viewTapped() {
+ screen.viewTapped()
+ }
}

diff:
@@ -17,32 +17,32 @@ import WorkflowUI
/// A `SplitScreenContainerScreen` displays two screens side by side with a separator in between.
-public struct SplitScreenContainerScreen<LeftScreenType: Screen, RightScreenType: Screen>: Screen {
+public struct SplitScreenContainerScreen<LeadingScreenType: Screen, TrailingScreenType: Screen>: Screen {
- /// The screen displayed to the left of the separator.
- public let leftScreen: LeftScreenType
+ /// The screen displayed leading the separator.
+ public let leadingScreen: LeadingScreenType
- /// The screen displayed to the right of the separator.
- public let rightScreen: RightScreenType
+ /// The screen displayed trailing the separator.
+ public let trailingScreen: TrailingScreenType
- /// The ratio of `leftScreen`'s width relative to that of `rightScreen`. Defaults to `.third`.
+ /// The ratio of `leadingScreen`'s width relative to that of `trailingScreen`. Defaults to `.third`.
public let ratio: CGFloat
- /// The color of the `separatorView` displayed between `leftScreen`'s and `rightScreen`'s views.
+ /// The color of the `separatorView` displayed between `leadingScreen`'s and `trailingScreen`'s views.
public let separatorColor: UIColor
- /// The width of the `separatorView` displayed between `leftScreen`'s and `rightScreen`'s views.
+ /// The width of the `separatorView` displayed between `leadingScreen`'s and `trailingScreen`'s views.
public let separatorWidth: CGFloat
public init(
- leftScreen: LeftScreenType,
- rightScreen: RightScreenType,
+ leadingScreen: LeadingScreenType,
+ trailingScreen: TrailingScreenType,
ratio: CGFloat = .third,
separatorColor: UIColor = .black,
separatorWidth: CGFloat = 1.0
) {
- self.leftScreen = leftScreen
- self.rightScreen = rightScreen
+ self.leadingScreen = leadingScreen
+ self.trailingScreen = trailingScreen
self.ratio = ratio
self.separatorColor = separatorColor
self.separatorWidth = separatorWidth

diff:
import Workflow
import WorkflowUI
-public final class SplitScreenContainerViewController<LeftScreenType: Screen, RightScreenType: Screen>: ScreenViewController<SplitScreenContainerViewController.ContainerScreen> {
- public typealias ContainerScreen = SplitScreenContainerScreen<LeftScreenType, RightScreenType>
+public final class SplitScreenContainerViewController<LeadingScreenType: Screen, TrailingScreenType: Screen>: ScreenViewController<SplitScreenContainerViewController.ContainerScreen> {
+ public typealias ContainerScreen = SplitScreenContainerScreen<LeadingScreenType, TrailingScreenType>
- private var leftContentViewController: ScreenViewController<LeftScreenType>? = nil
- private lazy var leftContainerView: UIView = .init()
+ private var leadingContentViewController: ScreenViewController<LeadingScreenType>? = nil
+ private lazy var leadingContainerView: UIView = .init()
private lazy var separatorView: UIView = .init()
- private var rightContentViewController: ScreenViewController<RightScreenType>? = nil
- private lazy var rightContainerView: UIView = .init()
+ private var trailingContentViewController: ScreenViewController<TrailingScreenType>? = nil
+ private lazy var trailingContainerView: UIView = .init()
private var currentRatio: CGFloat {
didSet {
@@ -63,8 +63,8 @@ public final class SplitScreenContainerViewController<LeftScreenType: Screen, Ri
private func update(with screen: ContainerScreen) {
separatorView.backgroundColor = screen.separatorColor
- leftContentViewController?.update(screen: screen.leftScreen)
- rightContentViewController?.update(screen: screen.rightScreen)
+ leadingContentViewController?.update(screen: screen.leadingScreen)
+ trailingContentViewController?.update(screen: screen.trailingScreen)
//Intentional force of layout pass after updating the child view controllers
view.layoutIfNeeded()
@@ -85,12 +85,12 @@ public final class SplitScreenContainerViewController<LeftScreenType: Screen, Ri
override public func viewDidLoad() {
super.viewDidLoad()
- view.addSubview(leftContainerView)
+ view.addSubview(leadingContainerView)
view.addSubview(separatorView)
- view.addSubview(rightContainerView)
+ view.addSubview(trailingContainerView)
- self.leftContentViewController = embed(screen.leftScreen, in: leftContainerView)
- self.rightContentViewController = embed(screen.rightScreen, in: rightContainerView)
+ self.leadingContentViewController = embed(screen.leadingScreen, in: leadingContainerView)
+ self.trailingContentViewController = embed(screen.trailingScreen, in: trailingContainerView)
update(with: screen)
}
@@ -99,15 +99,27 @@ public final class SplitScreenContainerViewController<LeftScreenType: Screen, Ri
let distance = view.bounds.width * currentRatio
- let (firstSlice, rightRect) = view.bounds.divided(atDistance: distance, from: .minXEdge)
+ let (firstSlice, trailingRect) = view.bounds.divided(atDistance: distance, from: .minXEdge)
- let (leftRect, separatorRect) = firstSlice.divided(atDistance: distance - currentSeparatorWidth, from: .minXEdge)
+ let (leadingRect, separatorRect) = firstSlice.divided(atDistance: distance - currentSeparatorWidth, from: .minXEdge)
+
+ let isRightToLeft = isLayoutDirectionRightToLeft()
+
+ leadingContainerView.frame = isRightToLeft ? trailingRect: leadingRect
+ leadingContentViewController?.view.frame = leadingContainerView.bounds
- leftContainerView.frame = leftRect
separatorView.frame = separatorRect
- rightContainerView.frame = rightRect
- leftContentViewController?.view.frame = leftRect
- rightContentViewController?.view.frame = CGRect(x: 0, y: 0, width: rightRect.width, height: rightRect.height)
+
+ trailingContainerView.frame = isRightToLeft ? leadingRect : trailingRect
+ trailingContentViewController?.view.frame = trailingContainerView.bounds
+ }
+
+ private func isLayoutDirectionRightToLeft() -> Bool {
+ if #available(iOS 10.0, *) {
+ return traitCollection.layoutDirection == .rightToLeft
+ } else {
+ return UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft
+ }
}
private func embed<ScreenType: Screen>(_ screen: ScreenType, in containerView: UIView) -> ScreenViewController<ScreenType> {


message: Add internal ContainerView class

diff:
+/*
+* 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.
+*/
+import UIKit
+
+internal class ContainerView: UIView {
+
+ var contentView: UIView = .init()
+
+ override init(frame: CGRect) {
+ super.init(frame: frame)
+
+ commonInit()
+ }
+
+ required init?(coder: NSCoder) {
+ super.init(coder: coder)
+
+ commonInit()
+ }
+
+ private func commonInit() {
+ self.addSubview(contentView)
+ }
+
+ override func layoutSubviews() {
+ super.layoutSubviews()
+
+ contentView.frame = self.bounds
+
+ contentView.subviews.forEach({ $0.frame = self.bounds })
+ }
+}

diff:
@@ -20,12 +20,12 @@ public final class SplitScreenContainerViewController<LeadingScreenType: Screen,
public typealias ContainerScreen = SplitScreenContainerScreen<LeadingScreenType, TrailingScreenType>
private var leadingContentViewController: ScreenViewController<LeadingScreenType>? = nil
- private lazy var leadingContainerView: UIView = .init()
+ private lazy var leadingContainerView: ContainerView = .init()
private lazy var separatorView: UIView = .init()
private var trailingContentViewController: ScreenViewController<TrailingScreenType>? = nil
- private lazy var trailingContainerView: UIView = .init()
+ private lazy var trailingContainerView: ContainerView = .init()
private var currentRatio: CGFloat {
didSet {
@@ -91,6 +91,7 @@ public final class SplitScreenContainerViewController<LeadingScreenType: Screen,
self.leadingContentViewController = embed(screen.leadingScreen, in: leadingContainerView)
self.trailingContentViewController = embed(screen.trailingScreen, in: trailingContainerView)
+
update(with: screen)
}
@@ -103,30 +104,30 @@ public final class SplitScreenContainerViewController<LeadingScreenType: Screen,
let (leadingRect, separatorRect) = firstSlice.divided(atDistance: distance - currentSeparatorWidth, from: .minXEdge)
- let isRightToLeft = isLayoutDirectionRightToLeft()
-
- leadingContainerView.frame = isRightToLeft ? trailingRect: leadingRect
- leadingContentViewController?.view.frame = leadingContainerView.bounds
+ leadingContainerView.frame = isLayoutDirectionRightToLeft ? trailingRect: leadingRect
separatorView.frame = separatorRect
- trailingContainerView.frame = isRightToLeft ? leadingRect : trailingRect
- trailingContentViewController?.view.frame = trailingContainerView.bounds
+ trailingContainerView.frame = isLayoutDirectionRightToLeft ? leadingRect : trailingRect
+ }
}
- private func isLayoutDirectionRightToLeft() -> Bool {
+fileprivate extension UIViewController {
+ var isLayoutDirectionRightToLeft: Bool {
if #available(iOS 10.0, *) {
return traitCollection.layoutDirection == .rightToLeft
} else {
return UIView.userInterfaceLayoutDirection(for: view.semanticContentAttribute) == .rightToLeft
}
}
+}
- private func embed<ScreenType: Screen>(_ screen: ScreenType, in containerView: UIView) -> ScreenViewController<ScreenType> {
+fileprivate extension ScreenViewController {
+ func embed<ScreenType: Screen>(_ screen: ScreenType, in containerView: ContainerView) -> ScreenViewController<ScreenType> {
let viewController = viewRegistry.provideView(for: screen)
addChild(viewController)
- containerView.addSubview(viewController.view)
+ containerView.contentView.addSubview(viewController.view)
viewController.didMove(toParent: self)
return viewController

original_message: Add internal ContainerView class


message:
Add SignalWorker to wrap Signal
Wrapping them in a Worker prevents a race condition where multiple Signals
fire on the same runloop, resulting in dropped events.

diff:
@@ -71,6 +71,7 @@ public class RenderContext<WorkflowType: Workflow>: RenderContextType {
fatalError()
}
+ @available(*, deprecated, message: "Use a SignalWorker instead")
public func subscribe<Action>(signal: Signal<Action, Never>) where Action : WorkflowAction, WorkflowType == Action.WorkflowType {
fatalError()
}

diff:
+/*
+ * 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 ReactiveSwift
+
+/// A `Worker` that wraps a `Signal`
+public struct SignalWorker<Key: Equatable, Value>: Worker {
+ let key: Key
+ let signal: Signal<Value, Never>
+
+ public init(key: Key, signal: Signal<Value, Never>) {
+ self.key = key
+ self.signal = signal
+ }
+
+ public func run() -> SignalProducer<Value, Never> {
+ return SignalProducer(signal)
+ }
+
+ public func isEquivalent(to otherWorker: SignalWorker) -> Bool {
+ return key == otherWorker.key
+ }
+}
+
+extension Signal where Error == Never {
+ public func asWorker<Key: Equatable>(key: Key) -> SignalWorker<Key, Value> {
+ return SignalWorker(key: key, signal: self)
+ }
+}


message: Update CHANGELOG for 0.25.0

diff:
Change Log
==========
+## Version 0.25.0
+
+_2020-03-27_
+
+### Kotlin
+
+* Swift-only release. No changes.
+
+### Swift
+
+* Introduce `SignalWorker` to wrap `Signal`s (#1048)
+
## Version 0.24.0
_2020-03-10_

original_message: Update CHANGELOG for 0.25.0


message: Fix Xcode 11 build issues with WorkflowTesting

diff:
@@ -23,6 +23,8 @@ Pod::Spec.new do |s|
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'swift/WorkflowTesting/Tests/**/*.swift'
test_spec.framework = 'XCTest'
+ test_spec.library = 'swiftDispatch'
+ test_spec.library = 'swiftFoundation'
end
end


message:
Bump CI to Xcode 11.4
Fix build failures and warnings.
Add Switch Xcode Step to Tutorial

diff:
@@ -43,7 +43,7 @@ jobs:
bundle check || bundle install
# Don't need to run pod gen, the website script does that itself.
brew install sourcedocs
- sudo xcode-select -s /Applications/Xcode_11.3.1.app
+ sudo xcode-select -s /Applications/Xcode_11.4.app
# Docs dependencies
- name: Set up Python

diff:
@@ -51,11 +51,11 @@ jobs:
bundle exec pod gen Development.podspec
- name: Switch Xcode
- run: sudo xcode-select -s /Applications/Xcode_11.3.1.app
+ run: sudo xcode-select -s /Applications/Xcode_11.4.app
- name: Build & Test
run: |
- set -o pipefail && xcodebuild -workspace gen/Development/Development.xcworkspace -scheme ${{ matrix.scheme }} -destination platform\=iOS\ Simulator,OS\=13.3,name\=iPad\ Pro\ \(9.7-inch\) build test | xcpretty
+ set -o pipefail && xcodebuild -workspace gen/Development/Development.xcworkspace -scheme ${{ matrix.scheme }} -destination platform\=iOS\ Simulator,OS\=13.4,name\=iPad\ Pro\ \(9.7-inch\) build test | xcpretty
spm:
runs-on: macOS-latest
@@ -87,11 +87,14 @@ jobs:
run: |
bundle check || bundle install --path .bundle
+ - name: Switch Xcode
+ run: sudo xcode-select -s /Applications/Xcode_11.4.app
+
- name: Tutorial App
run: |
cd swift/Samples/Tutorial
bundle exec pod install
- set -o pipefail && xcodebuild -workspace Tutorial.xcworkspace -scheme Tutorial -destination platform\=iOS\ Simulator,OS\=13.3,name\=iPad\ Pro\ \(9.7-inch\) build test | xcpretty
+ set -o pipefail && xcodebuild -workspace Tutorial.xcworkspace -scheme Tutorial -destination platform\=iOS\ Simulator,OS\=13.4,name\=iPad\ Pro\ \(9.7-inch\) build test | xcpretty
documentation-lint:
runs-on: macOS-latest

diff:
@@ -70,9 +70,7 @@ extension AnyWorkflowConvertible where Output == Never {
.render(
context: context,
key: key,
- outputMap: { event -> AnyWorkflowAction<T> in
- switch event { }
- })
+ outputMap: { _ -> AnyWorkflowAction<T> in })
}
}

diff:
@@ -447,6 +447,7 @@ final class ConcurrencyTests: XCTestCase {
XCTAssertEqual(0, host.rendering.value.count)
struct SourceDifferentiatingWorkflow: Workflow {
+ typealias Output = Never
var step: Step
enum Step {


message: Add pip requirements file, update scripts

diff:
@@ -53,7 +53,7 @@ jobs:
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
- pip install mkdocs mkdocs-material mkdocs-redirects
+ pip install -r requirements.txt
# This environment variable step should be run after all 3rd-party actions to ensure nothing
# else accidentally overrides any of our special variables.

diff:
@@ -25,7 +25,7 @@ jobs:
- name: Upgrade pip
run: python -m pip install --upgrade pip
- name: Install dependencies
- run: pip install mkdocs-material mkdocs-redirects
+ run: pip install -r requirements.txt
- name: Run mkdocs
run: mkdocs build

diff:
@@ -150,11 +150,11 @@ an error about Catalyst and only empty READMEs will get generated.
#### mkdocs
-Mkdocs is written in Python, so you'll need Python and pip in order to run it. Assuming those are
+Mkdocs is written in Python, so you'll need Python 3 and pip in order to run it. Assuming those are
set up, run:
```bash
-pip install mkdocs mkdocs-material mkdocs-redirects
+pip install -r requirements.txt
```
Generate the site manually with:

diff:
# The website is built using MkDocs with the Material theme.
# https://squidfunk.github.io/mkdocs-material/
-# It requires Python to run.
+# It requires Python 3 to run.
# Install the packages with the following command:
-# pip install mkdocs mkdocs-material mkdocs-redirects
+# pip install -r requirements.txt
# Preview the site as you're editing it with:
# mkdocs serve
# It also uses CocoaPods and Sourcedocs to build the Swift docs.


message:
Remove context.subscribe
This has been replaced with SignalWorker.

diff:
@@ -185,9 +185,9 @@ extension DemoWorkflow {
subscribeTitle = "Subscribe"
case .subscribing:
// Subscribe to the timer signal, simulating the title being tapped.
- context.subscribe(signal: state.signal.signal.map({ _ -> Action in
+ context.awaitResult(for: state.signal.signal.asWorker(key: "Timer")) { _ -> Action in
return .titleButtonTapped
- }))
+ }
subscribeTitle = "Stop"
}

diff:
@@ -71,11 +71,6 @@ public class RenderContext<WorkflowType: Workflow>: RenderContextType {
fatalError()
}
- @available(*, deprecated, message: "Use a SignalWorker instead")
- public func subscribe<Action>(signal: Signal<Action, Never>) where Action : WorkflowAction, WorkflowType == Action.WorkflowType {
- fatalError()
- }
-
public func awaitResult<W, Action>(for worker: W, outputMap: @escaping (W.Output) -> Action) where W : Worker, Action : WorkflowAction, WorkflowType == Action.WorkflowType {
fatalError()
}
@@ -109,12 +104,6 @@ public class RenderContext<WorkflowType: Workflow>: RenderContextType {
return implementation.makeSink(of: actionType)
}
- override func subscribe<Action>(signal: Signal<Action, Never>) where WorkflowType == Action.WorkflowType, Action : WorkflowAction {
- assertStillValid()
- return implementation.subscribe(signal: signal)
- }
-
-
override func awaitResult<W, Action>(for worker: W, outputMap: @escaping (W.Output) -> Action) where W : Worker, Action : WorkflowAction, WorkflowType == Action.WorkflowType {
assertStillValid()
implementation.awaitResult(for: worker, outputMap: outputMap)
@@ -136,8 +125,6 @@ internal protocol RenderContextType: class {
func makeSink<Action>(of actionType: Action.Type) -> Sink<Action> where Action: WorkflowAction, Action.WorkflowType == WorkflowType
- func subscribe<Action>(signal: Signal<Action, Never>) where Action: WorkflowAction, Action.WorkflowType == WorkflowType
-
func awaitResult<W, Action>(for worker: W, outputMap: @escaping (W.Output) -> Action) where W: Worker, Action: WorkflowAction, Action.WorkflowType == WorkflowType
}

diff:
@@ -36,9 +36,6 @@ extension WorkflowNode {
/// The current array of workers
private (set) internal var childWorkers: [AnyChildWorker] = []
- /// Subscriptions from the outside world.
- private var subscriptions: Subscriptions = Subscriptions(eventSources: [], eventPipe: EventPipe())
-
init() {}
/// Performs an update pass using the given closure.
@@ -67,8 +64,6 @@ extension WorkflowNode {
/// as a result of this call to `render`.
self.childWorkflows = context.usedChildWorkflows
self.childWorkers = context.usedChildWorkers
- /// Merge all of the signals together from the subscriptions.
- self.subscriptions = Subscriptions(eventSources: context.eventSources, eventPipe: EventPipe())
/// Captured the reusable sinks from this render pass.
self.previousSinks = context.sinkStore.usedSinks
@@ -76,7 +71,6 @@ extension WorkflowNode {
/// Capture all the pipes to be enabled after render completes.
self.eventPipes = context.eventPipes
self.eventPipes.append(contentsOf: context.sinkStore.eventPipes)
- self.eventPipes.append(self.subscriptions.eventPipe)
/// Set all event pipes to `pending`.
self.eventPipes.forEach { $0.setPending() }
@@ -158,8 +152,6 @@ extension WorkflowNode.SubtreeManager {
private let originalChildWorkers: [AnyChildWorker]
private (set) internal var usedChildWorkers: [AnyChildWorker]
- private (set) internal var eventSources: [Signal<AnyWorkflowAction<WorkflowType>, Never>] = []
-
internal init(previousSinks: [ObjectIdentifier:AnyReusableSink], originalChildWorkflows: [ChildKey:AnyChildWorkflow], originalChildWorkers: [AnyChildWorker]) {
self.eventPipes = []
@@ -228,10 +220,6 @@ extension WorkflowNode.SubtreeManager {
return sink
}
- func subscribe<Action>(signal: Signal<Action, Never>) where Action : WorkflowAction, WorkflowType == Action.WorkflowType {
- eventSources.append(signal.map { AnyWorkflowAction($0) })
- }
-
func awaitResult<W, Action>(for worker: W, outputMap: @escaping (W.Output) -> Action) where W : Worker, Action : WorkflowAction, WorkflowType == Action.WorkflowType {
let outputMap = { AnyWorkflowAction(outputMap($0)) }
@@ -470,31 +458,6 @@ extension WorkflowNode.SubtreeManager {
}
-// MARK: - Subscriptions
-
-extension WorkflowNode.SubtreeManager {
- fileprivate final class Subscriptions {
- private var (lifetime, token) = Lifetime.make()
- private (set) internal var eventPipe: EventPipe
-
- init(eventSources: [Signal<AnyWorkflowAction<WorkflowType>, Never>], eventPipe: EventPipe) {
- self.eventPipe = eventPipe
-
- Signal
- .merge(eventSources)
- .map({ action -> Output in
- return Output.update(action, source: .external)
- })
- .observe(on: QueueScheduler.workflowExecution)
- .take(during: lifetime)
- .observeValues({ output in
- eventPipe.handle(event: output)
- })
- }
- }
-}
-
-
// MARK: - Child Workflows
extension WorkflowNode.SubtreeManager {

diff:
@@ -300,6 +300,43 @@ final class ConcurrencyTests: XCTestCase {
disposable?.dispose()
}
+ func test_allSubscriptionActionsAreApplied() {
+ let signal1 = TestSignal()
+ let signal2 = TestSignal()
+ let host = WorkflowHost(
+ workflow: TestWorkflow(
+ running: .doubleSubscribing(secondSignal: signal2),
+ signal: signal1
+ )
+ )
+
+ let renderingExpectation = XCTestExpectation()
+ let outputExpectation = XCTestExpectation()
+ let outDisposable = host.output.signal.observeValues { output in
+ outputExpectation.fulfill()
+ }
+
+ let disposable = host.rendering.signal.observeValues { rendering in
+ renderingExpectation.fulfill()
+ }
+
+ let screen = host.rendering.value
+
+ XCTAssertEqual(0, screen.count)
+
+ signal1.send(value: 1)
+ signal2.send(value: 2)
+
+ XCTAssertEqual(0, host.rendering.value.count)
+
+ wait(for: [renderingExpectation, outputExpectation], timeout: 1.0)
+
+ XCTAssertEqual(101, host.rendering.value.count)
+
+ disposable?.dispose()
+ outDisposable?.dispose()
+ }
+
// Workers are subscribed on a different scheduler than the UI scheduler,
// which means that if they fire immediately, the action will be received after
// `render` has completed.
@@ -549,6 +586,7 @@ final class ConcurrencyTests: XCTestCase {
enum Running {
case idle
case signal
+ case doubleSubscribing(secondSignal: TestSignal)
case worker
}
var signal: TestSignal
@@ -574,12 +612,16 @@ final class ConcurrencyTests: XCTestCase {
typealias WorkflowType = TestWorkflow
case update
+ case secondUpdate
func apply(toState state: inout State) -> Output? {
switch self {
case .update:
state.count += 1
return .emit
+ case .secondUpdate:
+ state.count += 100
+ return nil
}
}
}
@@ -592,9 +634,17 @@ final class ConcurrencyTests: XCTestCase {
case .idle:
break
case .signal:
- context.subscribe(signal: signal.signal.map({ _ -> Action in
+ context.awaitResult(for: signal.signal.asWorker(key: "signal1")) { _ -> Action in
+ return .update
+ }
+
+ case .doubleSubscribing(secondSignal: let signal2):
+ context.awaitResult(for: signal2.signal.asWorker(key: "signal2")) { _ -> Action in
+ return .secondUpdate
+ }
+ context.awaitResult(for: signal.signal.asWorker(key: "signal1")) { _ -> Action in
return .update
- }))
+ }
case .worker:
context.awaitResult(for: TestWorker())

diff:
@@ -241,9 +241,9 @@ final class SubtreeManagerTests: XCTestCase {
func render(state: SubscribingWorkflow.State, context: RenderContext<SubscribingWorkflow>) -> Bool {
if let signal = signal {
- context.subscribe(signal: signal.map({ _ -> AnyWorkflowAction<SubscribingWorkflow> in
+ context.awaitResult(for: signal.asWorker(key: "signal")) { _ -> AnyWorkflowAction<SubscribingWorkflow> in
return AnyWorkflowAction.noAction
- }))
+ }
return true
} else {
return false

diff:
@@ -270,16 +270,14 @@ fileprivate final class RenderTestContext<T: Workflow>: RenderContextType {
let sink = Sink<Action> { action in
observer.send(value: AnyWorkflowAction(action))
}
- subscribe(signal: signal)
- return sink
- }
- func subscribe<Action>(signal: Signal<Action, Never>) where Action : WorkflowAction, RenderTestContext<T>.WorkflowType == Action.WorkflowType {
signal
.take(during: lifetime)
.observeValues { [weak self] action in
self?.apply(action: action)
}
+
+ return sink
}
func awaitResult<W, Action>(for worker: W, outputMap: @escaping (W.Output) -> Action) where W : Worker, Action : WorkflowAction, RenderTestContext<T>.WorkflowType == Action.WorkflowType {

diff:
@@ -135,13 +135,12 @@ fileprivate struct MockWorkflow: Workflow {
}
func render(state: State, context: RenderContext<MockWorkflow>) -> TestScreen {
-
- context.subscribe(signal: subscription.map { output in
+ context.awaitResult(for: subscription.asWorker(key: "signal")) { output in
return AnyWorkflowAction { state in
state = output
return output
}
- })
+ }
return TestScreen(string: "\(state)")
}


message: Add os_signpost logs for render() and Workflow and Worker lifecycle

diff:
+/*
+ * 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 os.signpost
+
+extension OSLog {
+ static let workflow = OSLog(subsystem: "com.squareup.Workflow", category: "Workflow")
+ static let worker = OSLog(subsystem: "com.squareup.Workflow", category: "Worker")
+}

diff:
import Dispatch
import ReactiveSwift
+import os.signpost
extension WorkflowNode {
/// Manages the subtree of a workflow. Specifically, this type encapsulates the logic required to update and manage
@@ -204,7 +205,14 @@ extension WorkflowNode.SubtreeManager {
func makeSink<Action>(of actionType: Action.Type) -> Sink<Action> where Action: WorkflowAction, WorkflowType == Action.WorkflowType {
let reusableSink = sinkStore.findOrCreate(actionType: Action.self)
+ let signpostRef = NSObject()
+
let sink = Sink<Action> { action in
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: signpostRef)
+ os_signpost(.event, log: .workflow, name: "Sink Event", signpostID: signpostID, "Event: %@", String(describing: action))
+ }
+
reusableSink.handle(action: action)
}
@@ -407,11 +415,36 @@ extension WorkflowNode.SubtreeManager {
self.outputMap = outputMap
super.init(eventPipe: eventPipe)
+ let signpostRef = NSObject()
+
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .worker, object: signpostRef)
+ os_signpost(.begin, log: .worker, name: "Worker Created", signpostID: signpostID, "Worker: %{public}@", String(describing: W.self))
+ }
+
signalProducer
.take(during: lifetime)
.observe(on: QueueScheduler.workflowExecution)
- .startWithValues { [weak self] output in
+ .start { [weak self] event in
+ switch event {
+ case .value(let output):
self?.handle(output: output)
+ case .completed:
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .worker, object: signpostRef)
+ os_signpost(.end, log: .worker, name: "Worker Created", signpostID: signpostID)
+ }
+ case .interrupted:
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .worker, object: signpostRef)
+ os_signpost(.end, log: .worker, name: "Worker Created", signpostID: signpostID, "Interrupted")
+ }
+ case .failed:
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .worker, object: signpostRef)
+ os_signpost(.end, log: .worker, name: "Worker Created", signpostID: signpostID, "Failed")
+ }
+ }
}
}

diff:
*/
import ReactiveSwift
+import os.signpost
/// Manages a running workflow.
final class WorkflowNode<WorkflowType: Workflow> {
@@ -34,11 +35,23 @@ final class WorkflowNode<WorkflowType: Workflow> {
self.workflow = workflow
self.state = workflow.makeInitialState()
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: self)
+ os_signpost(.begin, log: .workflow, name: "Alive", signpostID: signpostID, "Workflow: %{public}@", String(describing: WorkflowType.self))
+ }
+
subtreeManager.onUpdate = { [weak self] output in
self?.handle(subtreeOutput: output)
}
}
+ deinit {
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: self)
+ os_signpost(.end, log: .workflow, name: "Alive", signpostID: signpostID)
+ }
+ }
+
/// Handles an event produced by the subtree manager
private func handle(subtreeOutput: SubtreeManager.Output) {
let output: Output
@@ -71,6 +84,18 @@ final class WorkflowNode<WorkflowType: Workflow> {
}
func render() -> WorkflowType.Rendering {
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: self)
+ os_signpost(.begin, log: .workflow, name: "Render", signpostID: signpostID, "Render Workflow: %{public}@", String(describing: WorkflowType.self))
+ }
+
+ defer {
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: self)
+ os_signpost(.end, log: .workflow, name: "Render", signpostID: signpostID)
+ }
+ }
+
return subtreeManager.render { context in
workflow
.render(


message: Rename 'Worker Created' to 'Running'

diff:
@@ -419,7 +419,7 @@ extension WorkflowNode.SubtreeManager {
if #available(iOS 12.0, *) {
let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.begin, log: .worker, name: "Worker Created", signpostID: signpostID, "Worker: %{public}@", String(describing: W.self))
+ os_signpost(.begin, log: .worker, name: "Running", signpostID: signpostID, "Worker: %{public}@", String(describing: W.self))
}
signalProducer
@@ -432,17 +432,17 @@ extension WorkflowNode.SubtreeManager {
case .completed:
if #available(iOS 12.0, *) {
let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.end, log: .worker, name: "Worker Created", signpostID: signpostID)
+ os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID)
}
case .interrupted:
if #available(iOS 12.0, *) {
let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.end, log: .worker, name: "Worker Created", signpostID: signpostID, "Interrupted")
+ os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID, "Interrupted")
}
case .failed:
if #available(iOS 12.0, *) {
let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.end, log: .worker, name: "Worker Created", signpostID: signpostID, "Failed")
+ os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID, "Failed")
}
}
}

original_message: Rename 'Worker Created' to 'Running'


message: Log Worker events

diff:
@@ -428,6 +428,11 @@ extension WorkflowNode.SubtreeManager {
.start { [weak self] event in
switch event {
case .value(let output):
+ if #available(iOS 12.0, *) {
+ let signpostID = OSSignpostID(log: .worker, object: signpostRef)
+ os_signpost(.event, log: .worker, name: "Worker Event", signpostID: signpostID, "Event: %@", String(describing: output))
+ }
+
self?.handle(output: output)
case .completed:
if #available(iOS 12.0, *) {

original_message: Log Worker events


message: Consolidate all the os_signpost calls

diff:
import os.signpost
-extension OSLog {
+fileprivate extension OSLog {
static let workflow = OSLog(subsystem: "com.squareup.Workflow", category: "Workflow")
static let worker = OSLog(subsystem: "com.squareup.Workflow", category: "Worker")
}
+
+// MARK: -
+
+final class WorkflowLogger {
+ // MARK: Workflows
+
+ static func logWorkflowStarted<WorkflowType>(ref: WorkflowNode<WorkflowType>) {
+ if #available(iOS 12.0, macOS 10.14, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: ref)
+ os_signpost(.begin, log: .workflow, name: "Alive", signpostID: signpostID,
+ "Workflow: %{public}@", String(describing: WorkflowType.self))
+ }
+ }
+
+ static func logWorkflowFinished<WorkflowType>(ref: WorkflowNode<WorkflowType>) {
+ if #available(iOS 12.0, macOS 10.14, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: ref)
+ os_signpost(.end, log: .workflow, name: "Alive", signpostID: signpostID)
+ }
+ }
+
+ static func logSinkEvent<Action>(ref: AnyObject, action: Action) {
+ if #available(iOS 12.0, macOS 10.14, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: ref)
+ os_signpost(.event, log: .workflow, name: "Sink Event", signpostID: signpostID,
+ "Event: %@", String(describing: action))
+ }
+ }
+
+ // MARK: Rendering
+
+ static func logWorkflowStartedRendering<WorkflowType>(ref: WorkflowNode<WorkflowType>) {
+ if #available(iOS 12.0, macOS 10.14, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: ref)
+ os_signpost(.begin, log: .workflow, name: "Render", signpostID: signpostID,
+ "Render Workflow: %{public}@", String(describing: WorkflowType.self))
+ }
+ }
+
+ static func logWorkflowFinishedRendering<WorkflowType>(ref: WorkflowNode<WorkflowType>) {
+ if #available(iOS 12.0, macOS 10.14, *) {
+ let signpostID = OSSignpostID(log: .workflow, object: ref)
+ os_signpost(.end, log: .workflow, name: "Render", signpostID: signpostID)
+ }
+ }
+
+ // MARK: - Workers
+
+ static func logWorkerStartedRunning<WorkerType>(ref: AnyObject, workerType: WorkerType.Type) {
+ if #available(iOS 12.0, macOS 10.14, *) {
+ let signpostID = OSSignpostID(log: .worker, object: ref)
+ os_signpost(.begin, log: .worker, name: "Running", signpostID: signpostID,
+ "Worker: %{public}@", String(describing: WorkerType.self))
+ }
+ }
+
+ static func logWorkerFinishedRunning(ref: AnyObject, status: StaticString? = nil) {
+ if #available(iOS 12.0, macOS 10.14, *) {
+ let signpostID = OSSignpostID(log: .worker, object: ref)
+ if let status = status {
+ os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID, status)
+ } else {
+ os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID)
+ }
+ }
+ }
+
+ static func logWorkerOutput<Output>(ref: AnyObject, output: Output) {
+ if #available(iOS 12.0, macOS 10.14, *) {
+ let signpostID = OSSignpostID(log: .worker, object: ref)
+ os_signpost(.event, log: .worker, name: "Worker Event", signpostID: signpostID,
+ "Event: %@", String(describing: output))
+ }
+ }
+}

diff:
import Dispatch
import ReactiveSwift
-import os.signpost
extension WorkflowNode {
/// Manages the subtree of a workflow. Specifically, this type encapsulates the logic required to update and manage
@@ -208,10 +207,7 @@ extension WorkflowNode.SubtreeManager {
let signpostRef = NSObject()
let sink = Sink<Action> { action in
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .workflow, object: signpostRef)
- os_signpost(.event, log: .workflow, name: "Sink Event", signpostID: signpostID, "Event: %@", String(describing: action))
- }
+ WorkflowLogger.logSinkEvent(ref: signpostRef, action: action)
reusableSink.handle(action: action)
}
@@ -416,11 +412,7 @@ extension WorkflowNode.SubtreeManager {
super.init(eventPipe: eventPipe)
let signpostRef = NSObject()
-
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.begin, log: .worker, name: "Running", signpostID: signpostID, "Worker: %{public}@", String(describing: W.self))
- }
+ WorkflowLogger.logWorkerStartedRunning(ref: signpostRef, workerType: W.self)
signalProducer
.take(during: lifetime)
@@ -428,27 +420,15 @@ extension WorkflowNode.SubtreeManager {
.start { [weak self] event in
switch event {
case .value(let output):
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.event, log: .worker, name: "Worker Event", signpostID: signpostID, "Event: %@", String(describing: output))
- }
+ WorkflowLogger.logWorkerOutput(ref: signpostRef, output: output)
self?.handle(output: output)
case .completed:
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID)
- }
+ WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef)
case .interrupted:
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID, "Interrupted")
- }
+ WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef, status: "Interrupted")
case .failed:
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .worker, object: signpostRef)
- os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID, "Failed")
- }
+ WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef, status: "Failed")
}
}
}

diff:
*/
import ReactiveSwift
-import os.signpost
/// Manages a running workflow.
final class WorkflowNode<WorkflowType: Workflow> {
@@ -35,10 +34,7 @@ final class WorkflowNode<WorkflowType: Workflow> {
self.workflow = workflow
self.state = workflow.makeInitialState()
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .workflow, object: self)
- os_signpost(.begin, log: .workflow, name: "Alive", signpostID: signpostID, "Workflow: %{public}@", String(describing: WorkflowType.self))
- }
+ WorkflowLogger.logWorkflowStarted(ref: self)
subtreeManager.onUpdate = { [weak self] output in
self?.handle(subtreeOutput: output)
@@ -46,10 +42,7 @@ final class WorkflowNode<WorkflowType: Workflow> {
}
deinit {
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .workflow, object: self)
- os_signpost(.end, log: .workflow, name: "Alive", signpostID: signpostID)
- }
+ WorkflowLogger.logWorkflowFinished(ref: self)
}
/// Handles an event produced by the subtree manager
@@ -84,16 +77,10 @@ final class WorkflowNode<WorkflowType: Workflow> {
}
func render() -> WorkflowType.Rendering {
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .workflow, object: self)
- os_signpost(.begin, log: .workflow, name: "Render", signpostID: signpostID, "Render Workflow: %{public}@", String(describing: WorkflowType.self))
- }
+ WorkflowLogger.logWorkflowStartedRendering(ref: self)
defer {
- if #available(iOS 12.0, *) {
- let signpostID = OSSignpostID(log: .workflow, object: self)
- os_signpost(.end, log: .workflow, name: "Render", signpostID: signpostID)
- }
+ WorkflowLogger.logWorkflowFinishedRendering(ref: self)
}
return subtreeManager.render { context in

original_message: Consolidate all the os_signpost calls


message: Change events to log the Workflow/Worker type instead of a description of the event itself

diff:
@@ -420,7 +420,7 @@ extension WorkflowNode.SubtreeManager {
.start { [weak self] event in
switch event {
case .value(let output):
- WorkflowLogger.logWorkerOutput(ref: signpostRef, output: output)
+ WorkflowLogger.logWorkerOutput(ref: signpostRef, workerType: W.self)
self?.handle(output: output)
case .completed:

diff:
@@ -41,11 +41,11 @@ final class WorkflowLogger {
}
}
- static func logSinkEvent<Action>(ref: AnyObject, action: Action) {
+ static func logSinkEvent<Action: WorkflowAction>(ref: AnyObject, action: Action) {
if #available(iOS 12.0, macOS 10.14, *) {
let signpostID = OSSignpostID(log: .workflow, object: ref)
os_signpost(.event, log: .workflow, name: "Sink Event", signpostID: signpostID,
- "Event: %@", String(describing: action))
+ "Event for workflow: %{public}@", String(describing: Action.WorkflowType.self))
}
}
@@ -87,11 +87,11 @@ final class WorkflowLogger {
}
}
- static func logWorkerOutput<Output>(ref: AnyObject, output: Output) {
+ static func logWorkerOutput<WorkerType: Worker>(ref: AnyObject, workerType: WorkerType.Type) {
if #available(iOS 12.0, macOS 10.14, *) {
let signpostID = OSSignpostID(log: .worker, object: ref)
os_signpost(.event, log: .worker, name: "Worker Event", signpostID: signpostID,
- "Event: %@", String(describing: output))
+ "Event: %{public}@", String(describing: WorkerType.self))
}
}
}


message: Require status for 'worker finished' logging

diff:
@@ -424,7 +424,7 @@ extension WorkflowNode.SubtreeManager {
self?.handle(output: output)
case .completed:
- WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef)
+ WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef, status: "Completed")
case .interrupted:
WorkflowLogger.logWorkerFinishedRunning(ref: signpostRef, status: "Interrupted")
case .failed:

diff:
@@ -76,14 +76,10 @@ final class WorkflowLogger {
}
}
- static func logWorkerFinishedRunning(ref: AnyObject, status: StaticString? = nil) {
+ static func logWorkerFinishedRunning(ref: AnyObject, status: StaticString) {
if #available(iOS 12.0, macOS 10.14, *) {
let signpostID = OSSignpostID(log: .worker, object: ref)
- if let status = status {
os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID, status)
- } else {
- os_signpost(.end, log: .worker, name: "Running", signpostID: signpostID)
- }
}
}


message: Created a class to be used as a signpost ref instead of NSObject

diff:
@@ -204,7 +204,7 @@ extension WorkflowNode.SubtreeManager {
func makeSink<Action>(of actionType: Action.Type) -> Sink<Action> where Action: WorkflowAction, WorkflowType == Action.WorkflowType {
let reusableSink = sinkStore.findOrCreate(actionType: Action.self)
- let signpostRef = NSObject()
+ let signpostRef = SignpostRef()
let sink = Sink<Action> { action in
WorkflowLogger.logSinkEvent(ref: signpostRef, action: action)
@@ -411,7 +411,7 @@ extension WorkflowNode.SubtreeManager {
self.outputMap = outputMap
super.init(eventPipe: eventPipe)
- let signpostRef = NSObject()
+ let signpostRef = SignpostRef()
WorkflowLogger.logWorkerStartedRunning(ref: signpostRef, workerType: W.self)
signalProducer

diff:
@@ -23,6 +23,11 @@ fileprivate extension OSLog {
// MARK: -
+/// Simple class that can be used to create signpost IDs based on an object pointer.
+final class SignpostRef {
+ init() {}
+}
+
final class WorkflowLogger {
// MARK: Workflows


diff:
@@ -163,7 +163,7 @@ extension WorkflowNode.SubtreeManager {
/// If the key already exists in `used`, than a workflow of the same type has been rendered multiple times
/// during this render pass with the same key. This is not allowed.
guard usedChildWorkflows[childKey] == nil else {
- fatalError("Child workflows of the same type must be given unique keys. Duplicate workflows of type \(Child.self) were encountered with the key \"\(key)\"")
+ fatalError("Child workflows of the same type must be given unique keys. Duplicate workflows of type \(Child.self) were encountered with the key \"\(key)\" in \(WorkflowType.self)")
}
let child: ChildWorkflow<Child>
@@ -316,19 +316,19 @@ extension WorkflowNode.SubtreeManager {
switch validationState {
case .preparing:
- fatalError("Sink sent an action inside `render`. Sinks are not valid until `render` has completed.")
+ fatalError("[\(WorkflowType.self)] Sink sent an action inside `render`. Sinks are not valid until `render` has completed.")
case .pending:
validationState = .queued(event)
case .queued:
- fatalError("Action sent to pipe while already in the `queueing` state.")
+ fatalError("[\(WorkflowType.self)] Action sent to pipe while already in the `queueing` state.")
case let .valid(handler: handler):
handler(event)
case .invalid:
- fatalError("Sink sent an action after it was invalidated. Sinks can only be used for a single valid `Rendering`.")
+ fatalError("[\(WorkflowType.self)] Sink sent an action after it was invalidated. Sinks can only be used for a single valid `Rendering`.")
}
}

original_message:
Annotate fatalError()s with Workflow type

Fixes #968


message:
Make BackStackScreen generic
Part of

diff:
import WorkflowUI
-public final class BackStackContainer: ScreenViewController<BackStackScreen>, UINavigationControllerDelegate {
+public final class BackStackContainer<Content: Screen>: ScreenViewController<BackStackScreen<Content>>, UINavigationControllerDelegate {
private let navController: UINavigationController
- public required init(screen: BackStackScreen, environment: ViewEnvironment) {
+ public required init(screen: BackStackScreen<Content>, environment: ViewEnvironment) {
self.navController = UINavigationController()
super.init(screen: screen, environment: environment)
@@ -42,7 +42,7 @@ public final class BackStackContainer: ScreenViewController<BackStackScreen>, UI
navController.view.frame = view.bounds
}
- override public func screenDidChange(from previousScreen: BackStackScreen, previousEnvironment: ViewEnvironment) {
+ override public func screenDidChange(from previousScreen: BackStackScreen<Content>, previousEnvironment: ViewEnvironment) {
update(with: screen)
}
@@ -54,9 +54,9 @@ public final class BackStackContainer: ScreenViewController<BackStackScreen>, UI
// MARK: - Private Methods
- private func update(with screen: BackStackScreen) {
- var existingViewControllers: [ScreenWrapperViewController] = navController.viewControllers as! [ScreenWrapperViewController]
- var updatedViewControllers: [ScreenWrapperViewController] = []
+ private func update(with screen: BackStackScreen<Content>) {
+ var existingViewControllers: [ScreenWrapperViewController<Content>] = navController.viewControllers as! [ScreenWrapperViewController<Content>]
+ var updatedViewControllers: [ScreenWrapperViewController<Content>] = []
for item in screen.items {
if let idx = existingViewControllers.firstIndex(where: { viewController -> Bool in
@@ -73,7 +73,7 @@ public final class BackStackContainer: ScreenViewController<BackStackScreen>, UI
navController.setViewControllers(updatedViewControllers, animated: true)
}
- private func setNavigationBarVisibility(with screen: BackStackScreen, animated: Bool) {
+ private func setNavigationBarVisibility(with screen: BackStackScreen<Content>, animated: Bool) {
guard let topScreen = screen.items.last else {
return
}

diff:
import WorkflowUI
-public struct BackStackScreen: Screen {
+public struct BackStackScreen<ScreenType: Screen>: Screen {
var items: [Item]
- public init(items: [BackStackScreen.Item]) {
+ public init(items: [Item]) {
self.items = items
}
@@ -32,13 +32,11 @@ extension BackStackScreen {
/// A specific item in the back stack. The key and screen type is used to differentiate reused vs replaced screens.
public struct Item {
public var key: AnyHashable
- public var screen: AnyScreen
- var screenType: Any.Type
+ public var screen: ScreenType
public var barVisibility: BarVisibility
- public init<ScreenType: Screen, Key: Hashable>(key: Key?, screen: ScreenType, barVisibility: BarVisibility) {
- self.screen = AnyScreen(screen)
- self.screenType = ScreenType.self
+ public init<Key: Hashable>(key: Key?, screen: ScreenType, barVisibility: BarVisibility) {
+ self.screen = screen
if let key = key {
self.key = AnyHashable(key)
@@ -48,26 +46,26 @@ extension BackStackScreen {
self.barVisibility = barVisibility
}
- public init<ScreenType: Screen>(screen: ScreenType, barVisibility: BarVisibility) {
+ public init(screen: ScreenType, barVisibility: BarVisibility) {
let key = Optional<AnyHashable>.none
self.init(key: key, screen: screen, barVisibility: barVisibility)
}
- public init<ScreenType: Screen, Key: Hashable>(key: Key?, screen: ScreenType, barContent: BackStackScreen.BarContent) {
+ public init<Key: Hashable>(key: Key?, screen: ScreenType, barContent: BackStackScreen.BarContent) {
self.init(key: key, screen: screen, barVisibility: .visible(barContent))
}
- public init<ScreenType: Screen>(screen: ScreenType, barContent: BackStackScreen.BarContent) {
+ public init(screen: ScreenType, barContent: BackStackScreen.BarContent) {
let key = Optional<AnyHashable>.none
self.init(key: key, screen: screen, barContent: barContent)
}
- public init<ScreenType: Screen, Key: Hashable>(key: Key?, screen: ScreenType) {
+ public init<Key: Hashable>(key: Key?, screen: ScreenType) {
let barVisibility: BarVisibility = .visible(BarContent())
self.init(key: key, screen: screen, barVisibility: barVisibility)
}
- public init<ScreenType: Screen>(screen: ScreenType) {
+ public init(screen: ScreenType) {
let key = Optional<AnyHashable>.none
self.init(key: key, screen: screen)
}

diff:
@@ -19,16 +19,14 @@ import WorkflowUI
/**
Wrapper view controller for being hosted in a backstack. Handles updating the bar button items.
*/
-final class ScreenWrapperViewController: UIViewController {
+final class ScreenWrapperViewController<ScreenType: Screen>: UIViewController {
let key: AnyHashable
- let screenType: Any.Type
let environment: ViewEnvironment
let contentViewController: DescribedViewController
- init(item: BackStackScreen.Item, environment: ViewEnvironment) {
+ init(item: BackStackScreen<ScreenType>.Item, environment: ViewEnvironment) {
self.key = item.key
- self.screenType = item.screenType
self.environment = environment
self.contentViewController = DescribedViewController(screen: item.screen, environment: environment)
@@ -53,17 +51,17 @@ final class ScreenWrapperViewController: UIViewController {
contentViewController.view.frame = view.bounds
}
- func update(item: BackStackScreen.Item, environment: ViewEnvironment) {
+ func update(item: BackStackScreen<ScreenType>.Item, environment: ViewEnvironment) {
contentViewController.update(screen: item.screen, environment: environment)
update(barVisibility: item.barVisibility)
}
- func matches(item: BackStackScreen.Item) -> Bool {
+ func matches(item: BackStackScreen<ScreenType>.Item) -> Bool {
return item.key == key
- && item.screenType == screenType
+ && type(of: item.screen) == ScreenType.self
}
- private func update(barVisibility: BackStackScreen.BarVisibility) {
+ private func update(barVisibility: BackStackScreen<ScreenType>.BarVisibility) {
navigationItem.setHidesBackButton(true, animated: false)
guard case let .visible(barContent) = barVisibility else {
@@ -113,10 +111,13 @@ final class ScreenWrapperViewController: UIViewController {
}
}
+// MARK: -
+
+extension ScreenWrapperViewController {
final class CallbackBarButtonItem: UIBarButtonItem {
var handler: () -> Void
- init(button: BackStackScreen.BarContent.Button) {
+ init(button: BackStackScreen<ScreenType>.BarContent.Button) {
self.handler = {}
super.init()
@@ -129,7 +130,7 @@ final class CallbackBarButtonItem: UIBarButtonItem {
fatalError("init(coder:) has not been implemented")
}
- func update(with button: BackStackScreen.BarContent.Button) {
+ func update(with button: BackStackScreen<ScreenType>.BarContent.Button) {
switch button.content {
case let .text(title):
self.title = title
@@ -145,3 +146,4 @@ final class CallbackBarButtonItem: UIBarButtonItem {
handler()
}
}
+}

diff:
@@ -161,12 +161,12 @@ extension AuthenticationWorkflow {
// MARK: Rendering
extension AuthenticationWorkflow {
- typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen>>
+ typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen<AnyScreen>>>
func render(state: AuthenticationWorkflow.State, context: RenderContext<AuthenticationWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)
- var backStackItems: [BackStackScreen.Item] = []
+ var backStackItems: [BackStackScreen<AnyScreen>.Item] = []
var modals: [ModalContainerScreenModal] = []
var alert: Alert?
@@ -176,7 +176,7 @@ extension AuthenticationWorkflow {
return .login(email: email, password: password)
}
}.rendered(with: context)
- backStackItems.append(BackStackScreen.Item(screen: loginScreen, barVisibility: .hidden))
+ backStackItems.append(BackStackScreen.Item(screen: AnyScreen(loginScreen), barVisibility: .hidden))
switch state {
case .emailPassword:
@@ -233,7 +233,7 @@ extension AuthenticationWorkflow {
)
}
- private func twoFactorScreen(error: AuthenticationService.AuthenticationError?, intermediateSession: String, sink: Sink<Action>) -> BackStackScreen.Item {
+ private func twoFactorScreen(error: AuthenticationService.AuthenticationError?, intermediateSession: String, sink: Sink<Action>) -> BackStackScreen<AnyScreen>.Item {
let title: String
if let authenticationError = error {
title = authenticationError.localizedDescription
@@ -252,7 +252,7 @@ extension AuthenticationWorkflow {
)
return BackStackScreen.Item(
- screen: twoFactorScreen,
+ screen: AnyScreen(twoFactorScreen),
barVisibility: .visible(BackStackScreen.BarContent(
leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button(
content: .text("Cancel"),

diff:
@@ -84,19 +84,19 @@ extension RunGameWorkflow {
// MARK: Rendering
extension RunGameWorkflow {
- typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen>>
+ typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen<AnyScreen>>>
func render(state: RunGameWorkflow.State, context: RenderContext<RunGameWorkflow>) -> Rendering {
let sink = context.makeSink(of: Action.self)
var modals: [ModalContainerScreenModal] = []
var alert: Alert?
- var backStackItems: [BackStackScreen.Item] = [BackStackScreen.Item(
- screen: newGameScreen(
+ var backStackItems: [BackStackScreen<AnyScreen>.Item] = [BackStackScreen.Item(
+ screen: AnyScreen(newGameScreen(
sink: sink,
playerX: state.playerX,
playerO: state.playerO
- ),
+ )),
barVisibility: .hidden
)]
@@ -111,7 +111,7 @@ extension RunGameWorkflow {
)
.rendered(with: context)
backStackItems.append(BackStackScreen.Item(
- screen: takeTurnsScreen,
+ screen: AnyScreen(takeTurnsScreen),
barVisibility: .visible(BackStackScreen.BarContent(
leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button(
content: .text("Quit"),
@@ -130,7 +130,7 @@ extension RunGameWorkflow {
)
.rendered(with: context)
backStackItems.append(BackStackScreen.Item(
- screen: takeTurnsScreen,
+ screen: AnyScreen(takeTurnsScreen),
barVisibility: .visible(BackStackScreen.BarContent(
leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button(
content: .text("Quit"),

diff:
@@ -65,7 +65,7 @@ extension MainWorkflow {
// MARK: Rendering
extension MainWorkflow {
- typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen>>
+ typealias Rendering = AlertContainerScreen<ModalContainerScreen<BackStackScreen<AnyScreen>>>
func render(state: MainWorkflow.State, context: RenderContext<MainWorkflow>) -> Rendering {
switch state {

original_message:
Make BackStackScreen generic

Part of #1046


message: Add Screen.asAnyScreen() convenience

diff:
@@ -176,7 +176,7 @@ extension AuthenticationWorkflow {
return .login(email: email, password: password)
}
}.rendered(with: context)
- backStackItems.append(BackStackScreen.Item(screen: AnyScreen(loginScreen), barVisibility: .hidden))
+ backStackItems.append(BackStackScreen.Item(screen: loginScreen.asAnyScreen(), barVisibility: .hidden))
switch state {
case .emailPassword:
@@ -252,7 +252,7 @@ extension AuthenticationWorkflow {
)
return BackStackScreen.Item(
- screen: AnyScreen(twoFactorScreen),
+ screen: twoFactorScreen.asAnyScreen(),
barVisibility: .visible(BackStackScreen.BarContent(
leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button(
content: .text("Cancel"),

diff:
@@ -92,11 +92,11 @@ extension RunGameWorkflow {
var alert: Alert?
var backStackItems: [BackStackScreen<AnyScreen>.Item] = [BackStackScreen.Item(
- screen: AnyScreen(newGameScreen(
+ screen: newGameScreen(
sink: sink,
playerX: state.playerX,
playerO: state.playerO
- )),
+ ).asAnyScreen(),
barVisibility: .hidden
)]
@@ -111,7 +111,7 @@ extension RunGameWorkflow {
)
.rendered(with: context)
backStackItems.append(BackStackScreen.Item(
- screen: AnyScreen(takeTurnsScreen),
+ screen: takeTurnsScreen.asAnyScreen(),
barVisibility: .visible(BackStackScreen.BarContent(
leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button(
content: .text("Quit"),
@@ -130,7 +130,7 @@ extension RunGameWorkflow {
)
.rendered(with: context)
backStackItems.append(BackStackScreen.Item(
- screen: AnyScreen(takeTurnsScreen),
+ screen: takeTurnsScreen.asAnyScreen(),
barVisibility: .visible(BackStackScreen.BarContent(
leftItem: BackStackScreen.BarContent.BarButtonItem.button(BackStackScreen.BarContent.Button(
content: .text("Quit"),

diff:
}
}
+ extension Screen {
+ /// Wraps the screen in an AnyScreen
+ public func asAnyScreen() -> AnyScreen {
+ AnyScreen(self)
+ }
+ }
+
#endif

original_message: Add Screen.asAnyScreen() convenience


message: Fix Tutorial code

diff:
@@ -86,14 +86,14 @@ extension RootWorkflow {
// MARK: Rendering
extension RootWorkflow {
- typealias Rendering = BackStackScreen
+ typealias Rendering = BackStackScreen<AnyScreen>
func render(state: RootWorkflow.State, context: RenderContext<RootWorkflow>) -> Rendering {
// Create a sink to handle the back action from the TodoListWorkflow to logout.
let sink = context.makeSink(of: Action.self)
// Our list of back stack items. Will always include the "WelcomeScreen".
- var backStackItems: [BackStackScreen.Item] = []
+ var backStackItems: [BackStackScreen<AnyScreen>.Item] = []
let welcomeScreen = WelcomeWorkflow()
.mapOutput { output -> Action in
@@ -107,7 +107,7 @@ extension RootWorkflow {
let welcomeBackStackItem = BackStackScreen.Item(
key: "welcome",
- screen: welcomeScreen,
+ screen: welcomeScreen.asAnyScreen(),
// Hide the navigation bar.
barVisibility: .hidden
)
@@ -129,9 +129,9 @@ extension RootWorkflow {
let todoListBackStackItem = BackStackScreen.Item(
key: "todoList",
- screen: todoListScreen,
+ screen: todoListScreen.asAnyScreen(),
// Specify the title, back button, and right button.
- barContent: BackStackScreen.BarContent(
+ barContent: .init(
title: "Welcome \(name)",
// When `back` is pressed, emit the .logout action to return to the welcome screen.
leftItem: .button(.back(handler: {

diff:
@@ -86,13 +86,13 @@ extension RootWorkflow {
// MARK: Rendering
extension RootWorkflow {
- typealias Rendering = BackStackScreen
+ typealias Rendering = BackStackScreen<AnyScreen>
func render(state: RootWorkflow.State, context: RenderContext<RootWorkflow>) -> Rendering {
// Delete the `let sink = context.makeSink(of: ...) as we no longer need a sink.
// Our list of back stack items. Will always include the "WelcomeScreen".
- var backStackItems: [BackStackScreen.Item] = []
+ var backStackItems: [BackStackScreen<AnyScreen>.Item] = []
let welcomeScreen = WelcomeWorkflow()
.mapOutput { output -> Action in
@@ -106,7 +106,7 @@ extension RootWorkflow {
let welcomeBackStackItem = BackStackScreen.Item(
key: "welcome",
- screen: welcomeScreen,
+ screen: welcomeScreen.asAnyScreen(),
// Hide the navigation bar.
barVisibility: .hidden
)

diff:
@@ -106,7 +106,7 @@ extension TodoEditWorkflow {
// MARK: Rendering
extension TodoEditWorkflow {
- typealias Rendering = BackStackScreen.Item
+ typealias Rendering = BackStackScreen<AnyScreen>.Item
func render(state: TodoEditWorkflow.State, context: RenderContext<TodoEditWorkflow>) -> Rendering {
// The sink is used to send actions back to this workflow.
@@ -125,13 +125,13 @@ extension TodoEditWorkflow {
let backStackItem = BackStackScreen.Item(
key: "edit",
- screen: todoEditScreen,
- barContent: BackStackScreen.BarContent(
+ screen: todoEditScreen.asAnyScreen(),
+ barContent: .init(
title: "Edit",
leftItem: .button(.back(handler: {
sink.send(.discardChanges)
})),
- rightItem: .button(BackStackScreen.BarContent.Button(
+ rightItem: .button(.init(
content: .text("Save"),
handler: {
sink.send(.saveChanges)

diff:
@@ -116,7 +116,7 @@ extension TodoListWorkflow {
// MARK: Rendering
extension TodoListWorkflow {
- typealias Rendering = [BackStackScreen.Item]
+ typealias Rendering = [BackStackScreen<AnyScreen>.Item]
func render(state: TodoListWorkflow.State, context: RenderContext<TodoListWorkflow>) -> Rendering {
// Define a sink to be able to send actions.
@@ -135,8 +135,8 @@ extension TodoListWorkflow {
let todoListItem = BackStackScreen.Item(
key: "list",
- screen: todoListScreen,
- barContent: BackStackScreen.BarContent(
+ screen: todoListScreen.asAnyScreen(),
+ barContent: .init(
title: "Welcome \(name)",
leftItem: .button(.back(handler: {
// When the left button is tapped, send the .onBack action.

diff:
@@ -93,7 +93,7 @@ extension TodoListWorkflow {
// MARK: Rendering
extension TodoListWorkflow {
- typealias Rendering = BackStackScreen.Item
+ typealias Rendering = BackStackScreen<AnyScreen>.Item
func render(state: TodoListWorkflow.State, context: RenderContext<TodoListWorkflow>) -> Rendering {
// Define a sink to be able to send the .onBack action.
@@ -112,14 +112,14 @@ extension TodoListWorkflow {
let todoListItem = BackStackScreen.Item(
key: "list",
- screen: todoListScreen,
- barContent: BackStackScreen.BarContent(
+ screen: todoListScreen.asAnyScreen(),
+ barContent: .init(
title: "Welcome \(name)",
leftItem: .button(.back(handler: {
// When the left button is tapped, send the .onBack action.
sink.send(.onBack)
})),
- rightItem: .button(BackStackScreen.BarContent.Button(
+ rightItem: .button(.init(
content: .text("New Todo"),
handler: {
sink.send(.new)

diff:
@@ -133,7 +133,7 @@ extension TodoWorkflow {
// MARK: Rendering
extension TodoWorkflow {
- typealias Rendering = [BackStackScreen.Item]
+ typealias Rendering = [BackStackScreen<AnyScreen>.Item]
func render(state: TodoWorkflow.State, context: RenderContext<TodoWorkflow>) -> Rendering {
let todoListItem = TodoListWorkflow(

diff:
import BackStackContainer
import WorkflowTesting
import XCTest
+import Workflow
@testable import Tutorial5
class TodoWorkflowTests: XCTestCase {
@@ -39,7 +40,7 @@ class TodoWorkflowTests: XCTestCase {
screen: TodoListScreen(
todoTitles: ["Title"],
onTodoSelected: { _ in }
- )),
+ ).asAnyScreen()),
// Simulate selecting the first todo:
output: TodoListWorkflow.Output.selectTodo(index: 0)
),
@@ -81,7 +82,7 @@ class TodoWorkflowTests: XCTestCase {
screen: TodoListScreen(
todoTitles: ["Title"],
onTodoSelected: { _ in }
- ))
+ ).asAnyScreen())
),
// Expect the TodoEditWorkflow. Additionally, simulate it emitting an output of ".save" to update the state.
ExpectedWorkflow(
@@ -91,7 +92,7 @@ class TodoWorkflowTests: XCTestCase {
note: "Note",
onTitleChanged: { _ in },
onNoteChanged: { _ in }
- )),
+ ).asAnyScreen()),
output: TodoEditWorkflow.Output.save(TodoModel(
title: "Updated Title",
note: "Updated Note"

original_message: Fix Tutorial code


message: Fix missing libswiftos.dylib

diff:
@@ -21,6 +21,7 @@ Pod::Spec.new do |s|
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'swift/Workflow/Tests/**/*.swift'
test_spec.framework = 'XCTest'
+ test_spec.library = 'swiftos'
end
end

original_message: Fix missing libswiftos.dylib


message: Fix missing libswiftos.dylib in WorkflowTesting and WorkflowUI

diff:
@@ -23,8 +23,7 @@ Pod::Spec.new do |s|
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'swift/WorkflowTesting/Tests/**/*.swift'
test_spec.framework = 'XCTest'
- test_spec.library = 'swiftDispatch'
- test_spec.library = 'swiftFoundation'
+ test_spec.libraries = 'swiftDispatch', 'swiftFoundation', 'swiftos'
end
-
end
+

diff:
@@ -21,6 +21,7 @@ Pod::Spec.new do |s|
s.test_spec 'Tests' do |test_spec|
test_spec.source_files = 'swift/WorkflowUI/Tests/**/*.swift'
test_spec.framework = 'XCTest'
+ test_spec.library = 'swiftos'
end
-
end
+


message: Upgrade to mkdocs-material to 5.2.2.

diff:
@@ -27,12 +27,15 @@ copyright: 'Copyright © 2019 Square, Inc.'
theme:
name: 'material'
logo: images/icon-square.png
- favicon: 'images/icon-square.png'
+ favicon: images/icon-square.png
+ icon:
+ repo: fontawesome/brands/github
palette:
primary: 'red'
accent: 'pink'
- feature:
- tabs: true
+ features:
+ - tabs
+ - instant
extra_css:
- 'css/app.css'
@@ -55,9 +58,11 @@ markdown_extensions:
- pymdownx.magiclink
- pymdownx.smartsymbols
- pymdownx.superfences
+ - pymdownx.tabbed
- tables
plugins:
+ - search
- redirects:
redirect_maps:
# Redirect some of the most-visited pages from their old locations in case there are links
@@ -71,13 +76,11 @@ plugins:
extra:
# type is the name of the FontAwesome icon without the fa- prefix.
social:
- - type: globe
- link: https://square.github.io/
- - type: github-alt
+ - icon: fontawesome/brands/github-alt
link: https://github.com/square
- - type: twitter
+ - icon: fontawesome/brands/twitter
link: https://twitter.com/squareeng
- - type: linkedin
+ - icon: fontawesome/brands/linkedin
link: https://www.linkedin.com/company/joinsquare/
nav:

diff:
-mkdocs==1.1
-mkdocs-material==4.6.3
+mkdocs==1.1.2
+mkdocs-material==5.2.2
mkdocs-redirects==1.0.0

original_message: Upgrade to mkdocs-material to 5.2.2.


message: Update URLs in podspecs

diff:
@@ -5,7 +5,7 @@ Pod::Spec.new do |s|
s.homepage = 'https://www.github.com/square/workflow'
s.license = 'Apache License, Version 2.0'
s.author = 'Square'
- s.source = { :git => 'https://github.com/square/workflow.git', :tag => "v#{s.version}" }
+ s.source = { :git => 'https://github.com/square/workflow-swift.git', :tag => "v#{s.version}" }
# 1.7 is needed for `swift_versions` support
s.cocoapods_version = '>= 1.7.0'

original_message: Update URLs in podspecs