UnityAppController+ViewHandling.mm
16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#include "UnityAppController+ViewHandling.h"
#include "UnityAppController+Rendering.h"
#include "UI/OrientationSupport.h"
#include "UI/UnityView.h"
#include "UI/UnityViewControllerBase.h"
#include "Unity/DisplayManager.h"
// TEMP: ?
#include "UI/ActivityIndicator.h"
#include "UI/SplashScreen.h"
#include "UI/Keyboard.h"
#include <utility>
extern bool _skipPresent;
extern bool _unityAppReady;
@implementation UnityAppController (ViewHandling)
#if UNITY_SUPPORT_ROTATION
// special case for when we DO know the app orientation, but dont get it through normal mechanism (UIViewController orientation handling)
// how can this happen:
// 1. On startup: ios is not sending "change orientation" notifications on startup (but rather we "start" in correct one already)
// 2. When using presentation controller it can override orientation constraints, so on dismissing we need to tweak app orientation;
// pretty much like startup situation UIViewController would have correct orientation, and app will be out-of-sync
- (void)updateAppOrientation:(UIInterfaceOrientation)orientation
{
_curOrientation = orientation;
[_unityView willRotateToOrientation: orientation fromOrientation: (UIInterfaceOrientation)UIInterfaceOrientationUnknown];
[_unityView didRotate];
}
#endif
- (UnityView*)createUnityView
{
return [[UnityView alloc] initFromMainScreen];
}
- (UIViewController*)createUnityViewControllerDefault
{
UnityDefaultViewController* ret = [[UnityDefaultViewController alloc] init];
#if PLATFORM_TVOS
// This enables game controller use in on-screen keyboard
ret.controllerUserInteractionEnabled = YES;
#endif
return ret;
}
#if UNITY_SUPPORT_ROTATION
- (UIViewController*)createUnityViewControllerForOrientation:(UIInterfaceOrientation)orient
{
switch (orient)
{
case UIInterfaceOrientationPortrait: return [[UnityPortraitOnlyViewController alloc] init];
case UIInterfaceOrientationPortraitUpsideDown: return [[UnityPortraitUpsideDownOnlyViewController alloc] init];
case UIInterfaceOrientationLandscapeLeft: return [[UnityLandscapeLeftOnlyViewController alloc] init];
case UIInterfaceOrientationLandscapeRight: return [[UnityLandscapeRightOnlyViewController alloc] init];
default: NSAssert(false, @"bad UIInterfaceOrientation provided");
}
return nil;
}
#endif
- (UIViewController*)createRootViewController
{
UIViewController* ret = nil;
if (!UNITY_SUPPORT_ROTATION || UnityShouldAutorotate())
{
if (_viewControllerForOrientation[0] == nil)
_viewControllerForOrientation[0] = [self createUnityViewControllerDefault];
ret = _viewControllerForOrientation[0];
}
#if UNITY_SUPPORT_ROTATION
if (ret == nil)
{
UIInterfaceOrientation orientation = ConvertToIosScreenOrientation((ScreenOrientation)UnityRequestedScreenOrientation());
ret = [self createRootViewControllerForOrientation: orientation];
}
#endif
return ret;
}
- (UIViewController*)topMostController
{
UIViewController *topController = self.window.rootViewController;
while (topController.presentedViewController)
topController = topController.presentedViewController;
return topController;
}
- (void)willStartWithViewController:(UIViewController*)controller
{
_unityView.contentScaleFactor = UnityScreenScaleFactor([UIScreen mainScreen]);
_unityView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_rootController.view = _rootView = _unityView;
}
- (void)willTransitionToViewController:(UIViewController*)toController fromViewController:(UIViewController*)fromController
{
}
- (void)didTransitionToViewController:(UIViewController*)toController fromViewController:(UIViewController*)fromController
{
#if UNITY_SUPPORT_ROTATION
// when transitioning between view controllers ios will not send reorient events (because they are bound to controllers, not view)
// so we imitate them here so unity view can update its size/orientation
[_unityView willRotateToOrientation: UIViewControllerInterfaceOrientation(toController) fromOrientation: ConvertToIosScreenOrientation(_unityView.contentOrientation)];
[_unityView didRotate];
// NB: this is both important and insane at the same time (that we have several places to keep current orentation and we need to sync them)
_curOrientation = UIViewControllerInterfaceOrientation(toController);
#endif
}
- (UIView*)createSnapshotView
{
// Note that on iPads with iOS 9 or later (up to iOS 10.2 at least) there's a bug in the iOS compositor: any use of -[UIView snapshotViewAfterScreenUpdates]
// causes black screen being shown temporarily when 4 finger gesture to swipe to another app in the task switcher is being performed slowly
#if UNITY_SNAPSHOT_VIEW_ON_APPLICATION_PAUSE
return [_rootView snapshotViewAfterScreenUpdates: YES];
#else
return nil;
#endif
}
- (void)createUI
{
NSAssert(_unityView != nil, @"_unityView should be inited at this point");
NSAssert(_window != nil, @"_window should be inited at this point");
_rootController = [self createRootViewController];
[self willStartWithViewController: _rootController];
NSAssert(_rootView != nil, @"_rootView should be inited at this point");
NSAssert(_rootController != nil, @"_rootController should be inited at this point");
[_window makeKeyAndVisible];
[UIView setAnimationsEnabled: NO];
// TODO: extract it?
ShowSplashScreen(_window);
#if UNITY_SUPPORT_ROTATION
// to be able to query orientation from view controller we should actually show it.
// at this point we can only show splash screen, so update app orientation after we started showing it
// NB: _window.rootViewController = splash view controller (not _rootController)
[self updateAppOrientation: ConvertToIosScreenOrientation(UIViewControllerOrientation(_window.rootViewController))];
#endif
NSNumber* style = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"Unity_LoadingActivityIndicatorStyle"];
ShowActivityIndicator([SplashScreen Instance], style ? [style intValue] : -1);
NSNumber* vcControlled = [[[NSBundle mainBundle] infoDictionary] objectForKey: @"UIViewControllerBasedStatusBarAppearance"];
if (vcControlled && ![vcControlled boolValue])
printf_console("\nSetting UIViewControllerBasedStatusBarAppearance to NO is no longer supported.\n"
"Apple actively discourages that, and all application-wide methods of changing status bar appearance are deprecated\n\n"
);
}
- (void)showGameUI
{
HideActivityIndicator();
HideSplashScreen();
// make sure that we start up with correctly created/inited rendering surface
// NB: recreateRenderingSurface won't go into rendering because _unityAppReady is false
#if UNITY_SUPPORT_ROTATION
[self checkOrientationRequest];
#endif
[_unityView recreateRenderingSurface];
// UI hierarchy
[_window addSubview: _rootView];
_window.rootViewController = _rootController;
[_window bringSubviewToFront: _rootView];
#if UNITY_SUPPORT_ROTATION
// to be able to query orientation from view controller we should actually show it.
// at this point we finally started to show game view controller. Just in case update orientation again
[self updateAppOrientation: ConvertToIosScreenOrientation(UIViewControllerOrientation(_rootController))];
#endif
// why we set level ready only now:
// surface recreate will try to repaint if this var is set (poking unity to do it)
// but this frame now is actually the first one we want to process/draw
// so all the recreateSurface before now (triggered by reorientation) should simply change extents
_unityAppReady = true;
// why we skip present:
// this will be the first frame to draw, so Start methods will be called
// and we want to properly handle resolution request in Start (which might trigger surface recreate)
// NB: we want to draw right after showing window, to avoid black frame creeping in
_skipPresent = true;
if (!UnityIsPaused())
UnityRepaint();
_skipPresent = false;
[self repaint];
[UIView setAnimationsEnabled: YES];
}
- (void)transitionToViewController:(UIViewController*)vc
{
[self willTransitionToViewController: vc fromViewController: _rootController];
// first: remove from view hierarchy.
// if we simply hide the window before assigning the new view controller, it will cause black frame flickering
// on the other hand, hiding the window is important by itself to better signal the intent to iOS
// e.g. unless we hide the window view, safeArea might stop working (due to bug in iOS if we're to speculate)
// due to that we do this hide/unhide sequence: we want to to make it hidden, but still unhide it before changing window view controller.
_window.hidden = YES;
_window.hidden = NO;
_rootController.view = nil;
_window.rootViewController = nil;
// second: assign new root controller (and view hierarchy with that), restore bounds
_rootController = _window.rootViewController = vc;
_rootController.view = _rootView;
_window.bounds = [UIScreen mainScreen].bounds;
// required for iOS 8, otherwise view bounds will be incorrect
_rootView.bounds = _window.bounds;
_rootView.center = _window.center;
// third: restore window as key and layout subviews to finalize size changes
[_window makeKeyAndVisible];
[_window layoutSubviews];
[self didTransitionToViewController: vc fromViewController: _rootController];
}
#if UNITY_SUPPORT_ROTATION
- (void)interfaceWillChangeOrientationTo:(UIInterfaceOrientation)toInterfaceOrientation
{
UIInterfaceOrientation fromInterfaceOrientation = _curOrientation;
_curOrientation = toInterfaceOrientation;
[_unityView willRotateToOrientation: toInterfaceOrientation fromOrientation: fromInterfaceOrientation];
}
- (void)interfaceDidChangeOrientationFrom:(UIInterfaceOrientation)fromInterfaceOrientation
{
[_unityView didRotate];
}
#endif
#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
- (void)executeForEveryViewController:(void(^)(UIViewController*))callback
{
for (unsigned i = 0; i < ARRAY_SIZE(_viewControllerForOrientation); ++i)
{
UIViewController* vc = _viewControllerForOrientation[i];
if (vc)
callback(vc);
}
}
- (void)notifyHideHomeButtonChange
{
// Note that we need to update all view controllers because UIKit won't necessarily
// update the properties of view controllers when orientation is changed.
#if PLATFORM_IOS
if (@available(iOS 11.0, *))
{
[self executeForEveryViewController: ^(UIViewController* vc)
{
// setNeedsUpdateOfHomeIndicatorAutoHidden is not implemented on iOS 11.0.
// The bug has been fixed in iOS 11.0.1. See http://www.openradar.me/35127134
if ([vc respondsToSelector: @selector(setNeedsUpdateOfHomeIndicatorAutoHidden)])
[vc setNeedsUpdateOfHomeIndicatorAutoHidden];
}];
}
#endif
}
- (void)notifyDeferSystemGesturesChange
{
#if PLATFORM_IOS
if (@available(iOS 11.0, *))
{
[self executeForEveryViewController: ^(UIViewController* vc)
{
[vc setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}];
}
#endif
}
@end
#if UNITY_SUPPORT_ROTATION
@implementation UnityAppController (OrientationSupport)
- (UIViewController*)createRootViewControllerForOrientation:(UIInterfaceOrientation)orientation
{
NSAssert(orientation != 0, @"Bad UIInterfaceOrientation provided");
if (_viewControllerForOrientation[orientation] == nil)
_viewControllerForOrientation[orientation] = [self createUnityViewControllerForOrientation: orientation];
return _viewControllerForOrientation[orientation];
}
- (void)checkOrientationRequest
{
if (!UnityHasOrientationRequest() && !UnityShouldChangeAllowedOrientations())
return;
// normally we want to call attemptRotationToDeviceOrientation to tell iOS that we changed orientation constraints
// but if the current orientation is disabled we need special processing, as iOS will simply ignore us
// the only good/robust way is to simply recreate "autorotating" view controller and transition to it if needed
// please note that we want to trigger "orientation request" code path if we recreate autorotating view controller
bool changeOrient = UnityHasOrientationRequest();
// first we check if we need to update orientations enabled for autorotation
// this needs to be done *only* if we are to continue autorotating
// otherwise we will transition from this view controller
// and iOS will reread enabled orientations on next ViewController activation
const bool autorot = UnityShouldAutorotate();
if (UnityShouldChangeAllowedOrientations() && autorot)
{
NSUInteger rootOrient = 1 << UIViewControllerInterfaceOrientation(self.rootViewController);
if (_rootController == _viewControllerForOrientation[0] && (rootOrient & EnabledAutorotationInterfaceOrientations()))
{
// if we are currently autorotating AND changed allowed orientations while keeping current interface orientation allowed:
// we can simply trigger attemptRotationToDeviceOrientation and we are done
// please note that this can happen when current *device* orientation is disabled (and we want to enable it)
[UIViewController attemptRotationToDeviceOrientation];
}
else
{
// otherwise we recreate default autorotating view controller
// please note that below we will check if root controller still equals _viewControllerForOrientation[0]
// in that case (we update _viewControllerForOrientation[0]) the check will fail and will trigger transition (as expected)
// you may look at this check as "are we autorotating with same constraints"
_viewControllerForOrientation[0] = [self createUnityViewControllerDefault];
changeOrient = true;
}
}
if (changeOrient)
{
// on some devices like iPhone XS layoutSubview is not called when transitioning from different orientations with the same resolution
// therefore forcing layoutSubview on all orientation changes
[_unityView setNeedsLayout];
if (autorot)
{
if (_viewControllerForOrientation[0] == nil)
_viewControllerForOrientation[0] = [self createUnityViewControllerDefault];
if (_rootController != _viewControllerForOrientation[0])
[self transitionToViewController: _viewControllerForOrientation[0]];
[UIViewController attemptRotationToDeviceOrientation];
}
else
{
UIInterfaceOrientation requestedOrient = ConvertToIosScreenOrientation((ScreenOrientation)UnityRequestedScreenOrientation());
// on one hand orientInterface: should be perfectly fine "reorienting" to current orientation
// in reality, ios might be confused by transitionToViewController: shenanigans coupled with "nothing have changed actually"
// as an example: prior to ios12 that might result in status bar going "bad" (becoming transparent)
if (_rootController != _viewControllerForOrientation[requestedOrient])
[self orientInterface: requestedOrient];
}
}
UnityOrientationRequestWasCommitted();
}
- (void)orientInterface:(UIInterfaceOrientation)orient
{
if (_unityAppReady)
UnityFinishRendering();
[KeyboardDelegate StartReorientation];
[CATransaction begin];
{
UIInterfaceOrientation oldOrient = _curOrientation;
UIInterfaceOrientation newOrient = orient;
[self interfaceWillChangeOrientationTo: newOrient];
[self transitionToViewController: [self createRootViewControllerForOrientation: newOrient]];
[self interfaceDidChangeOrientationFrom: oldOrient];
[UIApplication sharedApplication].statusBarOrientation = orient;
}
[CATransaction commit];
[KeyboardDelegate FinishReorientation];
}
- (void)orientUnity:(UIInterfaceOrientation)orient
{
[self orientInterface: orient];
}
@end
#endif
extern "C" void UnityNotifyHideHomeButtonChange()
{
[GetAppController() notifyHideHomeButtonChange];
}
extern "C" void UnityNotifyDeferSystemGesturesChange()
{
[GetAppController() notifyDeferSystemGesturesChange];
}