Skip to content

Commit f2def8f

Browse files
committed
Vend preferredContentSize through the DescribedViewController
Because the `DescribedViewController` wraps all view controllers used in workflows, if it does not vend its current child's `preferredContentSize`, it will be lost. This makes it very difficult to create dynamic workflows that rely on self-sizing view controllers without introducing workarounds to have the children communicate their intended size to the parent, or by removing the convenience of the `DescribedViewController` entirely. This change solves the issue by having the `DescribedViewController` update its own `preferredContentSize` to match whatever the `currentViewController` is, and when that child's size changes.
1 parent 86b48ea commit f2def8f

File tree

2 files changed

+330
-0
lines changed

2 files changed

+330
-0
lines changed

swift/WorkflowUI/Sources/ViewControllerDescription/DescribedViewController.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ public final class DescribedViewController: UIViewController {
5252
view.addSubview(currentViewController.view)
5353
currentViewController.view.frame = view.bounds
5454
currentViewController.didMove(toParent: self)
55+
preferredContentSize = currentViewController.preferredContentSize
5556
}
5657
}
5758
}
@@ -66,6 +67,7 @@ public final class DescribedViewController: UIViewController {
6667
addChild(currentViewController)
6768
view.addSubview(currentViewController.view)
6869
currentViewController.didMove(toParent: self)
70+
preferredContentSize = currentViewController.preferredContentSize
6971
}
7072

7173
public override func viewDidLayoutSubviews() {
@@ -93,6 +95,16 @@ public final class DescribedViewController: UIViewController {
9395
return currentViewController.supportedInterfaceOrientations
9496
}
9597

98+
public override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) {
99+
super.preferredContentSizeDidChange(forChildContentContainer: container)
100+
101+
guard
102+
(container as? UIViewController) == currentViewController,
103+
container.preferredContentSize != preferredContentSize
104+
else { return }
105+
106+
preferredContentSize = container.preferredContentSize
107+
}
96108
}
97109

98110
#endif
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
//
2+
// DescribedViewControllerTests.swift
3+
// WorkflowUITests
4+
//
5+
// Created by Zachary Radke on 3/30/20.
6+
//
7+
8+
#if canImport(UIKit)
9+
10+
import XCTest
11+
12+
import ReactiveSwift
13+
import Workflow
14+
@testable import WorkflowUI
15+
16+
fileprivate enum TestScreen: Screen, Equatable {
17+
case counter(Int)
18+
case message(String)
19+
20+
func viewControllerDescription(environment: ViewEnvironment) -> ViewControllerDescription {
21+
switch self {
22+
case let .counter(count):
23+
return ViewControllerDescription(
24+
build: { CounterViewController(count: count) },
25+
update: { $0.count = count }
26+
)
27+
28+
case let .message(message):
29+
return ViewControllerDescription(
30+
build: { MessageViewController(message: message) },
31+
update: { $0.message = message }
32+
)
33+
}
34+
}
35+
}
36+
37+
fileprivate class ContainerViewController: UIViewController {
38+
let describedViewController: DescribedViewController
39+
40+
var preferredContentSizeSignal: Signal<CGSize, Never> { return signal.skipRepeats() }
41+
42+
private let (signal, sink) = Signal<CGSize, Never>.pipe()
43+
44+
init(describedViewController: DescribedViewController) {
45+
self.describedViewController = describedViewController
46+
super.init(nibName: nil, bundle: nil)
47+
}
48+
49+
@available(*, unavailable) required init?(coder: NSCoder) {
50+
fatalError("init(coder:) has not been implemented")
51+
}
52+
53+
override func viewDidLoad() {
54+
super.viewDidLoad()
55+
56+
addChild(describedViewController)
57+
describedViewController.view.frame = view.bounds
58+
describedViewController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
59+
view.addSubview(describedViewController.view)
60+
describedViewController.didMove(toParent: self)
61+
}
62+
63+
override func preferredContentSizeDidChange(forChildContentContainer container: UIContentContainer) {
64+
guard container === describedViewController else { return }
65+
66+
sink.send(value: container.preferredContentSize)
67+
}
68+
}
69+
70+
fileprivate class CounterViewController: UIViewController {
71+
var count: Int {
72+
didSet {
73+
preferredContentSize.width = CGFloat(count * 10)
74+
}
75+
}
76+
77+
init(count: Int) {
78+
self.count = count
79+
super.init(nibName: nil, bundle: nil)
80+
preferredContentSize.width = CGFloat(count * 10)
81+
}
82+
83+
@available(*, unavailable) required init?(coder: NSCoder) {
84+
fatalError("init(coder:) has not been implemented")
85+
}
86+
}
87+
88+
fileprivate class MessageViewController: UIViewController {
89+
var message: String {
90+
didSet {
91+
preferredContentSize.width = CGFloat(message.count * 10)
92+
}
93+
}
94+
95+
init(message: String) {
96+
self.message = message
97+
super.init(nibName: nil, bundle: nil)
98+
preferredContentSize.width = CGFloat(message.count * 10)
99+
}
100+
101+
@available(*, unavailable) required init?(coder: NSCoder) {
102+
fatalError("init(coder:) has not been implemented")
103+
}
104+
}
105+
106+
class DescribedViewControllerTests: XCTestCase {
107+
func test_init() {
108+
// Given
109+
let screen = TestScreen.counter(0)
110+
111+
// When
112+
let describedViewController = DescribedViewController(screen: screen, environment: .empty)
113+
114+
// Then
115+
guard
116+
let currentViewController = describedViewController.currentViewController as? CounterViewController
117+
else {
118+
XCTFail("Expected a \(String(reflecting: CounterViewController.self)), but got: \(describedViewController.currentViewController)")
119+
return
120+
}
121+
122+
XCTAssertEqual(currentViewController.count, 0)
123+
XCTAssertFalse(describedViewController.isViewLoaded)
124+
XCTAssertFalse(currentViewController.isViewLoaded)
125+
XCTAssertNil(currentViewController.parent)
126+
}
127+
128+
func test_viewDidLoad() {
129+
// Given
130+
let screen = TestScreen.counter(0)
131+
let describedViewController = DescribedViewController(screen: screen, environment: .empty)
132+
133+
// When
134+
_ = describedViewController.view
135+
136+
// Then
137+
XCTAssertEqual(describedViewController.currentViewController.parent, describedViewController)
138+
XCTAssertNotNil(describedViewController.currentViewController.viewIfLoaded?.superview)
139+
}
140+
141+
func test_update_toCompatibleDescription_beforeViewLoads() {
142+
// Given
143+
let screenA = TestScreen.counter(0)
144+
let screenB = TestScreen.counter(1)
145+
146+
let describedViewController = DescribedViewController(screen: screenA, environment: .empty)
147+
let initialChildViewController = describedViewController.currentViewController
148+
149+
// When
150+
describedViewController.update(screen: screenB, environment: .empty)
151+
152+
// Then
153+
XCTAssertEqual(initialChildViewController, describedViewController.currentViewController)
154+
XCTAssertEqual((describedViewController.currentViewController as? CounterViewController)?.count, 1)
155+
XCTAssertFalse(describedViewController.isViewLoaded)
156+
XCTAssertFalse(describedViewController.currentViewController.isViewLoaded)
157+
XCTAssertNil(describedViewController.currentViewController.parent)
158+
}
159+
160+
func test_update_toCompatibleDescription_afterViewLoads() {
161+
// Given
162+
let screenA = TestScreen.counter(0)
163+
let screenB = TestScreen.counter(1)
164+
165+
let describedViewController = DescribedViewController(screen: screenA, environment: .empty)
166+
let initialChildViewController = describedViewController.currentViewController
167+
168+
// When
169+
_ = describedViewController.view
170+
describedViewController.update(screen: screenB, environment: .empty)
171+
172+
// Then
173+
XCTAssertEqual(initialChildViewController, describedViewController.currentViewController)
174+
XCTAssertEqual((describedViewController.currentViewController as? CounterViewController)?.count, 1)
175+
}
176+
177+
func test_update_toIncompatibleDescription_beforeViewLoads() {
178+
// Given
179+
let screenA = TestScreen.counter(0)
180+
let screenB = TestScreen.message("Test")
181+
182+
let describedViewController = DescribedViewController(screen: screenA, environment: .empty)
183+
let initialChildViewController = describedViewController.currentViewController
184+
185+
// When
186+
describedViewController.update(screen: screenB, environment: .empty)
187+
188+
// Then
189+
XCTAssertNotEqual(initialChildViewController, describedViewController.currentViewController)
190+
XCTAssertEqual((describedViewController.currentViewController as? MessageViewController)?.message, "Test")
191+
XCTAssertFalse(describedViewController.isViewLoaded)
192+
XCTAssertFalse(describedViewController.currentViewController.isViewLoaded)
193+
XCTAssertNil(describedViewController.currentViewController.parent)
194+
}
195+
196+
func test_update_toIncompatibleDescription_afterViewLoads() {
197+
// Given
198+
let screenA = TestScreen.counter(0)
199+
let screenB = TestScreen.message("Test")
200+
201+
let describedViewController = DescribedViewController(screen: screenA, environment: .empty)
202+
let initialChildViewController = describedViewController.currentViewController
203+
204+
// When
205+
_ = describedViewController.view
206+
describedViewController.update(screen: screenB, environment: .empty)
207+
208+
// Then
209+
XCTAssertNotEqual(initialChildViewController, describedViewController.currentViewController)
210+
XCTAssertEqual((describedViewController.currentViewController as? MessageViewController)?.message, "Test")
211+
XCTAssertNil(initialChildViewController.parent)
212+
XCTAssertEqual(describedViewController.currentViewController.parent, describedViewController)
213+
XCTAssertNil(initialChildViewController.viewIfLoaded?.superview)
214+
XCTAssertNotNil(describedViewController.currentViewController.viewIfLoaded?.superview)
215+
}
216+
217+
func test_childViewControllerFor() {
218+
// Given
219+
let screen = TestScreen.counter(0)
220+
221+
let describedViewController = DescribedViewController(screen: screen, environment: .empty)
222+
let currentViewController = describedViewController.currentViewController
223+
224+
// When, Then
225+
XCTAssertEqual(describedViewController.childForStatusBarStyle, currentViewController)
226+
XCTAssertEqual(describedViewController.childForStatusBarHidden, currentViewController)
227+
XCTAssertEqual(describedViewController.childForHomeIndicatorAutoHidden, currentViewController)
228+
XCTAssertEqual(describedViewController.childForScreenEdgesDeferringSystemGestures, currentViewController)
229+
XCTAssertEqual(describedViewController.supportedInterfaceOrientations, currentViewController.supportedInterfaceOrientations)
230+
}
231+
232+
func test_childViewControllerFor_afterIncompatibleUpdate() {
233+
// Given
234+
let screenA = TestScreen.counter(0)
235+
let screenB = TestScreen.message("Test")
236+
237+
let describedViewController = DescribedViewController(screen: screenA, environment: .empty)
238+
let initialChildViewController = describedViewController.currentViewController
239+
240+
describedViewController.update(screen: screenB, environment: .empty)
241+
let currentViewController = describedViewController.currentViewController
242+
243+
// When, Then
244+
XCTAssertNotEqual(initialChildViewController, currentViewController)
245+
XCTAssertEqual(describedViewController.childForStatusBarStyle, currentViewController)
246+
XCTAssertEqual(describedViewController.childForStatusBarHidden, currentViewController)
247+
XCTAssertEqual(describedViewController.childForHomeIndicatorAutoHidden, currentViewController)
248+
XCTAssertEqual(describedViewController.childForScreenEdgesDeferringSystemGestures, currentViewController)
249+
XCTAssertEqual(describedViewController.supportedInterfaceOrientations, currentViewController.supportedInterfaceOrientations)
250+
}
251+
252+
func test_preferredContentSizeDidChange() {
253+
// Given
254+
let screenA = TestScreen.counter(1)
255+
let screenB = TestScreen.counter(2)
256+
257+
let describedViewController = DescribedViewController(screen: screenA, environment: .empty)
258+
let containerViewController = ContainerViewController(describedViewController: describedViewController)
259+
260+
// When
261+
let expectation = self.expectation(description: "did observe size changes")
262+
expectation.expectedFulfillmentCount = 2
263+
264+
var observedSizes: [CGSize] = []
265+
let disposable = containerViewController.preferredContentSizeSignal.observeValues {
266+
observedSizes.append($0)
267+
expectation.fulfill()
268+
}
269+
270+
defer { disposable?.dispose() }
271+
272+
_ = containerViewController.view
273+
describedViewController.update(screen: screenB, environment: .empty)
274+
275+
// Then
276+
let expectedSizes = [CGSize(width: 10, height: 0), CGSize(width: 20, height: 0)]
277+
waitForExpectations(timeout: 1, handler: nil)
278+
XCTAssertEqual(observedSizes, expectedSizes)
279+
}
280+
281+
func test_preferredContentSizeDidChange_afterIncompatibleUpdate() {
282+
// Given
283+
let screenA = TestScreen.counter(1)
284+
let screenB = TestScreen.message("Test")
285+
let screenC = TestScreen.message("Testing")
286+
287+
let describedViewController = DescribedViewController(screen: screenA, environment: .empty)
288+
let containerViewController = ContainerViewController(describedViewController: describedViewController)
289+
290+
// When
291+
let expectation = self.expectation(description: "did observe size changes")
292+
expectation.expectedFulfillmentCount = 3
293+
294+
var observedSizes: [CGSize] = []
295+
let disposable = containerViewController.preferredContentSizeSignal.observeValues {
296+
observedSizes.append($0)
297+
expectation.fulfill()
298+
}
299+
300+
defer { disposable?.dispose() }
301+
302+
_ = containerViewController.view
303+
describedViewController.update(screen: screenB, environment: .empty)
304+
describedViewController.update(screen: screenC, environment: .empty)
305+
306+
// Then
307+
let expectedSizes = [
308+
CGSize(width: 10, height: 0),
309+
CGSize(width: 40, height: 0),
310+
CGSize(width: 70, height: 0),
311+
]
312+
313+
waitForExpectations(timeout: 1, handler: nil)
314+
XCTAssertEqual(observedSizes, expectedSizes)
315+
}
316+
}
317+
318+
#endif

0 commit comments

Comments
 (0)