Skip to content

Commit 10cec95

Browse files
committed
Allow wrapping screens to guarantee a described view controller
1 parent edb6f17 commit 10cec95

File tree

4 files changed

+318
-31
lines changed

4 files changed

+318
-31
lines changed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright 2021 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
#if canImport(UIKit)
18+
19+
import Foundation
20+
21+
///
22+
///
23+
public struct AnyContentScreen: Screen {
24+
public var transition: ViewTransition
25+
public let content: AnyScreen
26+
27+
public init<ScreenType: Screen>(
28+
transition: ViewTransition = .fade(),
29+
content: () -> ScreenType
30+
) {
31+
let content = content()
32+
33+
if let content = content as? Self {
34+
self = content
35+
} else {
36+
self.content = content.asAnyScreen()
37+
}
38+
39+
self.transition = transition
40+
}
41+
42+
// MARK: Screen
43+
44+
public func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
45+
let description = content.viewControllerDescription(environment: environment)
46+
47+
return ViewControllerDescription(
48+
/// The inner `DescribedViewController` will respect `performInitialUpdate` from
49+
/// the nested screen – so our value should always be false.
50+
performInitialUpdate: false,
51+
transition: transition,
52+
type: DescribedViewController.self,
53+
build: {
54+
DescribedViewController(description: description)
55+
},
56+
update: { vc in
57+
vc.update(description: description, animated: true)
58+
}
59+
)
60+
}
61+
}
62+
63+
#endif

WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift

Lines changed: 72 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,17 @@
1818

1919
import UIKit
2020

21+
/// Displays the backing `ViewControllerDescription` for a given `Screen`.
22+
///
2123
public final class DescribedViewController: UIViewController {
22-
var currentViewController: UIViewController
24+
var content: UIViewController
2325

2426
public init(description: ViewControllerDescription) {
25-
self.currentViewController = description.buildViewController()
27+
self.content = description.buildViewController()
2628
super.init(nibName: nil, bundle: nil)
2729

28-
addChild(currentViewController)
29-
currentViewController.didMove(toParent: self)
30+
addChild(content)
31+
content.didMove(toParent: self)
3032
}
3133

3234
public convenience init<S: Screen>(screen: S, environment: ViewEnvironment) {
@@ -38,93 +40,132 @@
3840
fatalError("init(coder:) is unavailable")
3941
}
4042

41-
public func update(description: ViewControllerDescription) {
42-
if description.canUpdate(viewController: currentViewController) {
43-
description.update(viewController: currentViewController)
43+
public func update(description: ViewControllerDescription, animated: Bool = false) {
44+
if description.canUpdate(viewController: content) {
45+
description.update(viewController: content)
4446
} else {
45-
currentViewController.willMove(toParent: nil)
46-
currentViewController.viewIfLoaded?.removeFromSuperview()
47-
currentViewController.removeFromParent()
47+
let old = content
48+
let new = description.buildViewController()
4849

49-
currentViewController = description.buildViewController()
50-
51-
addChild(currentViewController)
50+
content = new
5251

5352
if isViewLoaded {
54-
currentViewController.view.frame = view.bounds
55-
view.addSubview(currentViewController.view)
56-
updatePreferredContentSizeIfNeeded()
53+
let animated = animated && view.window != nil
54+
55+
addChild(new)
56+
old.willMove(toParent: nil)
57+
58+
description.transition.transition(
59+
from: old.view,
60+
to: new.view,
61+
in: view,
62+
animated: animated,
63+
setup: {
64+
self.view.addSubview(new.view)
65+
},
66+
completion: {
67+
new.didMove(toParent: self)
68+
69+
old.view.removeFromSuperview()
70+
old.removeFromParent()
71+
72+
self.currentViewControllerChanged()
73+
}
74+
)
75+
76+
} else {
77+
addChild(new)
78+
new.didMove(toParent: self)
79+
80+
old.willMove(toParent: nil)
81+
old.removeFromParent()
5782
}
5883

59-
currentViewController.didMove(toParent: self)
60-
6184
updatePreferredContentSizeIfNeeded()
6285
}
6386
}
6487

6588
public func update<S: Screen>(screen: S, environment: ViewEnvironment) {
66-
update(description: screen.viewControllerDescription(environment: environment))
89+
if let screen = screen as? AnyContentScreen {
90+
update(description: screen.content.viewControllerDescription(environment: environment))
91+
} else {
92+
update(description: screen.viewControllerDescription(environment: environment))
93+
}
6794
}
6895

6996
override public func viewDidLoad() {
7097
super.viewDidLoad()
7198

72-
currentViewController.view.frame = view.bounds
73-
view.addSubview(currentViewController.view)
99+
content.view.frame = view.bounds
100+
view.addSubview(content.view)
74101

75102
updatePreferredContentSizeIfNeeded()
76103
}
77104

78105
override public func viewDidLayoutSubviews() {
79106
super.viewDidLayoutSubviews()
80-
currentViewController.view.frame = view.bounds
107+
content.view.frame = view.bounds
81108
}
82109

83110
override public var childForStatusBarStyle: UIViewController? {
84-
return currentViewController
111+
return content
85112
}
86113

87114
override public var childForStatusBarHidden: UIViewController? {
88-
return currentViewController
115+
return content
89116
}
90117

91118
override public var childForHomeIndicatorAutoHidden: UIViewController? {
92-
return currentViewController
119+
return content
93120
}
94121

95122
override public var childForScreenEdgesDeferringSystemGestures: UIViewController? {
96-
return currentViewController
123+
return content
97124
}
98125

99126
override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
100-
return currentViewController.supportedInterfaceOrientations
127+
return content.supportedInterfaceOrientations
101128
}
102129

103130
override public var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
104-
return currentViewController.preferredStatusBarUpdateAnimation
131+
return content.preferredStatusBarUpdateAnimation
105132
}
106133

107134
@available(iOS 14.0, *)
108135
override public var childViewControllerForPointerLock: UIViewController? {
109-
return currentViewController
136+
return content
110137
}
111138

112139
override public func preferredContentSizeDidChange(
113140
forChildContentContainer container: UIContentContainer
114141
) {
115142
super.preferredContentSizeDidChange(forChildContentContainer: container)
116143

117-
guard container === currentViewController else { return }
144+
guard container === content else { return }
118145

119146
updatePreferredContentSizeIfNeeded()
120147
}
121148

122149
private func updatePreferredContentSizeIfNeeded() {
123-
let newPreferredContentSize = currentViewController.preferredContentSize
150+
let newPreferredContentSize = content.preferredContentSize
124151

125152
guard newPreferredContentSize != preferredContentSize else { return }
126153

127154
preferredContentSize = newPreferredContentSize
128155
}
156+
157+
private func currentViewControllerChanged() {
158+
setNeedsFocusUpdate()
159+
setNeedsUpdateOfHomeIndicatorAutoHidden()
160+
161+
if #available(iOS 14.0, *) {
162+
self.setNeedsUpdateOfPrefersPointerLocked()
163+
}
164+
165+
setNeedsUpdateOfScreenEdgesDeferringSystemGestures()
166+
setNeedsStatusBarAppearanceUpdate()
167+
168+
UIAccessibility.post(notification: .screenChanged, argument: nil)
169+
}
129170
}
130171
#endif

WorkflowUI/Sources/ViewControllerDescription/ViewControllerDescription.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
/// duplicate updates to your children if they are created in `init`.
4444
public var performInitialUpdate: Bool
4545

46+
public var transition: ViewTransition
47+
4648
/// Describes the `UIViewController` type that backs the `ViewControllerDescription`
4749
/// in a way that is `Equatable` and `Hashable`. When implementing view controller
4850
/// updating and diffing, you can use this type to identify if the backing view controller
@@ -69,11 +71,13 @@
6971
/// - update: Closure that updates the given view controller
7072
public init<VC: UIViewController>(
7173
performInitialUpdate: Bool = true,
74+
transition: ViewTransition = .none,
7275
type: VC.Type = VC.self,
7376
build: @escaping () -> VC,
7477
update: @escaping (VC) -> Void
7578
) {
7679
self.performInitialUpdate = performInitialUpdate
80+
self.transition = transition
7781

7882
self.kind = .init(VC.self)
7983

0 commit comments

Comments
 (0)