Skip to content

Commit 14ffc04

Browse files
authoredMar 28, 2024··
[feat]: add support for sizingOptions to SwiftUIScreen (#277)
1 parent 0b81d19 commit 14ffc04

File tree

4 files changed

+190
-3
lines changed

4 files changed

+190
-3
lines changed
 

‎Development.podspec

+6
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ Pod::Spec.new do |s|
109109
test_spec.framework = 'XCTest'
110110
end
111111

112+
s.test_spec 'WorkflowSwiftUIExperimentalTests' do |test_spec|
113+
test_spec.requires_app_host = true
114+
test_spec.source_files = 'WorkflowSwiftUIExperimental/Tests/**/*.swift'
115+
test_spec.framework = 'XCTest'
116+
end
117+
112118
s.test_spec 'WorkflowTests' do |test_spec|
113119
test_spec.requires_app_host = true
114120
test_spec.source_files = 'Workflow/Tests/**/*.swift'

‎WorkflowSwiftUIExperimental.podspec

+12
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,16 @@ Pod::Spec.new do |s|
2222
s.dependency 'WorkflowUI', "~> #{WORKFLOW_MAJOR_VERSION}.0"
2323

2424
s.pod_target_xcconfig = { 'APPLICATION_EXTENSION_API_ONLY' => 'YES' }
25+
26+
s.test_spec 'Tests' do |test_spec|
27+
test_spec.source_files = 'WorkflowSwiftUIExperimental/Tests/**/*.swift'
28+
test_spec.framework = 'XCTest'
29+
test_spec.library = 'swiftos'
30+
31+
# Create an app host so that we can host
32+
# view or view controller based tests in a real environment.
33+
test_spec.requires_app_host = true
34+
35+
test_spec.pod_target_xcconfig = { 'APPLICATION_EXTENSION_API_ONLY' => 'NO' }
36+
end
2537
end

‎WorkflowSwiftUIExperimental/Sources/SwiftUIScreen.swift

+108-3
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,20 @@ import WorkflowUI
2323
public protocol SwiftUIScreen: Screen {
2424
associatedtype Content: View
2525

26+
var sizingOptions: SwiftUIScreenSizingOptions { get }
27+
2628
@ViewBuilder
2729
static func makeView(model: ObservableValue<Self>) -> Content
2830

2931
static var isEquivalent: ((Self, Self) -> Bool)? { get }
3032
}
3133

3234
public extension SwiftUIScreen {
33-
static var isEquivalent: ((Self, Self) -> Bool)? { return nil }
35+
var sizingOptions: SwiftUIScreenSizingOptions { [] }
36+
}
37+
38+
public extension SwiftUIScreen {
39+
static var isEquivalent: ((Self, Self) -> Bool)? { nil }
3440
}
3541

3642
public extension SwiftUIScreen where Self: Equatable {
@@ -53,17 +59,29 @@ public extension SwiftUIScreen {
5359
viewEnvironment: viewEnvironment,
5460
content: Self.makeView(model: model)
5561
)
56-
})
62+
}),
63+
swiftUIScreenSizingOptions: sizingOptions
5764
)
5865
},
5966
update: {
6067
$0.modelSink.send(self)
6168
$0.viewEnvironmentSink.send(environment)
69+
$0.swiftUIScreenSizingOptions = sizingOptions
6270
}
6371
)
6472
}
6573
}
6674

75+
public struct SwiftUIScreenSizingOptions: OptionSet {
76+
public let rawValue: Int
77+
78+
public init(rawValue: Int) {
79+
self.rawValue = rawValue
80+
}
81+
82+
public static let preferredContentSize: SwiftUIScreenSizingOptions = .init(rawValue: 1 << 0)
83+
}
84+
6785
private struct EnvironmentInjectingView<Content: View>: View {
6886
@ObservedObject var viewEnvironment: ObservableValue<ViewEnvironment>
6987
let content: Content
@@ -77,17 +95,104 @@ private struct EnvironmentInjectingView<Content: View>: View {
7795
private final class ModeledHostingController<Model, Content: View>: UIHostingController<Content> {
7896
let modelSink: Sink<Model>
7997
let viewEnvironmentSink: Sink<ViewEnvironment>
98+
var swiftUIScreenSizingOptions: SwiftUIScreenSizingOptions {
99+
didSet {
100+
updateSizingOptionsIfNeeded()
80101

81-
init(modelSink: Sink<Model>, viewEnvironmentSink: Sink<ViewEnvironment>, rootView: Content) {
102+
if !hasLaidOutOnce {
103+
setNeedsLayoutBeforeFirstLayoutIfNeeded()
104+
}
105+
}
106+
}
107+
108+
private var hasLaidOutOnce = false
109+
110+
init(
111+
modelSink: Sink<Model>,
112+
viewEnvironmentSink: Sink<ViewEnvironment>,
113+
rootView: Content,
114+
swiftUIScreenSizingOptions: SwiftUIScreenSizingOptions
115+
) {
82116
self.modelSink = modelSink
83117
self.viewEnvironmentSink = viewEnvironmentSink
118+
self.swiftUIScreenSizingOptions = swiftUIScreenSizingOptions
84119

85120
super.init(rootView: rootView)
121+
122+
updateSizingOptionsIfNeeded()
86123
}
87124

88125
required init?(coder aDecoder: NSCoder) {
89126
fatalError("not implemented")
90127
}
128+
129+
override func viewDidLoad() {
130+
super.viewDidLoad()
131+
132+
view.backgroundColor = .clear
133+
134+
setNeedsLayoutBeforeFirstLayoutIfNeeded()
135+
}
136+
137+
override func viewDidLayoutSubviews() {
138+
super.viewDidLayoutSubviews()
139+
140+
defer { hasLaidOutOnce = true }
141+
142+
if #available(iOS 16.0, *) {
143+
// Handled in initializer, but set it on first layout to resolve a bug where the PCS is
144+
// not updated appropriately after the first layout.
145+
// UI-5797
146+
if !hasLaidOutOnce,
147+
swiftUIScreenSizingOptions.contains(.preferredContentSize) {
148+
let size = view.sizeThatFits(view.frame.size)
149+
150+
if preferredContentSize != size {
151+
preferredContentSize = size
152+
}
153+
}
154+
} else if swiftUIScreenSizingOptions.contains(.preferredContentSize) {
155+
let size = view.sizeThatFits(view.frame.size)
156+
157+
if preferredContentSize != size {
158+
preferredContentSize = size
159+
}
160+
}
161+
}
162+
163+
private func updateSizingOptionsIfNeeded() {
164+
if #available(iOS 16.0, *) {
165+
self.sizingOptions = swiftUIScreenSizingOptions.uiHostingControllerSizingOptions
166+
}
167+
168+
if !swiftUIScreenSizingOptions.contains(.preferredContentSize),
169+
preferredContentSize != .zero {
170+
preferredContentSize = .zero
171+
}
172+
}
173+
174+
private func setNeedsLayoutBeforeFirstLayoutIfNeeded() {
175+
if swiftUIScreenSizingOptions.contains(.preferredContentSize) {
176+
// Without manually calling setNeedsLayout here it was observed that a call to
177+
// layoutIfNeeded() immediately after loading the view would not perform a layout, and
178+
// therefore would not update the preferredContentSize in viewDidLayoutSubviews().
179+
// UI-5797
180+
view.setNeedsLayout()
181+
}
182+
}
183+
}
184+
185+
extension SwiftUIScreenSizingOptions {
186+
@available(iOS 16.0, *)
187+
fileprivate var uiHostingControllerSizingOptions: UIHostingControllerSizingOptions {
188+
var options = UIHostingControllerSizingOptions()
189+
190+
if contains(.preferredContentSize) {
191+
options.insert(.preferredContentSize)
192+
}
193+
194+
return options
195+
}
91196
}
92197

93198
#endif
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import SwiftUI
2+
import UIKit
3+
import WorkflowSwiftUIExperimental
4+
import XCTest
5+
6+
final class SwiftUIScreenTests: XCTestCase {
7+
func test_noSizingOptions() {
8+
let viewController = ContentScreen(sizingOptions: [])
9+
.buildViewController(in: .empty)
10+
11+
viewController.view.layoutIfNeeded()
12+
13+
XCTAssertEqual(viewController.preferredContentSize, .zero)
14+
}
15+
16+
func test_preferredContentSize() {
17+
let viewController = ContentScreen(sizingOptions: .preferredContentSize)
18+
.buildViewController(in: .empty)
19+
20+
viewController.view.layoutIfNeeded()
21+
22+
XCTAssertEqual(
23+
viewController.preferredContentSize,
24+
.init(width: 42, height: 42)
25+
)
26+
}
27+
28+
func test_preferredContentSize_sizingOptionsChanges() {
29+
let viewController = ContentScreen(sizingOptions: [])
30+
.buildViewController(in: .empty)
31+
32+
viewController.view.layoutIfNeeded()
33+
34+
XCTAssertEqual(viewController.preferredContentSize, .zero)
35+
36+
ContentScreen(sizingOptions: .preferredContentSize)
37+
.viewControllerDescription(environment: .empty)
38+
.update(viewController: viewController)
39+
40+
viewController.view.layoutIfNeeded()
41+
42+
XCTAssertEqual(
43+
viewController.preferredContentSize,
44+
.init(width: 42, height: 42)
45+
)
46+
47+
ContentScreen(sizingOptions: [])
48+
.viewControllerDescription(environment: .empty)
49+
.update(viewController: viewController)
50+
51+
viewController.view.layoutIfNeeded()
52+
53+
XCTAssertEqual(viewController.preferredContentSize, .zero)
54+
}
55+
}
56+
57+
private struct ContentScreen: SwiftUIScreen {
58+
let sizingOptions: SwiftUIScreenSizingOptions
59+
60+
static func makeView(model: ObservableValue<ContentScreen>) -> some View {
61+
Color.clear
62+
.frame(width: 42, height: 42)
63+
}
64+
}

0 commit comments

Comments
 (0)
Please sign in to comment.