Skip to content

Commit ddbe355

Browse files
authored
Merge pull request #1051 from zradke/zradke/preferred-content-size
Vend `preferredContentSize` through the `DescribedViewController`
2 parents b029988 + 889a198 commit ddbe355

File tree

2 files changed

+346
-0
lines changed

2 files changed

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

0 commit comments

Comments
 (0)