@@ -24,38 +24,164 @@ public protocol ObservableScreen: Screen {
24
24
/// The type of the model that this screen observes.
25
25
associatedtype Model : ObservableModel
26
26
27
- /// The sizing options for the screen.
28
- var sizingOptions : SwiftUIScreenSizingOptions { get }
29
27
/// The model that this screen observes.
30
28
var model : Model { get }
31
29
30
+ // MARK: - Optional configuration
31
+
32
+ /// The sizing options for the screen.
33
+ var sizingOptions : SwiftUIScreenSizingOptions { get }
34
+
35
+ /// The preferred status bar style when this screen is in control of the status bar appearance.
36
+ ///
37
+ /// Defaults to `.default`.
38
+ func preferredStatusBarStyle( in context: ObservableScreenContext ) -> UIStatusBarStyle
39
+
40
+ /// If the status bar is shown or hidden when this screen is in control of
41
+ /// the status bar appearance.
42
+ ///
43
+ /// Defaults to `false`
44
+ func prefersStatusBarHidden( in context: ObservableScreenContext ) -> Bool
45
+
46
+ /// The preferred animation style when the status bar appearance changes when this screen is in
47
+ /// control of the status bar appearance.
48
+ ///
49
+ /// Defaults to `.fade`
50
+ func preferredStatusBarUpdateAnimation(
51
+ in context: ObservableScreenContext
52
+ ) -> UIStatusBarAnimation
53
+
54
+ /// The supported interface orientations of this screen.
55
+ ///
56
+ /// Defaults to all orientations for iPad, and portrait / portrait upside down for iPhone.
57
+ func supportedInterfaceOrientations(
58
+ in context: ObservableScreenContext
59
+ ) -> UIInterfaceOrientationMask
60
+
61
+ /// Which screen edges should defer system gestures when this screen is in control.
62
+ ///
63
+ /// Defaults to `[]` (none).
64
+ func preferredScreenEdgesDeferringSystemGestures(
65
+ in context: ObservableScreenContext
66
+ ) -> UIRectEdge
67
+
68
+ /// If the home indicator should be auto hidden or not when this screen is in control of the
69
+ /// home indicator appearance.
70
+ ///
71
+ /// Defaults to `false`
72
+ func prefersHomeIndicatorAutoHidden( in context: ObservableScreenContext ) -> Bool
73
+
74
+ /// Invoked when a physical button is pressed, such as one of a hardware keyboard. Return `true`
75
+ /// if the event is handled by the screen, otherwise `false` to forward the message along the
76
+ /// responder chain.
77
+ ///
78
+ /// Defaults to `false` for all events.
79
+ func pressesBegan( _ presses: Set < UIPress > , with event: UIPressesEvent ? ) -> Bool
80
+
81
+ /// This method is called when VoiceOver is enabled and the escape gesture is performed (a
82
+ /// 2-finger Z shape).
83
+ ///
84
+ /// Implement this method if your screen is a modal that can be dismissed without an explicit
85
+ /// action. For example, most modals with a close button should implement this method and have
86
+ /// the same behavior as tapping close. Return `true` if this method did dismiss the modal.
87
+ ///
88
+ /// Defaults to `false`.
89
+ func accessibilityPerformEscape( ) -> Bool
90
+
32
91
/// Constructs the root view for this screen. This is only called once to initialize the view.
33
92
/// After the initial construction, the view will be updated by injecting new values into the
34
93
/// store.
35
94
@ViewBuilder
36
95
static func makeView( store: Store < Model > ) -> Content
37
96
}
38
97
98
+ /// Context that holds view values for `ObservableScreen` customization hooks.
99
+ public struct ObservableScreenContext {
100
+ /// The view environment of the associated view controller.
101
+ public let environment : ViewEnvironment
102
+
103
+ /// The safe area insets of this screen in its current position.
104
+ public let safeAreaInsets : UIEdgeInsets
105
+
106
+ /// The size of the view controller's containing window, if available.
107
+ public let windowSize : CGSize ?
108
+
109
+ public init (
110
+ environment: ViewEnvironment ,
111
+ safeAreaInsets: UIEdgeInsets ,
112
+ windowSize: CGSize ? = nil
113
+ ) {
114
+ self . environment = environment
115
+ self . safeAreaInsets = safeAreaInsets
116
+ self . windowSize = windowSize
117
+ }
118
+ }
119
+
39
120
extension ObservableScreen {
40
- public var sizingOptions : SwiftUIScreenSizingOptions { [ ] }
121
+ public var sizingOptions : SwiftUIScreenSizingOptions {
122
+ [ ]
123
+ }
124
+
125
+ public func preferredStatusBarStyle( in context: ObservableScreenContext ) -> UIStatusBarStyle {
126
+ . default
127
+ }
128
+
129
+ public func prefersStatusBarHidden( in context: ObservableScreenContext ) -> Bool {
130
+ false
131
+ }
132
+
133
+ public func preferredStatusBarUpdateAnimation(
134
+ in context: ObservableScreenContext
135
+ ) -> UIStatusBarAnimation {
136
+ . fade
137
+ }
138
+
139
+ public func supportedInterfaceOrientations(
140
+ in context: ObservableScreenContext
141
+ ) -> UIInterfaceOrientationMask {
142
+ if UIDevice . current. userInterfaceIdiom == . pad {
143
+ . all
144
+ } else {
145
+ [ . portrait, . portraitUpsideDown]
146
+ }
147
+ }
148
+
149
+ public func preferredScreenEdgesDeferringSystemGestures(
150
+ in context: ObservableScreenContext
151
+ ) -> UIRectEdge {
152
+ [ ]
153
+ }
154
+
155
+ public func prefersHomeIndicatorAutoHidden( in context: ObservableScreenContext ) -> Bool {
156
+ false
157
+ }
158
+
159
+ public func pressesBegan( _ presses: Set < UIPress > , with event: UIPressesEvent ? ) -> Bool {
160
+ false
161
+ }
162
+
163
+ public func accessibilityPerformEscape( ) -> Bool {
164
+ false
165
+ }
41
166
}
42
167
43
168
extension ObservableScreen {
44
169
public func viewControllerDescription( environment: ViewEnvironment ) -> ViewControllerDescription {
45
170
ViewControllerDescription (
46
- type: ModeledHostingController < Model , Content > . self,
171
+ performInitialUpdate: false ,
172
+ type: ObservableScreenViewController< Self, Content> . self ,
47
173
environment: environment,
48
174
build: {
49
175
let ( store, setModel) = Store . make ( model: model)
50
- return ModeledHostingController (
176
+ return ObservableScreenViewController (
51
177
setModel: setModel,
52
178
viewEnvironment: environment,
53
179
rootView: Self . makeView ( store: store) ,
54
- sizingOptions : sizingOptions
180
+ screen : self
55
181
)
56
182
} ,
57
183
update: { hostingController in
58
- hostingController. setModel ( model )
184
+ hostingController. update ( screen : self )
59
185
// ViewEnvironment updates are handled by the ModeledHostingController internally
60
186
}
61
187
)
@@ -89,33 +215,35 @@ private final class ViewEnvironmentHolder: ObservableObject {
89
215
}
90
216
}
91
217
92
- private final class ModeledHostingController < Model, Content: View > : UIHostingController < ModifiedContent < Content , ViewEnvironmentModifier > > , ViewEnvironmentObserving {
93
- let setModel : ( Model ) -> Void
218
+ private final class ObservableScreenViewController < ScreenType: ObservableScreen , Content: View > :
219
+ UIHostingController < ModifiedContent < Content , ViewEnvironmentModifier > > ,
220
+ ViewEnvironmentObserving
221
+ {
222
+ typealias Model = ScreenType . Model
94
223
224
+ private let setModel : ( Model ) -> Void
95
225
private let viewEnvironmentHolder : ViewEnvironmentHolder
96
226
97
- var swiftUIScreenSizingOptions : SwiftUIScreenSizingOptions {
98
- didSet {
99
- updateSizingOptionsIfNeeded ( )
100
- if isViewLoaded {
101
- setNeedsLayoutBeforeFirstLayoutIfNeeded ( )
102
- }
103
- }
104
- }
105
-
227
+ private var screen : ScreenType
106
228
private var hasLaidOutOnce = false
107
229
private var maxFrameWidth : CGFloat = 0
108
230
private var maxFrameHeight : CGFloat = 0
109
231
232
+ private var previousPreferredStatusBarStyle : UIStatusBarStyle ?
233
+ private var previousPrefersStatusBarHidden : Bool ?
234
+ private var previousSupportedInterfaceOrientations : UIInterfaceOrientationMask ?
235
+ private var previousPreferredScreenEdgesDeferringSystemGestures : UIRectEdge ?
236
+ private var previousPrefersHomeIndicatorAutoHidden : Bool ?
237
+
110
238
init (
111
239
setModel: @escaping ( Model ) -> Void ,
112
240
viewEnvironment: ViewEnvironment ,
113
241
rootView: Content ,
114
- sizingOptions swiftUIScreenSizingOptions : SwiftUIScreenSizingOptions
242
+ screen : ScreenType
115
243
) {
116
244
self . setModel = setModel
117
245
self . viewEnvironmentHolder = ViewEnvironmentHolder ( viewEnvironment: viewEnvironment)
118
- self . swiftUIScreenSizingOptions = swiftUIScreenSizingOptions
246
+ self . screen = screen
119
247
120
248
super. init (
121
249
rootView: rootView
@@ -130,6 +258,12 @@ private final class ModeledHostingController<Model, Content: View>: UIHostingCon
130
258
fatalError ( " not implemented " )
131
259
}
132
260
261
+ func update( screen: ScreenType ) {
262
+ self . screen = screen
263
+ setModel ( screen. model)
264
+ updateViewControllerContainmentForwarding ( )
265
+ }
266
+
133
267
override func viewDidLoad( ) {
134
268
super. viewDidLoad ( )
135
269
@@ -146,7 +280,7 @@ private final class ModeledHostingController<Model, Content: View>: UIHostingCon
146
280
147
281
defer { hasLaidOutOnce = true }
148
282
149
- if swiftUIScreenSizingOptions . contains ( . preferredContentSize) {
283
+ if screen . sizingOptions . contains ( . preferredContentSize) {
150
284
// Use the largest frame ever laid out in as a constraint for preferredContentSize
151
285
// measurements.
152
286
let width = max ( view. frame. width, maxFrameWidth)
@@ -175,6 +309,8 @@ private final class ModeledHostingController<Model, Content: View>: UIHostingCon
175
309
if preferredContentSize != size {
176
310
preferredContentSize = size
177
311
}
312
+ } else if preferredContentSize != . zero {
313
+ preferredContentSize = . zero
178
314
}
179
315
}
180
316
@@ -184,16 +320,97 @@ private final class ModeledHostingController<Model, Content: View>: UIHostingCon
184
320
applyEnvironmentIfNeeded ( )
185
321
}
186
322
323
+ override var preferredStatusBarStyle : UIStatusBarStyle {
324
+ screen. preferredStatusBarStyle ( in: makeCurrentContext ( ) )
325
+ }
326
+
327
+ override var prefersStatusBarHidden : Bool {
328
+ screen. prefersStatusBarHidden ( in: makeCurrentContext ( ) )
329
+ }
330
+
331
+ override var preferredStatusBarUpdateAnimation : UIStatusBarAnimation {
332
+ screen. preferredStatusBarUpdateAnimation ( in: makeCurrentContext ( ) )
333
+ }
334
+
335
+ override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
336
+ screen. supportedInterfaceOrientations ( in: makeCurrentContext ( ) )
337
+ }
338
+
339
+ override var preferredScreenEdgesDeferringSystemGestures : UIRectEdge {
340
+ screen. preferredScreenEdgesDeferringSystemGestures ( in: makeCurrentContext ( ) )
341
+ }
342
+
343
+ override var prefersHomeIndicatorAutoHidden : Bool {
344
+ screen. prefersHomeIndicatorAutoHidden ( in: makeCurrentContext ( ) )
345
+ }
346
+
347
+ override func accessibilityPerformEscape( ) -> Bool {
348
+ screen. accessibilityPerformEscape ( )
349
+ }
350
+
351
+ override func pressesBegan( _ presses: Set < UIPress > , with event: UIPressesEvent ? ) {
352
+ let handled = screen. pressesBegan ( presses, with: event)
353
+ if !handled {
354
+ super. pressesBegan ( presses, with: event)
355
+ }
356
+ }
357
+
358
+ private func makeCurrentContext( ) -> ObservableScreenContext {
359
+ ObservableScreenContext (
360
+ environment: environment,
361
+ safeAreaInsets: viewIfLoaded? . safeAreaInsets ?? . zero,
362
+ windowSize: view. window? . bounds. size
363
+ )
364
+ }
365
+
187
366
private func updateSizingOptionsIfNeeded( ) {
188
- if !swiftUIScreenSizingOptions. contains ( . preferredContentSize) ,
189
- preferredContentSize != . zero
190
- {
367
+ if !screen. sizingOptions. contains ( . preferredContentSize) , preferredContentSize != . zero {
191
368
preferredContentSize = . zero
192
369
}
193
370
}
194
371
372
+ private func updateViewControllerContainmentForwarding( ) {
373
+ // Update status bar.
374
+ let preferredStatusBarStyle = preferredStatusBarStyle
375
+ let prefersStatusBarHidden = prefersStatusBarHidden
376
+ if ( previousPreferredStatusBarStyle != nil && previousPreferredStatusBarStyle != preferredStatusBarStyle) ||
377
+ ( previousPrefersStatusBarHidden != nil && previousPrefersStatusBarHidden != prefersStatusBarHidden)
378
+ {
379
+ setNeedsStatusBarAppearanceUpdate ( )
380
+ }
381
+ previousPreferredStatusBarStyle = preferredStatusBarStyle
382
+ previousPrefersStatusBarHidden = prefersStatusBarHidden
383
+
384
+ // Update interface orientation.
385
+ let supportedInterfaceOrientations = supportedInterfaceOrientations
386
+ if previousSupportedInterfaceOrientations != nil ,
387
+ previousSupportedInterfaceOrientations != supportedInterfaceOrientations
388
+ {
389
+ setNeedsUpdateOfSupportedInterfaceOrientationsAndRotateIfNeeded ( )
390
+ }
391
+ previousSupportedInterfaceOrientations = supportedInterfaceOrientations
392
+
393
+ // Update screen edges deferring system gestures.
394
+ let preferredScreenEdgesDeferringSystemGestures = preferredScreenEdgesDeferringSystemGestures
395
+ if previousPreferredScreenEdgesDeferringSystemGestures != nil ,
396
+ previousPreferredScreenEdgesDeferringSystemGestures != preferredScreenEdgesDeferringSystemGestures
397
+ {
398
+ setNeedsUpdateOfScreenEdgesDeferringSystemGestures ( )
399
+ }
400
+ previousPreferredScreenEdgesDeferringSystemGestures = preferredScreenEdgesDeferringSystemGestures
401
+
402
+ // Update home indicator visibility.
403
+ let prefersHomeIndicatorAutoHidden = prefersHomeIndicatorAutoHidden
404
+ if previousPrefersHomeIndicatorAutoHidden != nil ,
405
+ previousPrefersHomeIndicatorAutoHidden != prefersHomeIndicatorAutoHidden
406
+ {
407
+ setNeedsUpdateOfHomeIndicatorAutoHidden ( )
408
+ }
409
+ previousPrefersHomeIndicatorAutoHidden = prefersHomeIndicatorAutoHidden
410
+ }
411
+
195
412
private func setNeedsLayoutBeforeFirstLayoutIfNeeded( ) {
196
- if swiftUIScreenSizingOptions . contains ( . preferredContentSize) , !hasLaidOutOnce {
413
+ if screen . sizingOptions . contains ( . preferredContentSize) , !hasLaidOutOnce {
197
414
// Without manually calling setNeedsLayout here it was observed that a call to
198
415
// layoutIfNeeded() immediately after loading the view would not perform a layout, and
199
416
// therefore would not update the preferredContentSize in viewDidLayoutSubviews().
0 commit comments