| Flutter iOS Embedder |

FlutterViewController.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #define FML_USED_ON_EMBEDDER
6
7 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
8
9 #import <os/log.h>
10 #include <memory>
11
12 #include "flutter/common/constants.h"
13 #include "flutter/fml/memory/weak_ptr.h"
14 #include "flutter/fml/message_loop.h"
15 #include "flutter/fml/platform/darwin/platform_version.h"
16 #include "flutter/runtime/ptrace_check.h"
17 #include "flutter/shell/common/thread_host.h"
18 #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
19 #import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h"
20 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterAppDelegate_Internal.h"
21 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterChannelKeyResponder.h"
22 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.h"
23 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
24 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyPrimaryResponder.h"
25 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h"
26 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterLaunchEngine.h"
27 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
28 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
29 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate_internal.h"
30 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h"
31 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"
32 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
33 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h"
34 #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h"
35 #import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h"
36 #import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h"
37 #import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
38 #import "flutter/shell/platform/embedder/embedder.h"
39 #import "flutter/third_party/spring_animation/spring_animation.h"
40
41 FLUTTER_ASSERT_ARC
42
43 static constexpr int kMicrosecondsPerSecond = 1000 * 1000;
44 static constexpr CGFloat kScrollViewContentSize = 2.0;
45
46 static NSString* const kFlutterRestorationStateAppData = @"FlutterRestorationStateAppData";
47
48 NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate";
49 NSNotificationName const FlutterViewControllerWillDealloc = @"FlutterViewControllerWillDealloc";
50 NSNotificationName const FlutterViewControllerHideHomeIndicator =
51 @"FlutterViewControllerHideHomeIndicator";
52 NSNotificationName const FlutterViewControllerShowHomeIndicator =
53 @"FlutterViewControllerShowHomeIndicator";
54
55 // Struct holding data to help adapt system mouse/trackpad events to embedder events.
56 typedef struct MouseState {
57 // Current coordinate of the mouse cursor in physical device pixels.
58 CGPoint location = CGPointZero;
59
60 // Last reported translation for an in-flight pan gesture in physical device pixels.
61 CGPoint last_translation = CGPointZero;
62 } MouseState;
63
64 // This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the
65 // change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are
66 // just a warning.
67 @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegate>
68 // TODO(dkwingsmt): Make the view ID property public once the iOS shell
69 // supports multiple views.
70 // https://github.com/flutter/flutter/issues/138168
71 @property(nonatomic, readonly) int64_t viewIdentifier;
72
73 // We keep a separate reference to this and create it ahead of time because we want to be able to
74 // set up a shell along with its platform view before the view has to appear.
75 @property(nonatomic, strong) FlutterView* flutterView;
76 @property(nonatomic, strong) void (^flutterViewRenderedCallback)(void);
77
78 @property(nonatomic, assign) UIInterfaceOrientationMask orientationPreferences;
79 @property(nonatomic, assign) UIStatusBarStyle statusBarStyle;
80 @property(nonatomic, assign) BOOL initialized;
81 @property(nonatomic, assign) BOOL engineNeedsLaunch;
82 @property(nonatomic, assign) BOOL awokenFromNib;
83
84 @property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
85 @property(nonatomic, assign) BOOL isHomeIndicatorHidden;
86 @property(nonatomic, assign) BOOL isPresentingViewControllerAnimating;
87
88 // Internal state backing override of UIView.prefersStatusBarHidden.
89 @property(nonatomic, assign) BOOL flutterPrefersStatusBarHidden;
90
91 @property(nonatomic, strong) NSMutableSet<NSNumber*>* ongoingTouches;
92 // This scroll view is a workaround to accommodate iOS 13 and higher. There isn't a way to get
93 // touches on the status bar to trigger scrolling to the top of a scroll view. We place a
94 // UIScrollView with height zero and a content offset so we can get those events. See also:
95 // https://github.com/flutter/flutter/issues/35050
96 @property(nonatomic, strong) UIScrollView* scrollView;
97 @property(nonatomic, strong) UIView* keyboardAnimationView;
98 @property(nonatomic, strong) SpringAnimation* keyboardSpringAnimation;
99
100 /**
101 * Whether we should ignore viewport metrics updates during rotation transition.
102 */
103 @property(nonatomic, assign) BOOL shouldIgnoreViewportMetricsUpdatesDuringRotation;
104 /**
105 * Keyboard animation properties
106 */
107 @property(nonatomic, assign) CGFloat targetViewInsetBottom;
108 @property(nonatomic, assign) CGFloat originalViewInsetBottom;
109 @property(nonatomic, strong) VSyncClient* keyboardAnimationVSyncClient;
110 @property(nonatomic, assign) BOOL keyboardAnimationIsShowing;
111 @property(nonatomic, assign) fml::TimePoint keyboardAnimationStartTime;
112 @property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground;
113
114 /// Timestamp after which a scroll inertia cancel event should be inferred.
115 @property(nonatomic, assign) NSTimeInterval scrollInertiaEventStartline;
116
117 /// When an iOS app is running in emulation on an Apple Silicon Mac, trackpad input goes through
118 /// a translation layer, and events are not received with precise deltas. Due to this, we can't
119 /// rely on checking for a stationary trackpad event. Fortunately, AppKit will send an event of
120 /// type UIEventTypeScroll following a scroll when inertia should stop. This field is needed to
121 /// estimate if such an event represents the natural end of scrolling inertia or a user-initiated
122 /// cancellation.
123 @property(nonatomic, assign) NSTimeInterval scrollInertiaEventAppKitDeadline;
124
125 /// VSyncClient for touch events delivery frame rate correction.
126 ///
127 /// On promotion devices(eg: iPhone13 Pro), the delivery frame rate of touch events is 60HZ
128 /// but the frame rate of rendering is 120HZ, which is different and will leads jitter and laggy.
129 /// With this VSyncClient, it can correct the delivery frame rate of touch events to let it keep
130 /// the same with frame rate of rendering.
131 @property(nonatomic, strong) VSyncClient* touchRateCorrectionVSyncClient;
132
133 /// The size of the FlutterView's frame, as determined by auto-layout,
134 /// before Flutter's custom auto-resizing constraints are applied.
135 @property(nonatomic, assign) CGSize sizeBeforeAutoResized;
136
137 /*
138 * Mouse and trackpad gesture recognizers
139 */
140 // Mouse and trackpad hover
141 @property(nonatomic, strong)
142 UIHoverGestureRecognizer* hoverGestureRecognizer API_AVAILABLE(ios(13.4));
143 // Mouse wheel scrolling
144 @property(nonatomic, strong)
145 UIPanGestureRecognizer* discreteScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4));
146 // Trackpad and Magic Mouse scrolling
147 @property(nonatomic, strong)
148 UIPanGestureRecognizer* continuousScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4));
149 // Trackpad pinching
150 @property(nonatomic, strong)
151 UIPinchGestureRecognizer* pinchGestureRecognizer API_AVAILABLE(ios(13.4));
152 // Trackpad rotating
153 @property(nonatomic, strong)
154 UIRotationGestureRecognizer* rotationGestureRecognizer API_AVAILABLE(ios(13.4));
155
156 /// Creates and registers plugins used by this view controller.
157 - (void)addInternalPlugins;
158 - (void)deregisterNotifications;
159
160 /// Called when the first frame has been rendered. Invokes any registered first-frame callback.
161 - (void)onFirstFrameRendered;
162
163 /// Handles updating viewport metrics on keyboard animation.
164 - (void)handleKeyboardAnimationCallbackWithTargetTime:(fml::TimePoint)targetTime;
165 @end
166
167 @implementation FlutterViewController {
168 flutter::ViewportMetrics _viewportMetrics;
169 MouseState _mouseState;
170 }
171
172 // Synthesize properties with an overridden getter/setter.
173 @synthesize viewOpaque = _viewOpaque;
174 @synthesize displayingFlutterUI = _displayingFlutterUI;
175
176 // TODO(dkwingsmt): https://github.com/flutter/flutter/issues/138168
177 // No backing ivar is currently required; when multiple views are supported, we'll need to
178 // synthesize the ivar and store the view identifier.
179 @dynamic viewIdentifier;
180
181 #pragma mark - Manage and override all designated initializers
182
183 - (instancetype)initWithEngine:(FlutterEngine*)engine
184 nibName:(nullable NSString*)nibName
185 bundle:(nullable NSBundle*)nibBundle {
186 FML_CHECK(engine) << "initWithEngine:nibName:bundle: must be called with non-nil engine";
187 self = [super initWithNibName:nibName bundle:nibBundle];
188 if (self) {
189 _viewOpaque = YES;
190 if (engine.viewController) {
191 NSString* errorMessage =
192 [NSString stringWithFormat:
193 @"The supplied FlutterEngine %@ is already used with FlutterViewController "
194 "instance %@. One instance of the FlutterEngine can only be attached to "
195 "one FlutterViewController at a time. Set FlutterEngine.viewController to "
196 "nil before attaching it to another FlutterViewController.",
197 engine.description, engine.viewController.description];
198 [FlutterLogger logError:errorMessage];
199 }
200 _engine = engine;
201 _engineNeedsLaunch = NO;
202 _flutterView = [[FlutterView alloc] initWithDelegate:_engine
203 opaque:self.isViewOpaque
204 enableWideGamut:engine.project.isWideGamutEnabled];
205 _ongoingTouches = [[NSMutableSet alloc] init];
206
207 // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
208 // Eliminate method calls in initializers and dealloc.
209 [self performCommonViewControllerInitialization];
210 [engine setViewController:self];
211 }
212
213 return self;
214 }
215
216 - (instancetype)initWithProject:(FlutterDartProject*)project
217 nibName:(NSString*)nibName
218 bundle:(NSBundle*)nibBundle {
219 self = [super initWithNibName:nibName bundle:nibBundle];
220 if (self) {
221 // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
222 // Eliminate method calls in initializers and dealloc.
223 [self sharedSetupWithProject:project initialRoute:nil];
224 }
225
226 return self;
227 }
228
229 - (instancetype)initWithProject:(FlutterDartProject*)project
230 initialRoute:(NSString*)initialRoute
231 nibName:(NSString*)nibName
232 bundle:(NSBundle*)nibBundle {
233 self = [super initWithNibName:nibName bundle:nibBundle];
234 if (self) {
235 // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
236 // Eliminate method calls in initializers and dealloc.
237 [self sharedSetupWithProject:project initialRoute:initialRoute];
238 }
239
240 return self;
241 }
242
243 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
244 return [self initWithProject:nil nibName:nil bundle:nil];
245 }
246
247 - (instancetype)initWithCoder:(NSCoder*)aDecoder {
248 self = [super initWithCoder:aDecoder];
249 return self;
250 }
251
252 - (void)awakeFromNib {
253 [super awakeFromNib];
254 self.awokenFromNib = YES;
255 if (!self.engine) {
256 [self sharedSetupWithProject:nil initialRoute:nil];
257 }
258 }
259
260 - (instancetype)init {
261 return [self initWithProject:nil nibName:nil bundle:nil];
262 }
263
264 - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
265 initialRoute:(nullable NSString*)initialRoute {
266 id appDelegate = FlutterSharedApplication.application.delegate;
267 FlutterEngine* engine;
268 if ([appDelegate respondsToSelector:@selector(takeLaunchEngine)]) {
269 if (self.nibName) {
270 // Only grab the launch engine if it was created with a nib.
271 // FlutterViewControllers created from nibs can't specify their initial
272 // routes so it's safe to take it.
273 engine = [appDelegate takeLaunchEngine];
274 } else {
275 // If we registered plugins with a FlutterAppDelegate without a xib, throw
276 // away the engine that was registered through the FlutterAppDelegate.
277 // That's not a valid usage of the API.
278 [appDelegate takeLaunchEngine];
279 }
280 }
281 if (!engine) {
282 // Need the project to get settings for the view. Initializing it here means
283 // the Engine class won't initialize it later.
284 if (!project) {
285 project = [[FlutterDartProject alloc] init];
286 }
287
288 engine = [[FlutterEngine alloc] initWithName:@"io.flutter"
289 project:project
290 allowHeadlessExecution:self.engineAllowHeadlessExecution
291 restorationEnabled:self.restorationIdentifier != nil];
292 }
293 if (!engine) {
294 return;
295 }
296
297 _viewOpaque = YES;
298 _engine = engine;
299 _flutterView = [[FlutterView alloc] initWithDelegate:_engine
300 opaque:_viewOpaque
301 enableWideGamut:engine.project.isWideGamutEnabled];
302 [_engine createShell:nil libraryURI:nil initialRoute:initialRoute];
303
304 // We call this from the FlutterViewController instead of the FlutterEngine directly because this
305 // is only needed when the FlutterEngine is implicit. If it's not implicit there's no need for
306 // them to have a callback to expose the engine since they created the FlutterEngine directly.
307 // This is the earliest this can be called because it depends on the shell being created.
308 BOOL performedCallback = [_engine performImplicitEngineCallback];
309
310 // TODO(vashworth): Deprecate, see https://github.com/flutter/flutter/issues/176424
311 if ([FlutterSharedApplication.application.delegate
312 respondsToSelector:@selector(pluginRegistrant)]) {
313 NSObject<FlutterPluginRegistrant>* pluginRegistrant =
314 [FlutterSharedApplication.application.delegate performSelector:@selector(pluginRegistrant)];
315 [pluginRegistrant registerWithRegistry:self];
316 performedCallback = YES;
317 }
318 // When migrated to scenes, the FlutterViewController from the storyboard is initialized after the
319 // application launch events. Therefore, plugins may not be registered yet since they're expected
320 // to be registered during the implicit engine callbacks. As a workaround, send the app launch
321 // events after the application callbacks.
322 if (self.awokenFromNib && performedCallback && FlutterSharedApplication.hasSceneDelegate &&
323 [appDelegate isKindOfClass:[FlutterAppDelegate class]]) {
324 id applicationLifeCycleDelegate = ((FlutterAppDelegate*)appDelegate).lifeCycleDelegate;
325 [applicationLifeCycleDelegate
326 sceneFallbackWillFinishLaunchingApplication:FlutterSharedApplication.application];
327 [applicationLifeCycleDelegate
328 sceneFallbackDidFinishLaunchingApplication:FlutterSharedApplication.application];
329 }
330
331 _engineNeedsLaunch = YES;
332 _ongoingTouches = [[NSMutableSet alloc] init];
333
334 // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
335 // Eliminate method calls in initializers and dealloc.
336 [self loadDefaultSplashScreenView];
337 [self performCommonViewControllerInitialization];
338 }
339
340 - (BOOL)isViewOpaque {
341 return _viewOpaque;
342 }
343
344 - (void)setViewOpaque:(BOOL)value {
345 _viewOpaque = value;
346 if (self.flutterView.layer.opaque != value) {
347 self.flutterView.layer.opaque = value;
348 [self.flutterView.layer setNeedsLayout];
349 }
350 }
351
352 #pragma mark - Common view controller initialization tasks
353
354 - (void)performCommonViewControllerInitialization {
355 if (_initialized) {
356 return;
357 }
358
359 _initialized = YES;
360 _orientationPreferences = UIInterfaceOrientationMaskAll;
361 _statusBarStyle = UIStatusBarStyleDefault;
362
363 // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
364 // Eliminate method calls in initializers and dealloc.
365 [self setUpNotificationCenterObservers];
366 }
367
368 - (void)setUpNotificationCenterObservers {
369 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
370 [center addObserver:self
371 selector:@selector(onOrientationPreferencesUpdated:)
372 name:@(flutter::kOrientationUpdateNotificationName)
373 object:nil];
374
375 [center addObserver:self
376 selector:@selector(onPreferredStatusBarStyleUpdated:)
377 name:@(flutter::kOverlayStyleUpdateNotificationName)
378 object:nil];
379
380 if (FlutterSharedApplication.isAvailable) {
381 [self setUpApplicationLifecycleNotifications:center];
382 } else {
383 [self setUpSceneLifecycleNotifications:center];
384 }
385
386 [center addObserver:self
387 selector:@selector(keyboardWillChangeFrame:)
388 name:UIKeyboardWillChangeFrameNotification
389 object:nil];
390
391 [center addObserver:self
392 selector:@selector(keyboardWillShowNotification:)
393 name:UIKeyboardWillShowNotification
394 object:nil];
395
396 [center addObserver:self
397 selector:@selector(keyboardWillBeHidden:)
398 name:UIKeyboardWillHideNotification
399 object:nil];
400
401 [center addObserver:self
402 selector:@selector(onAccessibilityStatusChanged:)
403 name:UIAccessibilityVoiceOverStatusDidChangeNotification
404 object:nil];
405
406 [center addObserver:self
407 selector:@selector(onAccessibilityStatusChanged:)
408 name:UIAccessibilitySwitchControlStatusDidChangeNotification
409 object:nil];
410
411 [center addObserver:self
412 selector:@selector(onAccessibilityStatusChanged:)
413 name:UIAccessibilitySpeakScreenStatusDidChangeNotification
414 object:nil];
415
416 [center addObserver:self
417 selector:@selector(onAccessibilityStatusChanged:)
418 name:UIAccessibilityInvertColorsStatusDidChangeNotification
419 object:nil];
420
421 [center addObserver:self
422 selector:@selector(onAccessibilityStatusChanged:)
423 name:UIAccessibilityReduceMotionStatusDidChangeNotification
424 object:nil];
425
426 [center addObserver:self
427 selector:@selector(onAccessibilityStatusChanged:)
428 name:UIAccessibilityBoldTextStatusDidChangeNotification
429 object:nil];
430
431 [center addObserver:self
432 selector:@selector(onAccessibilityStatusChanged:)
433 name:UIAccessibilityDarkerSystemColorsStatusDidChangeNotification
434 object:nil];
435
436 [center addObserver:self
437 selector:@selector(onAccessibilityStatusChanged:)
438 name:UIAccessibilityOnOffSwitchLabelsDidChangeNotification
439 object:nil];
440
441 [center addObserver:self
442 selector:@selector(onUserSettingsChanged:)
443 name:UIContentSizeCategoryDidChangeNotification
444 object:nil];
445
446 [center addObserver:self
447 selector:@selector(onHideHomeIndicatorNotification:)
448 name:FlutterViewControllerHideHomeIndicator
449 object:nil];
450
451 [center addObserver:self
452 selector:@selector(onShowHomeIndicatorNotification:)
453 name:FlutterViewControllerShowHomeIndicator
454 object:nil];
455 }
456
457 - (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) {
458 [center addObserver:self
459 selector:@selector(sceneBecameActive:)
460 name:UISceneDidActivateNotification
461 object:nil];
462
463 [center addObserver:self
464 selector:@selector(sceneWillResignActive:)
465 name:UISceneWillDeactivateNotification
466 object:nil];
467
468 [center addObserver:self
469 selector:@selector(sceneWillDisconnect:)
470 name:UISceneDidDisconnectNotification
471 object:nil];
472
473 [center addObserver:self
474 selector:@selector(sceneDidEnterBackground:)
475 name:UISceneDidEnterBackgroundNotification
476 object:nil];
477
478 [center addObserver:self
479 selector:@selector(sceneWillEnterForeground:)
480 name:UISceneWillEnterForegroundNotification
481 object:nil];
482 }
483
484 - (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center {
485 [center addObserver:self
486 selector:@selector(applicationBecameActive:)
487 name:UIApplicationDidBecomeActiveNotification
488 object:nil];
489
490 [center addObserver:self
491 selector:@selector(applicationWillResignActive:)
492 name:UIApplicationWillResignActiveNotification
493 object:nil];
494
495 [center addObserver:self
496 selector:@selector(applicationWillTerminate:)
497 name:UIApplicationWillTerminateNotification
498 object:nil];
499
500 [center addObserver:self
501 selector:@selector(applicationDidEnterBackground:)
502 name:UIApplicationDidEnterBackgroundNotification
503 object:nil];
504
505 [center addObserver:self
506 selector:@selector(applicationWillEnterForeground:)
507 name:UIApplicationWillEnterForegroundNotification
508 object:nil];
509 }
510
511 - (void)setInitialRoute:(NSString*)route {
512 [self.engine.navigationChannel invokeMethod:@"setInitialRoute" arguments:route];
513 }
514
515 - (void)popRoute {
516 [self.engine.navigationChannel invokeMethod:@"popRoute" arguments:nil];
517 }
518
519 - (void)pushRoute:(NSString*)route {
520 [self.engine.navigationChannel invokeMethod:@"pushRoute" arguments:route];
521 }
522
523 #pragma mark - Loading the view
524
525 static UIView* GetViewOrPlaceholder(UIView* existing_view) {
526 if (existing_view) {
527 return existing_view;
528 }
529
530 auto placeholder = [[UIView alloc] init];
531
532 placeholder.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
533 placeholder.backgroundColor = UIColor.systemBackgroundColor;
534 placeholder.autoresizesSubviews = YES;
535
536 // Only add the label when we know we have failed to enable tracing (and it was necessary).
537 // Otherwise, a spurious warning will be shown in cases where an engine cannot be initialized for
538 // other reasons.
539 if (flutter::GetTracingResult() == flutter::TracingResult::kDisabled) {
540 auto messageLabel = [[UILabel alloc] init];
541 messageLabel.numberOfLines = 0u;
542 messageLabel.textAlignment = NSTextAlignmentCenter;
543 messageLabel.autoresizingMask =
544 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
545 messageLabel.text =
546 @"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling, "
547 @"IDEs with Flutter plugins or from Xcode.\n\nAlternatively, build in profile or release "
548 @"modes to enable launching from the home screen.";
549 [placeholder addSubview:messageLabel];
550 }
551
552 return placeholder;
553 }
554
555 - (void)loadView {
556 self.view = GetViewOrPlaceholder(self.flutterView);
557 self.view.multipleTouchEnabled = YES;
558 self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
559
560 [self installSplashScreenViewIfNecessary];
561
562 // Create and set up the scroll view.
563 UIScrollView* scrollView = [[UIScrollView alloc] init];
564 scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
565 // The color shouldn't matter since it is offscreen.
566 scrollView.backgroundColor = UIColor.whiteColor;
567 scrollView.delegate = self;
568 // This is an arbitrary small size.
569 scrollView.contentSize = CGSizeMake(kScrollViewContentSize, kScrollViewContentSize);
570 // This is an arbitrary offset that is not CGPointZero.
571 scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
572
573 [self.view addSubview:scrollView];
574 self.scrollView = scrollView;
575 }
576
577 - (flutter::PointerData)generatePointerDataForFake {
578 flutter::PointerData pointer_data;
579 pointer_data.Clear();
580 pointer_data.kind = flutter::PointerData::DeviceKind::kTouch;
581 // `UITouch.timestamp` is defined as seconds since system startup. Synthesized events can get this
582 // time with `NSProcessInfo.systemUptime`. See
583 // https://developer.apple.com/documentation/uikit/uitouch/1618144-timestamp?language=objc
584 pointer_data.time_stamp = [[NSProcessInfo processInfo] systemUptime] * kMicrosecondsPerSecond;
585 return pointer_data;
586 }
587
588 static void SendFakeTouchEvent(UIScreen* screen,
589 FlutterEngine* engine,
590 CGPoint location,
591 flutter::PointerData::Change change) {
592 const CGFloat scale = screen.scale;
593 flutter::PointerData pointer_data = [[engine viewController] generatePointerDataForFake];
594 pointer_data.physical_x = location.x * scale;
595 pointer_data.physical_y = location.y * scale;
596 auto packet = std::make_unique<flutter::PointerDataPacket>(/*count=*/1);
597 pointer_data.change = change;
598 packet->SetPointerData(0, pointer_data);
599 [engine dispatchPointerDataPacket:std::move(packet)];
600 }
601
602 - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
603 if (!self.engine) {
604 return NO;
605 }
606 CGPoint statusBarPoint = CGPointZero;
607 UIScreen* screen = self.flutterScreenIfViewLoaded;
608 if (screen) {
609 SendFakeTouchEvent(screen, self.engine, statusBarPoint, flutter::PointerData::Change::kDown);
610 SendFakeTouchEvent(screen, self.engine, statusBarPoint, flutter::PointerData::Change::kUp);
611 }
612 return NO;
613 }
614
615 #pragma mark - Managing launch views
616
617 - (void)installSplashScreenViewIfNecessary {
618 // Show the launch screen view again on top of the FlutterView if available.
619 // This launch screen view will be removed once the first Flutter frame is rendered.
620 if (self.splashScreenView && (self.isBeingPresented || self.isMovingToParentViewController)) {
621 [self.splashScreenView removeFromSuperview];
622 self.splashScreenView = nil;
623 return;
624 }
625
626 // Use the property getter to initialize the default value.
627 UIView* splashScreenView = self.splashScreenView;
628 if (splashScreenView == nil) {
629 return;
630 }
631 splashScreenView.frame = self.view.bounds;
632 [self.view addSubview:splashScreenView];
633 }
634
635 + (BOOL)automaticallyNotifiesObserversOfDisplayingFlutterUI {
636 return NO;
637 }
638
639 - (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI {
640 if (_displayingFlutterUI != displayingFlutterUI) {
641 if (displayingFlutterUI == YES) {
642 if (!self.viewIfLoaded.window) {
643 return;
644 }
645 }
646 [self willChangeValueForKey:@"displayingFlutterUI"];
647 _displayingFlutterUI = displayingFlutterUI;
648 [self didChangeValueForKey:@"displayingFlutterUI"];
649 }
650 }
651
652 - (void)callViewRenderedCallback {
653 self.displayingFlutterUI = YES;
654 if (self.flutterViewRenderedCallback) {
655 self.flutterViewRenderedCallback();
656 self.flutterViewRenderedCallback = nil;
657 }
658 }
659
660 - (void)removeSplashScreenWithCompletion:(dispatch_block_t _Nullable)onComplete {
661 NSAssert(self.splashScreenView, @"The splash screen view must not be nil");
662 UIView* splashScreen = self.splashScreenView;
663 // setSplashScreenView calls this method. Assign directly to ivar to avoid an infinite loop.
664 _splashScreenView = nil;
665 [UIView animateWithDuration:0.2
666 animations:^{
667 splashScreen.alpha = 0;
668 }
669 completion:^(BOOL finished) {
670 [splashScreen removeFromSuperview];
671 if (onComplete) {
672 onComplete();
673 }
674 }];
675 }
676
677 - (void)onFirstFrameRendered {
678 if (self.splashScreenView) {
679 __weak FlutterViewController* weakSelf = self;
680 [self removeSplashScreenWithCompletion:^{
681 [weakSelf callViewRenderedCallback];
682 }];
683 } else {
684 [self callViewRenderedCallback];
685 }
686 }
687
688 - (void)installFirstFrameCallback {
689 if (!self.engine) {
690 return;
691 }
692 __weak FlutterViewController* weakSelf = self;
693 [self.engine installFirstFrameCallback:^{
694 [weakSelf onFirstFrameRendered];
695 }];
696 }
697
698 #pragma mark - Properties
699
700 - (int64_t)viewIdentifier {
701 // TODO(dkwingsmt): Fill the view ID property with the correct value once the
702 // iOS shell supports multiple views.
703 return flutter::kFlutterImplicitViewId;
704 }
705
706 - (BOOL)loadDefaultSplashScreenView {
707 NSString* launchscreenName =
708 [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"];
709 if (launchscreenName == nil) {
710 return NO;
711 }
712 UIView* splashView = [self splashScreenFromStoryboard:launchscreenName];
713 if (!splashView) {
714 splashView = [self splashScreenFromXib:launchscreenName];
715 }
716 if (!splashView) {
717 return NO;
718 }
719 self.splashScreenView = splashView;
720 return YES;
721 }
722
723 - (UIView*)splashScreenFromStoryboard:(NSString*)name {
724 UIStoryboard* storyboard = nil;
725 @try {
726 storyboard = [UIStoryboard storyboardWithName:name bundle:nil];
727 } @catch (NSException* exception) {
728 return nil;
729 }
730 if (storyboard) {
731 UIViewController* splashScreenViewController = [storyboard instantiateInitialViewController];
732 return splashScreenViewController.view;
733 }
734 return nil;
735 }
736
737 - (UIView*)splashScreenFromXib:(NSString*)name {
738 NSArray* objects = nil;
739 @try {
740 objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil];
741 } @catch (NSException* exception) {
742 return nil;
743 }
744 if ([objects count] != 0) {
745 UIView* view = [objects objectAtIndex:0];
746 return view;
747 }
748 return nil;
749 }
750
751 - (void)setSplashScreenView:(UIView*)view {
752 if (view == _splashScreenView) {
753 return;
754 }
755
756 // Special case: user wants to remove the splash screen view.
757 if (!view) {
758 if (_splashScreenView) {
759 [self removeSplashScreenWithCompletion:nil];
760 }
761 return;
762 }
763
764 _splashScreenView = view;
765 _splashScreenView.autoresizingMask =
766 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
767 }
768
769 - (void)setFlutterViewDidRenderCallback:(void (^)(void))callback {
770 _flutterViewRenderedCallback = callback;
771 }
772
773 - (UISceneActivationState)activationState {
774 return self.flutterWindowSceneIfViewLoaded.activationState;
775 }
776
777 - (BOOL)stateIsActive {
778 // [UIApplication sharedApplication API is not available for app extension.
779 UIApplication* flutterApplication = FlutterSharedApplication.application;
780 BOOL isActive = flutterApplication
781 ? [self isApplicationStateMatching:UIApplicationStateActive
782 withApplication:flutterApplication]
783 : [self isSceneStateMatching:UISceneActivationStateForegroundActive];
784 return isActive;
785 }
786
787 - (BOOL)stateIsBackground {
788 // [UIApplication sharedApplication API is not available for app extension.
789 UIApplication* flutterApplication = FlutterSharedApplication.application;
790 return flutterApplication ? [self isApplicationStateMatching:UIApplicationStateBackground
791 withApplication:flutterApplication]
792 : [self isSceneStateMatching:UISceneActivationStateBackground];
793 }
794
795 - (BOOL)isApplicationStateMatching:(UIApplicationState)match
796 withApplication:(UIApplication*)application {
797 switch (application.applicationState) {
798 case UIApplicationStateActive:
799 case UIApplicationStateInactive:
800 case UIApplicationStateBackground:
801 return application.applicationState == match;
802 }
803 }
804
805 - (BOOL)isSceneStateMatching:(UISceneActivationState)match API_AVAILABLE(ios(13.0)) {
806 switch (self.activationState) {
807 case UISceneActivationStateForegroundActive:
808 case UISceneActivationStateUnattached:
809 case UISceneActivationStateForegroundInactive:
810 case UISceneActivationStateBackground:
811 return self.activationState == match;
812 }
813 }
814
815 #pragma mark - Surface creation and teardown updates
816
817 - (void)surfaceUpdated:(BOOL)appeared {
818 if (!self.engine) {
819 return;
820 }
821
822 // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and raster
823 // thread.
824 if (appeared) {
825 [self installFirstFrameCallback];
826 self.platformViewsController.flutterView = self.flutterView;
827 self.platformViewsController.flutterViewController = self;
828 [self.engine notifyViewCreated];
829 } else {
830 self.displayingFlutterUI = NO;
831 [self.engine notifyViewDestroyed];
832 self.platformViewsController.flutterView = nil;
833 self.platformViewsController.flutterViewController = nil;
834 }
835 }
836
837 #pragma mark - UIViewController lifecycle notifications
838
839 - (void)viewDidLoad {
840 TRACE_EVENT0("flutter", "viewDidLoad");
841
842 if (self.engine && self.engineNeedsLaunch) {
843 [self.engine launchEngine:nil libraryURI:nil entrypointArgs:nil];
844 [self.engine setViewController:self];
845 self.engineNeedsLaunch = NO;
846 } else if (self.engine.viewController == self) {
847 [self.engine attachView];
848 }
849
850 // Register internal plugins.
851 [self addInternalPlugins];
852
853 // Create a vsync client to correct delivery frame rate of touch events if needed.
854 [self createTouchRateCorrectionVSyncClientIfNeeded];
855
856 if (@available(iOS 13.4, *)) {
857 _hoverGestureRecognizer =
858 [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)];
859 _hoverGestureRecognizer.delegate = self;
860 [self.flutterView addGestureRecognizer:_hoverGestureRecognizer];
861
862 _discreteScrollingPanGestureRecognizer =
863 [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(discreteScrollEvent:)];
864 _discreteScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
865 // Disallowing all touch types. If touch events are allowed here, touches to the screen will be
866 // consumed by the UIGestureRecognizer instead of being passed through to flutter via
867 // touchesBegan. Trackpad and mouse scrolls are sent by the platform as scroll events rather
868 // than touch events, so they will still be received.
869 _discreteScrollingPanGestureRecognizer.allowedTouchTypes = @[];
870 _discreteScrollingPanGestureRecognizer.delegate = self;
871 [self.flutterView addGestureRecognizer:_discreteScrollingPanGestureRecognizer];
872 _continuousScrollingPanGestureRecognizer =
873 [[UIPanGestureRecognizer alloc] initWithTarget:self
874 action:@selector(continuousScrollEvent:)];
875 _continuousScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
876 _continuousScrollingPanGestureRecognizer.allowedTouchTypes = @[];
877 _continuousScrollingPanGestureRecognizer.delegate = self;
878 [self.flutterView addGestureRecognizer:_continuousScrollingPanGestureRecognizer];
879 _pinchGestureRecognizer =
880 [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchEvent:)];
881 _pinchGestureRecognizer.allowedTouchTypes = @[];
882 _pinchGestureRecognizer.delegate = self;
883 [self.flutterView addGestureRecognizer:_pinchGestureRecognizer];
884 _rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] init];
885 _rotationGestureRecognizer.allowedTouchTypes = @[];
886 _rotationGestureRecognizer.delegate = self;
887 [self.flutterView addGestureRecognizer:_rotationGestureRecognizer];
888 }
889
890 [super viewDidLoad];
891 }
892
893 - (void)addInternalPlugins {
894 self.keyboardManager = [[FlutterKeyboardManager alloc] init];
895 __weak FlutterViewController* weakSelf = self;
896 FlutterSendKeyEvent sendEvent =
897 ^(const FlutterKeyEvent& event, FlutterKeyEventCallback callback, void* userData) {
898 [weakSelf.engine sendKeyEvent:event callback:callback userData:userData];
899 };
900 [self.keyboardManager
901 addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] initWithSendEvent:sendEvent]];
902 FlutterChannelKeyResponder* responder =
903 [[FlutterChannelKeyResponder alloc] initWithChannel:self.engine.keyEventChannel];
904 [self.keyboardManager addPrimaryResponder:responder];
905 FlutterTextInputPlugin* textInputPlugin = self.engine.textInputPlugin;
906 if (textInputPlugin != nil) {
907 [self.keyboardManager addSecondaryResponder:textInputPlugin];
908 }
909 if (self.engine.viewController == self) {
910 [textInputPlugin setUpIndirectScribbleInteraction:self];
911 }
912 }
913
914 - (void)removeInternalPlugins {
915 self.keyboardManager = nil;
916 }
917
918 - (void)viewWillAppear:(BOOL)animated {
919 TRACE_EVENT0("flutter", "viewWillAppear");
920 if (self.engine.viewController == self) {
921 // Send platform settings to Flutter, e.g., platform brightness.
922 [self onUserSettingsChanged:nil];
923
924 // Only recreate surface on subsequent appearances when viewport metrics are known.
925 // First time surface creation is done on viewDidLayoutSubviews.
926 if (_viewportMetrics.physical_width) {
927 [self surfaceUpdated:YES];
928 }
929 [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"];
930 [self.engine.restorationPlugin markRestorationComplete];
931 }
932
933 [super viewWillAppear:animated];
934 }
935
936 - (void)viewDidAppear:(BOOL)animated {
937 TRACE_EVENT0("flutter", "viewDidAppear");
938 if (self.engine.viewController == self) {
939 [self onUserSettingsChanged:nil];
940 [self onAccessibilityStatusChanged:nil];
941
942 if (self.stateIsActive) {
943 [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.resumed"];
944 }
945 }
946 [super viewDidAppear:animated];
947 }
948
949 - (void)viewWillDisappear:(BOOL)animated {
950 TRACE_EVENT0("flutter", "viewWillDisappear");
951 if (self.engine.viewController == self) {
952 [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"];
953 }
954 [super viewWillDisappear:animated];
955 }
956
957 - (void)viewDidDisappear:(BOOL)animated {
958 TRACE_EVENT0("flutter", "viewDidDisappear");
959 if (self.engine.viewController == self) {
960 [self invalidateKeyboardAnimationVSyncClient];
961 [self ensureViewportMetricsIsCorrect];
962 [self surfaceUpdated:NO];
963 [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.paused"];
964 [self flushOngoingTouches];
965 [self.engine notifyLowMemory];
966 }
967
968 [super viewDidDisappear:animated];
969 }
970
971 - (void)viewWillTransitionToSize:(CGSize)size
972 withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
973 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
974
975 // We delay the viewport metrics update for half of rotation transition duration, to address
976 // a bug with distorted aspect ratio.
977 // See: https://github.com/flutter/flutter/issues/16322
978 //
979 // This approach does not fully resolve all distortion problem. But instead, it reduces the
980 // rotation distortion roughly from 4x to 2x. The most distorted frames occur in the middle
981 // of the transition when it is rotating the fastest, making it hard to notice.
982
983 NSTimeInterval transitionDuration = coordinator.transitionDuration;
984 // Do not delay viewport metrics update if zero transition duration.
985 if (transitionDuration == 0) {
986 return;
987 }
988
989 __weak FlutterViewController* weakSelf = self;
990 _shouldIgnoreViewportMetricsUpdatesDuringRotation = YES;
991 dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
992 static_cast<int64_t>(transitionDuration / 2.0 * NSEC_PER_SEC)),
993 dispatch_get_main_queue(), ^{
994 FlutterViewController* strongSelf = weakSelf;
995 if (!strongSelf) {
996 return;
997 }
998
999 // `viewWillTransitionToSize` is only called after the previous rotation is
1000 // complete. So there won't be race condition for this flag.
1001 strongSelf.shouldIgnoreViewportMetricsUpdatesDuringRotation = NO;
1002 [strongSelf updateViewportMetricsIfNeeded];
1003 });
1004 }
1005
1006 - (void)flushOngoingTouches {
1007 if (self.engine && self.ongoingTouches.count > 0) {
1008 auto packet = std::make_unique<flutter::PointerDataPacket>(self.ongoingTouches.count);
1009 size_t pointer_index = 0;
1010 // If the view controller is going away, we want to flush cancel all the ongoing
1011 // touches to the framework so nothing gets orphaned.
1012 for (NSNumber* device in self.ongoingTouches) {
1013 // Create fake PointerData to balance out each previously started one for the framework.
1014 flutter::PointerData pointer_data = [self generatePointerDataForFake];
1015
1016 pointer_data.change = flutter::PointerData::Change::kCancel;
1017 pointer_data.device = device.longLongValue;
1018 pointer_data.pointer_identifier = 0;
1019 pointer_data.view_id = self.viewIdentifier;
1020
1021 // Anything we put here will be arbitrary since there are no touches.
1022 pointer_data.physical_x = 0;
1023 pointer_data.physical_y = 0;
1024 pointer_data.physical_delta_x = 0.0;
1025 pointer_data.physical_delta_y = 0.0;
1026 pointer_data.pressure = 1.0;
1027 pointer_data.pressure_max = 1.0;
1028
1029 packet->SetPointerData(pointer_index++, pointer_data);
1030 }
1031
1032 [self.ongoingTouches removeAllObjects];
1033 [self.engine dispatchPointerDataPacket:std::move(packet)];
1034 }
1035 }
1036
1037 - (void)deregisterNotifications {
1038 [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerWillDealloc
1039 object:self
1040 userInfo:nil];
1041 [[NSNotificationCenter defaultCenter] removeObserver:self];
1042 }
1043
1044 - (void)dealloc {
1045 // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
1046 // Eliminate method calls in initializers and dealloc.
1047 [self removeInternalPlugins];
1048 [self deregisterNotifications];
1049
1050 [self invalidateKeyboardAnimationVSyncClient];
1051 [self invalidateTouchRateCorrectionVSyncClient];
1052
1053 // TODO(cbracken): https://github.com/flutter/flutter/issues/156222
1054 // Ensure all delegates are weak and remove this.
1055 _scrollView.delegate = nil;
1056 _hoverGestureRecognizer.delegate = nil;
1057 _discreteScrollingPanGestureRecognizer.delegate = nil;
1058 _continuousScrollingPanGestureRecognizer.delegate = nil;
1059 _pinchGestureRecognizer.delegate = nil;
1060 _rotationGestureRecognizer.delegate = nil;
1061 }
1062
1063 #pragma mark - Application lifecycle notifications
1064
1065 - (void)applicationBecameActive:(NSNotification*)notification {
1066 TRACE_EVENT0("flutter", "applicationBecameActive");
1067 [self appOrSceneBecameActive];
1068 }
1069
1070 - (void)applicationWillResignActive:(NSNotification*)notification {
1071 TRACE_EVENT0("flutter", "applicationWillResignActive");
1072 [self appOrSceneWillResignActive];
1073 }
1074
1075 - (void)applicationWillTerminate:(NSNotification*)notification {
1076 [self appOrSceneWillTerminate];
1077 }
1078
1079 - (void)applicationDidEnterBackground:(NSNotification*)notification {
1080 TRACE_EVENT0("flutter", "applicationDidEnterBackground");
1081 [self appOrSceneDidEnterBackground];
1082 }
1083
1084 - (void)applicationWillEnterForeground:(NSNotification*)notification {
1085 TRACE_EVENT0("flutter", "applicationWillEnterForeground");
1086 [self appOrSceneWillEnterForeground];
1087 }
1088
1089 #pragma mark - Scene lifecycle notifications
1090
1091 - (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1092 TRACE_EVENT0("flutter", "sceneBecameActive");
1093 [self appOrSceneBecameActive];
1094 }
1095
1096 - (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1097 TRACE_EVENT0("flutter", "sceneWillResignActive");
1098 [self appOrSceneWillResignActive];
1099 }
1100
1101 - (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1102 [self appOrSceneWillTerminate];
1103 }
1104
1105 - (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1106 TRACE_EVENT0("flutter", "sceneDidEnterBackground");
1107 [self appOrSceneDidEnterBackground];
1108 }
1109
1110 - (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1111 TRACE_EVENT0("flutter", "sceneWillEnterForeground");
1112 [self appOrSceneWillEnterForeground];
1113 }
1114
1115 #pragma mark - Lifecycle shared
1116
1117 - (void)appOrSceneBecameActive {
1118 self.isKeyboardInOrTransitioningFromBackground = NO;
1119 if (_viewportMetrics.physical_width) {
1120 [self surfaceUpdated:YES];
1121 }
1122 [self performSelector:@selector(goToApplicationLifecycle:)
1123 withObject:@"AppLifecycleState.resumed"
1124 afterDelay:0.0f];
1125 }
1126
1127 - (void)appOrSceneWillResignActive {
1128 [NSObject cancelPreviousPerformRequestsWithTarget:self
1129 selector:@selector(goToApplicationLifecycle:)
1130 object:@"AppLifecycleState.resumed"];
1131 [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
1132 }
1133
1134 - (void)appOrSceneWillTerminate {
1135 [self goToApplicationLifecycle:@"AppLifecycleState.detached"];
1136 [self.engine destroyContext];
1137 }
1138
1139 - (void)appOrSceneDidEnterBackground {
1140 self.isKeyboardInOrTransitioningFromBackground = YES;
1141 [self surfaceUpdated:NO];
1142 [self goToApplicationLifecycle:@"AppLifecycleState.paused"];
1143 }
1144
1145 - (void)appOrSceneWillEnterForeground {
1146 [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
1147 }
1148
1149 // Make this transition only while this current view controller is visible.
1150 - (void)goToApplicationLifecycle:(nonnull NSString*)state {
1151 // Accessing self.view will create the view. Instead use viewIfLoaded
1152 // to check whether the view is attached to window.
1153 if (self.viewIfLoaded.window) {
1154 [self.engine.lifecycleChannel sendMessage:state];
1155 }
1156 }
1157
1158 #pragma mark - Touch event handling
1159
1160 static flutter::PointerData::Change PointerDataChangeFromUITouchPhase(UITouchPhase phase) {
1161 switch (phase) {
1162 case UITouchPhaseBegan:
1163 return flutter::PointerData::Change::kDown;
1164 case UITouchPhaseMoved:
1165 case UITouchPhaseStationary:
1166 // There is no EVENT_TYPE_POINTER_STATIONARY. So we just pass a move type
1167 // with the same coordinates
1168 return flutter::PointerData::Change::kMove;
1169 case UITouchPhaseEnded:
1170 return flutter::PointerData::Change::kUp;
1171 case UITouchPhaseCancelled:
1172 return flutter::PointerData::Change::kCancel;
1173 default:
1174 // TODO(53695): Handle the `UITouchPhaseRegion`... enum values.
1175 FML_DLOG(INFO) << "Unhandled touch phase: " << phase;
1176 break;
1177 }
1178
1179 return flutter::PointerData::Change::kCancel;
1180 }
1181
1182 static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) {
1183 switch (touch.type) {
1184 case UITouchTypeDirect:
1185 case UITouchTypeIndirect:
1186 return flutter::PointerData::DeviceKind::kTouch;
1187 case UITouchTypeStylus:
1188 return flutter::PointerData::DeviceKind::kStylus;
1189 case UITouchTypeIndirectPointer:
1190 return flutter::PointerData::DeviceKind::kMouse;
1191 default:
1192 FML_DLOG(INFO) << "Unhandled touch type: " << touch.type;
1193 break;
1194 }
1195
1196 return flutter::PointerData::DeviceKind::kTouch;
1197 }
1198
1199 // Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined
1200 // from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events
1201 // in the status bar area are available to framework code. The change type (optional) of the faked
1202 // touch is specified in the second argument.
1203 - (void)dispatchTouches:(NSSet*)touches
1204 pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change
1205 event:(UIEvent*)event {
1206 if (!self.engine) {
1207 return;
1208 }
1209
1210 // If the UIApplicationSupportsIndirectInputEvents in Info.plist returns YES, then the platform
1211 // dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of
1212 // UITouchTypeIndirectPointer and different identifiers for each click. They are translated into
1213 // Flutter pointer events with type of kMouse and different device IDs. These devices must be
1214 // terminated with kRemove events when the touches end, otherwise they will keep triggering hover
1215 // events.
1216 //
1217 // If the UIApplicationSupportsIndirectInputEvents in Info.plist returns NO, then the platform
1218 // dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of
1219 // UITouchTypeIndirectPointer and different identifiers for each click. They are translated into
1220 // Flutter pointer events with type of kTouch and different device IDs. Removing these devices is
1221 // neither necessary nor harmful.
1222 //
1223 // Therefore Flutter always removes these devices. The touches_to_remove_count tracks how many
1224 // remove events are needed in this group of touches to properly allocate space for the packet.
1225 // The remove event of a touch is synthesized immediately after its normal event.
1226 //
1227 // See also:
1228 // https://developer.apple.com/documentation/uikit/pointer_interactions?language=objc
1229 // https://developer.apple.com/documentation/bundleresources/information_property_list/uiapplicationsupportsindirectinputevents?language=objc
1230 NSUInteger touches_to_remove_count = 0;
1231 for (UITouch* touch in touches) {
1232 if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
1233 touches_to_remove_count++;
1234 }
1235 }
1236
1237 // Activate or pause the correction of delivery frame rate of touch events.
1238 [self triggerTouchRateCorrectionIfNeeded:touches];
1239
1240 const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
1241 auto packet =
1242 std::make_unique<flutter::PointerDataPacket>(touches.count + touches_to_remove_count);
1243
1244 size_t pointer_index = 0;
1245
1246 for (UITouch* touch in touches) {
1247 CGPoint windowCoordinates = [touch locationInView:self.view];
1248
1249 flutter::PointerData pointer_data;
1250 pointer_data.Clear();
1251
1252 constexpr int kMicrosecondsPerSecond = 1000 * 1000;
1253 pointer_data.time_stamp = touch.timestamp * kMicrosecondsPerSecond;
1254
1255 pointer_data.change = overridden_change != nullptr
1256 ? *overridden_change
1257 : PointerDataChangeFromUITouchPhase(touch.phase);
1258
1259 pointer_data.kind = DeviceKindFromTouchType(touch);
1260
1261 pointer_data.device = reinterpret_cast<int64_t>(touch);
1262
1263 pointer_data.view_id = self.viewIdentifier;
1264
1265 // Pointer will be generated in pointer_data_packet_converter.cc.
1266 pointer_data.pointer_identifier = 0;
1267
1268 pointer_data.physical_x = windowCoordinates.x * scale;
1269 pointer_data.physical_y = windowCoordinates.y * scale;
1270
1271 // Delta will be generated in pointer_data_packet_converter.cc.
1272 pointer_data.physical_delta_x = 0.0;
1273 pointer_data.physical_delta_y = 0.0;
1274
1275 NSNumber* deviceKey = [NSNumber numberWithLongLong:pointer_data.device];
1276 // Track touches that began and not yet stopped so we can flush them
1277 // if the view controller goes away.
1278 switch (pointer_data.change) {
1279 case flutter::PointerData::Change::kDown:
1280 [self.ongoingTouches addObject:deviceKey];
1281 break;
1282 case flutter::PointerData::Change::kCancel:
1283 case flutter::PointerData::Change::kUp:
1284 [self.ongoingTouches removeObject:deviceKey];
1285 break;
1286 case flutter::PointerData::Change::kHover:
1287 case flutter::PointerData::Change::kMove:
1288 // We're only tracking starts and stops.
1289 break;
1290 case flutter::PointerData::Change::kAdd:
1291 case flutter::PointerData::Change::kRemove:
1292 // We don't use kAdd/kRemove.
1293 break;
1294 case flutter::PointerData::Change::kPanZoomStart:
1295 case flutter::PointerData::Change::kPanZoomUpdate:
1296 case flutter::PointerData::Change::kPanZoomEnd:
1297 // We don't send pan/zoom events here
1298 break;
1299 }
1300
1301 // pressure_min is always 0.0
1302 pointer_data.pressure = touch.force;
1303 pointer_data.pressure_max = touch.maximumPossibleForce;
1304 pointer_data.radius_major = touch.majorRadius;
1305 pointer_data.radius_min = touch.majorRadius - touch.majorRadiusTolerance;
1306 pointer_data.radius_max = touch.majorRadius + touch.majorRadiusTolerance;
1307
1308 // iOS Documentation: altitudeAngle
1309 // A value of 0 radians indicates that the stylus is parallel to the surface. The value of
1310 // this property is Pi/2 when the stylus is perpendicular to the surface.
1311 //
1312 // PointerData Documentation: tilt
1313 // The angle of the stylus, in radians in the range:
1314 // 0 <= tilt <= pi/2
1315 // giving the angle of the axis of the stylus, relative to the axis perpendicular to the input
1316 // surface (thus 0.0 indicates the stylus is orthogonal to the plane of the input surface,
1317 // while pi/2 indicates that the stylus is flat on that surface).
1318 //
1319 // Discussion:
1320 // The ranges are the same. Origins are swapped.
1321 pointer_data.tilt = M_PI_2 - touch.altitudeAngle;
1322
1323 // iOS Documentation: azimuthAngleInView:
1324 // With the tip of the stylus touching the screen, the value of this property is 0 radians
1325 // when the cap end of the stylus (that is, the end opposite of the tip) points along the
1326 // positive x axis of the device's screen. The azimuth angle increases as the user swings the
1327 // cap end of the stylus in a clockwise direction around the tip.
1328 //
1329 // PointerData Documentation: orientation
1330 // The angle of the stylus, in radians in the range:
1331 // -pi < orientation <= pi
1332 // giving the angle of the axis of the stylus projected onto the input surface, relative to
1333 // the positive y-axis of that surface (thus 0.0 indicates the stylus, if projected onto that
1334 // surface, would go from the contact point vertically up in the positive y-axis direction, pi
1335 // would indicate that the stylus would go down in the negative y-axis direction; pi/4 would
1336 // indicate that the stylus goes up and to the right, -pi/2 would indicate that the stylus
1337 // goes to the left, etc).
1338 //
1339 // Discussion:
1340 // Sweep direction is the same. Phase of M_PI_2.
1341 pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2;
1342
1343 if (@available(iOS 13.4, *)) {
1344 if (event != nullptr) {
1345 pointer_data.buttons = (((event.buttonMask & UIEventButtonMaskPrimary) > 0)
1346 ? flutter::PointerButtonMouse::kPointerButtonMousePrimary
1347 : 0) |
1348 (((event.buttonMask & UIEventButtonMaskSecondary) > 0)
1349 ? flutter::PointerButtonMouse::kPointerButtonMouseSecondary
1350 : 0);
1351 }
1352 }
1353
1354 packet->SetPointerData(pointer_index++, pointer_data);
1355
1356 if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
1357 flutter::PointerData remove_pointer_data = pointer_data;
1358 remove_pointer_data.change = flutter::PointerData::Change::kRemove;
1359 packet->SetPointerData(pointer_index++, remove_pointer_data);
1360 }
1361 }
1362
1363 [self.engine dispatchPointerDataPacket:std::move(packet)];
1364 }
1365
1366 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1367 [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1368 }
1369
1370 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1371 [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1372 }
1373
1374 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1375 [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1376 }
1377
1378 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1379 [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1380 }
1381
1382 - (void)forceTouchesCancelled:(NSSet*)touches {
1383 flutter::PointerData::Change cancel = flutter::PointerData::Change::kCancel;
1384 [self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
1385 }
1386
1387 #pragma mark - Touch events rate correction
1388
1389 - (void)createTouchRateCorrectionVSyncClientIfNeeded {
1390 if (_touchRateCorrectionVSyncClient != nil) {
1391 return;
1392 }
1393
1394 double displayRefreshRate = DisplayLinkManager.displayRefreshRate;
1395 const double epsilon = 0.1;
1396 if (displayRefreshRate < 60.0 + epsilon) { // displayRefreshRate <= 60.0
1397
1398 // If current device's max frame rate is not larger than 60HZ, the delivery rate of touch events
1399 // is the same with render vsync rate. So it is unnecessary to create
1400 // _touchRateCorrectionVSyncClient to correct touch callback's rate.
1401 return;
1402 }
1403
1404 auto callback = [](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1405 // Do nothing in this block. Just trigger system to callback touch events with correct rate.
1406 };
1407 _touchRateCorrectionVSyncClient =
1408 [[VSyncClient alloc] initWithTaskRunner:self.engine.platformTaskRunner callback:callback];
1409 _touchRateCorrectionVSyncClient.allowPauseAfterVsync = NO;
1410 }
1411
1412 - (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches {
1413 if (_touchRateCorrectionVSyncClient == nil) {
1414 // If the _touchRateCorrectionVSyncClient is not created, means current devices doesn't
1415 // need to correct the touch rate. So just return.
1416 return;
1417 }
1418
1419 // As long as there is a touch's phase is UITouchPhaseBegan or UITouchPhaseMoved,
1420 // activate the correction. Otherwise pause the correction.
1421 BOOL isUserInteracting = NO;
1422 for (UITouch* touch in touches) {
1423 if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
1424 isUserInteracting = YES;
1425 break;
1426 }
1427 }
1428
1429 if (isUserInteracting && self.engine.viewController == self) {
1430 [_touchRateCorrectionVSyncClient await];
1431 } else {
1432 [_touchRateCorrectionVSyncClient pause];
1433 }
1434 }
1435
1436 - (void)invalidateTouchRateCorrectionVSyncClient {
1437 [_touchRateCorrectionVSyncClient invalidate];
1438 _touchRateCorrectionVSyncClient = nil;
1439 }
1440
1441 #pragma mark - Handle view resizing
1442
1443 - (void)updateViewportMetricsIfNeeded {
1444 if (_shouldIgnoreViewportMetricsUpdatesDuringRotation) {
1445 return;
1446 }
1447 if (self.engine.viewController == self) {
1448 [self.engine updateViewportMetrics:_viewportMetrics];
1449 }
1450 }
1451
1452 - (void)viewDidLayoutSubviews {
1453 CGRect viewBounds = self.view.bounds;
1454 CGFloat scale = self.flutterScreenIfViewLoaded.scale;
1455
1456 // Purposefully place this not visible.
1457 self.scrollView.frame = CGRectMake(0.0, 0.0, viewBounds.size.width, 0.0);
1458 self.scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
1459
1460 // First time since creation that the dimensions of its view is known.
1461 bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
1462 _viewportMetrics.device_pixel_ratio = scale;
1463 [self setViewportMetricsSize];
1464 [self checkAndUpdateAutoResizeConstraints];
1465 [self setViewportMetricsPaddings];
1466 [self updateViewportMetricsIfNeeded];
1467
1468 // There is no guarantee that UIKit will layout subviews when the application/scene is active.
1469 // Creating the surface when inactive will cause GPU accesses from the background. Only wait for
1470 // the first frame to render when the application/scene is actually active.
1471 // This must run after updateViewportMetrics so that the surface creation tasks are queued after
1472 // the viewport metrics update tasks.
1473 if (firstViewBoundsUpdate && self.stateIsActive && self.engine) {
1474 [self surfaceUpdated:YES];
1475 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
1476 NSTimeInterval timeout = 0.2;
1477 #else
1478 NSTimeInterval timeout = 0.1;
1479 #endif
1480 [self.engine
1481 waitForFirstFrameSync:timeout
1482 callback:^(BOOL didTimeout) {
1483 if (didTimeout) {
1484 [FlutterLogger logInfo:@"Timeout waiting for the first frame to render. "
1485 "This may happen in unoptimized builds. If this is"
1486 "a release build, you should load a less complex "
1487 "frame to avoid the timeout."];
1488 }
1489 }];
1490 }
1491 }
1492
1493 - (BOOL)isAutoResizable {
1494 return self.flutterView.autoResizable;
1495 }
1496
1497 - (void)setAutoResizable:(BOOL)value {
1498 self.flutterView.autoResizable = value;
1499 self.flutterView.contentMode = UIViewContentModeCenter;
1500 }
1501
1502 - (void)checkAndUpdateAutoResizeConstraints {
1503 if (!self.isAutoResizable) {
1504 return;
1505 }
1506
1507 [self updateAutoResizeConstraints];
1508 }
1509
1510 /**
1511 * Updates the FlutterAutoResizeLayoutConstraints based on the view's
1512 * current frame.
1513 *
1514 * This method is invoked during viewDidLayoutSubviews, at which point the
1515 * view has completed its subview layout and applied any existing Auto Layout
1516 * constraints.
1517 *
1518 * Initially, the view's frame is used to determine the maximum size allowed
1519 * by the native layout system. This size is then used to establish the viewport
1520 * constraints for the Flutter engine.
1521 *
1522 * A critical consideration is that this initial frame-based sizing is only
1523 * applicable if FlutterAutoResizeLayoutConstraints have not yet been applied
1524 * by Flutter. Once Flutter applies its own FlutterAutoResizeLayoutConstraints,
1525 * these constraints will subsequently dictate the view's frame.
1526 *
1527 * This interaction imposes a limitation: native layout constraints that are
1528 * updated after Flutter has applied its auto-resize constraints may not
1529 * function as expected or properly influence the FlutterView's size.
1530 */
1531 - (void)updateAutoResizeConstraints {
1532 BOOL hasBeenAutoResized = NO;
1533 for (NSLayoutConstraint* constraint in self.view.constraints) {
1534 if ([constraint isKindOfClass:[FlutterAutoResizeLayoutConstraint class]]) {
1535 hasBeenAutoResized = YES;
1536 break;
1537 }
1538 }
1539 if (!hasBeenAutoResized) {
1540 self.sizeBeforeAutoResized = self.view.frame.size;
1541 }
1542
1543 CGFloat maxWidth = self.sizeBeforeAutoResized.width;
1544 CGFloat maxHeight = self.sizeBeforeAutoResized.height;
1545 CGFloat minWidth = self.sizeBeforeAutoResized.width;
1546 CGFloat minHeight = self.sizeBeforeAutoResized.height;
1547
1548 // maxWidth or maxHeight may be 0 when the width/height are ambiguous, eg. for
1549 // unsized widgets
1550 if (maxWidth == 0) {
1551 maxWidth = CGFLOAT_MAX;
1552 [FlutterLogger
1553 logWarning:
1554 @"Warning: The outermost widget in the autoresizable Flutter view is unsized or has "
1555 @"ambiguous dimensions, causing the host native view's width to be 0. The autoresizing "
1556 @"logic is setting the viewport constraint to unbounded DBL_MAX to prevent "
1557 @"rendering failure. Please ensure your top-level Flutter widget has explicit "
1558 @"constraints (e.g., using SizedBox or Container)."];
1559 }
1560 if (maxHeight == 0) {
1561 maxHeight = CGFLOAT_MAX;
1562 [FlutterLogger
1563 logWarning:
1564 @"Warning: The outermost widget in the autoresizable Flutter view is unsized or has "
1565 @"ambiguous dimensions, causing the host native view's width to be 0. The autoresizing "
1566 @"logic is setting the viewport constraint to unbounded DBL_MAX to prevent "
1567 @"rendering failure. Please ensure your top-level Flutter widget has explicit "
1568 @"constraints (e.g., using SizedBox or Container)."];
1569 }
1570 _viewportMetrics.physical_min_width_constraint = minWidth * _viewportMetrics.device_pixel_ratio;
1571 _viewportMetrics.physical_max_width_constraint = maxWidth * _viewportMetrics.device_pixel_ratio;
1572 _viewportMetrics.physical_min_height_constraint = minHeight * _viewportMetrics.device_pixel_ratio;
1573 _viewportMetrics.physical_max_height_constraint = maxHeight * _viewportMetrics.device_pixel_ratio;
1574 }
1575
1576 - (void)viewSafeAreaInsetsDidChange {
1577 [self setViewportMetricsPaddings];
1578 [self updateViewportMetricsIfNeeded];
1579 [super viewSafeAreaInsetsDidChange];
1580 }
1581
1582 // Set _viewportMetrics physical size.
1583 - (void)setViewportMetricsSize {
1584 UIScreen* screen = self.flutterScreenIfViewLoaded;
1585 if (!screen) {
1586 return;
1587 }
1588
1589 CGFloat scale = screen.scale;
1590 _viewportMetrics.physical_width = self.view.bounds.size.width * scale;
1591 _viewportMetrics.physical_height = self.view.bounds.size.height * scale;
1592 // TODO(louisehsu): update for https://github.com/flutter/flutter/issues/169147
1593 _viewportMetrics.physical_min_width_constraint = _viewportMetrics.physical_width;
1594 _viewportMetrics.physical_max_width_constraint = _viewportMetrics.physical_width;
1595 _viewportMetrics.physical_min_height_constraint = _viewportMetrics.physical_height;
1596 _viewportMetrics.physical_max_height_constraint = _viewportMetrics.physical_height;
1597 }
1598
1599 // Set _viewportMetrics physical paddings.
1600 //
1601 // Viewport paddings represent the iOS safe area insets.
1602 - (void)setViewportMetricsPaddings {
1603 UIScreen* screen = self.flutterScreenIfViewLoaded;
1604 if (!screen) {
1605 return;
1606 }
1607
1608 CGFloat scale = screen.scale;
1609 _viewportMetrics.physical_padding_top = self.view.safeAreaInsets.top * scale;
1610 _viewportMetrics.physical_padding_left = self.view.safeAreaInsets.left * scale;
1611 _viewportMetrics.physical_padding_right = self.view.safeAreaInsets.right * scale;
1612 _viewportMetrics.physical_padding_bottom = self.view.safeAreaInsets.bottom * scale;
1613 }
1614
1615 #pragma mark - Keyboard events
1616
1617 - (void)keyboardWillShowNotification:(NSNotification*)notification {
1618 // Immediately prior to a docked keyboard being shown or when a keyboard goes from
1619 // undocked/floating to docked, this notification is triggered. This notification also happens
1620 // when Minimized/Expanded Shortcuts bar is dropped after dragging (the keyboard's end frame will
1621 // be CGRectZero).
1622 [self handleKeyboardNotification:notification];
1623 }
1624
1625 - (void)keyboardWillChangeFrame:(NSNotification*)notification {
1626 // Immediately prior to a change in keyboard frame, this notification is triggered.
1627 // Sometimes when the keyboard is being hidden or undocked, this notification's keyboard's end
1628 // frame is not yet entirely out of screen, which is why we also use
1629 // UIKeyboardWillHideNotification.
1630 [self handleKeyboardNotification:notification];
1631 }
1632
1633 - (void)keyboardWillBeHidden:(NSNotification*)notification {
1634 // When keyboard is hidden or undocked, this notification will be triggered.
1635 // This notification might not occur when the keyboard is changed from docked to floating, which
1636 // is why we also use UIKeyboardWillChangeFrameNotification.
1637 [self handleKeyboardNotification:notification];
1638 }
1639
1640 - (void)handleKeyboardNotification:(NSNotification*)notification {
1641 // See https://flutter.dev/go/ios-keyboard-calculating-inset for more details
1642 // on why notifications are used and how things are calculated.
1643 if ([self shouldIgnoreKeyboardNotification:notification]) {
1644 return;
1645 }
1646
1647 NSDictionary* info = notification.userInfo;
1648 CGRect beginKeyboardFrame = [info[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
1649 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
1650 FlutterKeyboardMode keyboardMode = [self calculateKeyboardAttachMode:notification];
1651 CGFloat calculatedInset = [self calculateKeyboardInset:keyboardFrame keyboardMode:keyboardMode];
1652 NSTimeInterval duration = [info[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
1653
1654 // If the software keyboard is displayed before displaying the PasswordManager prompt,
1655 // UIKeyboardWillHideNotification will occur immediately after UIKeyboardWillShowNotification.
1656 // The duration of the animation will be 0.0, and the calculated inset will be 0.0.
1657 // In this case, it is necessary to cancel the animation and hide the keyboard immediately.
1658 // https://github.com/flutter/flutter/pull/164884
1659 if (keyboardMode == FlutterKeyboardModeHidden && calculatedInset == 0.0 && duration == 0.0) {
1660 [self hideKeyboardImmediately];
1661 return;
1662 }
1663
1664 // Avoid double triggering startKeyBoardAnimation.
1665 if (self.targetViewInsetBottom == calculatedInset) {
1666 return;
1667 }
1668
1669 self.targetViewInsetBottom = calculatedInset;
1670
1671 // Flag for simultaneous compounding animation calls.
1672 // This captures animation calls made while the keyboard animation is currently animating. If the
1673 // new animation is in the same direction as the current animation, this flag lets the current
1674 // animation continue with an updated targetViewInsetBottom instead of starting a new keyboard
1675 // animation. This allows for smoother keyboard animation interpolation.
1676 BOOL keyboardWillShow = beginKeyboardFrame.origin.y > keyboardFrame.origin.y;
1677 BOOL keyboardAnimationIsCompounding =
1678 self.keyboardAnimationIsShowing == keyboardWillShow && _keyboardAnimationVSyncClient != nil;
1679
1680 // Mark keyboard as showing or hiding.
1681 self.keyboardAnimationIsShowing = keyboardWillShow;
1682
1683 if (!keyboardAnimationIsCompounding) {
1684 [self startKeyBoardAnimation:duration];
1685 } else if (self.keyboardSpringAnimation) {
1686 self.keyboardSpringAnimation.toValue = self.targetViewInsetBottom;
1687 }
1688 }
1689
1690 - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification {
1691 // Don't ignore UIKeyboardWillHideNotification notifications.
1692 // Even if the notification is triggered in the background or by a different app/view controller,
1693 // we want to always handle this notification to avoid inaccurate inset when in a mulitasking mode
1694 // or when switching between apps.
1695 if (notification.name == UIKeyboardWillHideNotification) {
1696 return NO;
1697 }
1698
1699 // Ignore notification when keyboard's dimensions and position are all zeroes for
1700 // UIKeyboardWillChangeFrameNotification. This happens when keyboard is dragged. Do not ignore if
1701 // the notification is UIKeyboardWillShowNotification, as CGRectZero for that notfication only
1702 // occurs when Minimized/Expanded Shortcuts Bar is dropped after dragging, which we later use to
1703 // categorize it as floating.
1704 NSDictionary* info = notification.userInfo;
1705 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
1706 if (notification.name == UIKeyboardWillChangeFrameNotification &&
1707 CGRectEqualToRect(keyboardFrame, CGRectZero)) {
1708 return YES;
1709 }
1710
1711 // When keyboard's height or width is set to 0, don't ignore. This does not happen
1712 // often but can happen sometimes when switching between multitasking modes.
1713 if (CGRectIsEmpty(keyboardFrame)) {
1714 return NO;
1715 }
1716
1717 // Ignore keyboard notifications related to other apps or view controllers.
1718 if ([self isKeyboardNotificationForDifferentView:notification]) {
1719 return YES;
1720 }
1721 return NO;
1722 }
1723
1724 - (BOOL)isKeyboardNotificationForDifferentView:(NSNotification*)notification {
1725 NSDictionary* info = notification.userInfo;
1726 // Keyboard notifications related to other apps.
1727 // If the UIKeyboardIsLocalUserInfoKey key doesn't exist (this should not happen after iOS 8),
1728 // proceed as if it was local so that the notification is not ignored.
1729 id isLocal = info[UIKeyboardIsLocalUserInfoKey];
1730 if (isLocal && ![isLocal boolValue]) {
1731 return YES;
1732 }
1733 return self.engine.viewController != self;
1734 }
1735
1736 - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification {
1737 // There are multiple types of keyboard: docked, undocked, split, split docked,
1738 // floating, expanded shortcuts bar, minimized shortcuts bar. This function will categorize
1739 // the keyboard as one of the following modes: docked, floating, or hidden.
1740 // Docked mode includes docked, split docked, expanded shortcuts bar (when opening via click),
1741 // and minimized shortcuts bar (when opened via click).
1742 // Floating includes undocked, split, floating, expanded shortcuts bar (when dragged and dropped),
1743 // and minimized shortcuts bar (when dragged and dropped).
1744 NSDictionary* info = notification.userInfo;
1745 CGRect keyboardFrame = [info[UIKeyboardFrameEndUserInfoKey] CGRectValue];
1746
1747 if (notification.name == UIKeyboardWillHideNotification) {
1748 return FlutterKeyboardModeHidden;
1749 }
1750
1751 // If keyboard's dimensions and position are all zeroes, that means it's a Minimized/Expanded
1752 // Shortcuts Bar that has been dropped after dragging, which we categorize as floating.
1753 if (CGRectEqualToRect(keyboardFrame, CGRectZero)) {
1754 return FlutterKeyboardModeFloating;
1755 }
1756 // If keyboard's width or height are 0, it's hidden.
1757 if (CGRectIsEmpty(keyboardFrame)) {
1758 return FlutterKeyboardModeHidden;
1759 }
1760
1761 CGRect screenRect = self.flutterScreenIfViewLoaded.bounds;
1762 CGRect adjustedKeyboardFrame = keyboardFrame;
1763 adjustedKeyboardFrame.origin.y += [self calculateMultitaskingAdjustment:screenRect
1764 keyboardFrame:keyboardFrame];
1765
1766 // If the keyboard is partially or fully showing within the screen, it's either docked or
1767 // floating. Sometimes with custom keyboard extensions, the keyboard's position may be off by a
1768 // small decimal amount (which is why CGRectIntersectRect can't be used). Round to compare.
1769 CGRect intersection = CGRectIntersection(adjustedKeyboardFrame, screenRect);
1770 CGFloat intersectionHeight = CGRectGetHeight(intersection);
1771 CGFloat intersectionWidth = CGRectGetWidth(intersection);
1772 if (round(intersectionHeight) > 0 && intersectionWidth > 0) {
1773 // If the keyboard is above the bottom of the screen, it's floating.
1774 CGFloat screenHeight = CGRectGetHeight(screenRect);
1775 CGFloat adjustedKeyboardBottom = CGRectGetMaxY(adjustedKeyboardFrame);
1776 if (round(adjustedKeyboardBottom) < screenHeight) {
1777 return FlutterKeyboardModeFloating;
1778 }
1779 return FlutterKeyboardModeDocked;
1780 }
1781 return FlutterKeyboardModeHidden;
1782 }
1783
1784 - (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame {
1785 // In Slide Over mode, the keyboard's frame does not include the space
1786 // below the app, even though the keyboard may be at the bottom of the screen.
1787 // To handle, shift the Y origin by the amount of space below the app.
1788 if (self.viewIfLoaded.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad &&
1789 self.viewIfLoaded.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
1790 self.viewIfLoaded.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
1791 CGFloat screenHeight = CGRectGetHeight(screenRect);
1792 CGFloat keyboardBottom = CGRectGetMaxY(keyboardFrame);
1793
1794 // Stage Manager mode will also meet the above parameters, but it does not handle
1795 // the keyboard positioning the same way, so skip if keyboard is at bottom of page.
1796 if (screenHeight == keyboardBottom) {
1797 return 0;
1798 }
1799 CGRect viewRectRelativeToScreen =
1800 [self.viewIfLoaded convertRect:self.viewIfLoaded.frame
1801 toCoordinateSpace:self.flutterScreenIfViewLoaded.coordinateSpace];
1802 CGFloat viewBottom = CGRectGetMaxY(viewRectRelativeToScreen);
1803 CGFloat offset = screenHeight - viewBottom;
1804 if (offset > 0) {
1805 return offset;
1806 }
1807 }
1808 return 0;
1809 }
1810
1811 - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger)keyboardMode {
1812 // Only docked keyboards will have an inset.
1813 if (keyboardMode == FlutterKeyboardModeDocked) {
1814 // Calculate how much of the keyboard intersects with the view.
1815 CGRect viewRectRelativeToScreen =
1816 [self.viewIfLoaded convertRect:self.viewIfLoaded.frame
1817 toCoordinateSpace:self.flutterScreenIfViewLoaded.coordinateSpace];
1818 CGRect intersection = CGRectIntersection(keyboardFrame, viewRectRelativeToScreen);
1819 CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection);
1820
1821 // The keyboard is treated as an inset since we want to effectively reduce the window size by
1822 // the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
1823 // bottom padding.
1824 CGFloat scale = self.flutterScreenIfViewLoaded.scale;
1825 return portionOfKeyboardInView * scale;
1826 }
1827 return 0;
1828 }
1829
1830 - (void)startKeyBoardAnimation:(NSTimeInterval)duration {
1831 // If current physical_view_inset_bottom == targetViewInsetBottom, do nothing.
1832 if (_viewportMetrics.physical_view_inset_bottom == self.targetViewInsetBottom) {
1833 return;
1834 }
1835
1836 // When this method is called for the first time,
1837 // initialize the keyboardAnimationView to get animation interpolation during animation.
1838 if (!self.keyboardAnimationView) {
1839 UIView* keyboardAnimationView = [[UIView alloc] init];
1840 keyboardAnimationView.hidden = YES;
1841 self.keyboardAnimationView = keyboardAnimationView;
1842 }
1843
1844 if (!self.keyboardAnimationView.superview) {
1845 [self.view addSubview:self.keyboardAnimationView];
1846 }
1847
1848 // Remove running animation when start another animation.
1849 [self.keyboardAnimationView.layer removeAllAnimations];
1850
1851 // Set animation begin value and DisplayLink tracking values.
1852 self.keyboardAnimationView.frame =
1853 CGRectMake(0, _viewportMetrics.physical_view_inset_bottom, 0, 0);
1854 self.keyboardAnimationStartTime = fml::TimePoint().Now();
1855 self.originalViewInsetBottom = _viewportMetrics.physical_view_inset_bottom;
1856
1857 // Invalidate old vsync client if old animation is not completed.
1858 [self invalidateKeyboardAnimationVSyncClient];
1859
1860 __weak FlutterViewController* weakSelf = self;
1861 [self setUpKeyboardAnimationVsyncClient:^(fml::TimePoint targetTime) {
1862 [weakSelf handleKeyboardAnimationCallbackWithTargetTime:targetTime];
1863 }];
1864 VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient;
1865
1866 [UIView animateWithDuration:duration
1867 animations:^{
1868 FlutterViewController* strongSelf = weakSelf;
1869 if (!strongSelf) {
1870 return;
1871 }
1872
1873 // Set end value.
1874 strongSelf.keyboardAnimationView.frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0);
1875
1876 // Setup keyboard animation interpolation.
1877 CAAnimation* keyboardAnimation =
1878 [strongSelf.keyboardAnimationView.layer animationForKey:@"position"];
1879 [strongSelf setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation];
1880 }
1881 completion:^(BOOL finished) {
1882 if (_keyboardAnimationVSyncClient == currentVsyncClient) {
1883 FlutterViewController* strongSelf = weakSelf;
1884 if (!strongSelf) {
1885 return;
1886 }
1887
1888 // Indicates the vsync client captured by this block is the original one, which also
1889 // indicates the animation has not been interrupted from its beginning. Moreover,
1890 // indicates the animation is over and there is no more to execute.
1891 [strongSelf invalidateKeyboardAnimationVSyncClient];
1892 [strongSelf removeKeyboardAnimationView];
1893 [strongSelf ensureViewportMetricsIsCorrect];
1894 }
1895 }];
1896 }
1897
1898 - (void)hideKeyboardImmediately {
1899 [self invalidateKeyboardAnimationVSyncClient];
1900 if (self.keyboardAnimationView) {
1901 [self.keyboardAnimationView.layer removeAllAnimations];
1902 [self removeKeyboardAnimationView];
1903 self.keyboardAnimationView = nil;
1904 }
1905 if (self.keyboardSpringAnimation) {
1906 self.keyboardSpringAnimation = nil;
1907 }
1908 // Reset targetViewInsetBottom to 0.0.
1909 self.targetViewInsetBottom = 0.0;
1910 [self ensureViewportMetricsIsCorrect];
1911 }
1912
1913 - (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation {
1914 // If keyboard animation is null or not a spring animation, fallback to DisplayLink tracking.
1915 if (keyboardAnimation == nil || ![keyboardAnimation isKindOfClass:[CASpringAnimation class]]) {
1916 _keyboardSpringAnimation = nil;
1917 return;
1918 }
1919
1920 // Setup keyboard spring animation details for spring curve animation calculation.
1921 CASpringAnimation* keyboardCASpringAnimation = (CASpringAnimation*)keyboardAnimation;
1922 _keyboardSpringAnimation =
1923 [[SpringAnimation alloc] initWithStiffness:keyboardCASpringAnimation.stiffness
1924 damping:keyboardCASpringAnimation.damping
1925 mass:keyboardCASpringAnimation.mass
1926 initialVelocity:keyboardCASpringAnimation.initialVelocity
1927 fromValue:self.originalViewInsetBottom
1928 toValue:self.targetViewInsetBottom];
1929 }
1930
1931 - (void)handleKeyboardAnimationCallbackWithTargetTime:(fml::TimePoint)targetTime {
1932 // If the view controller's view is not loaded, bail out.
1933 if (!self.isViewLoaded) {
1934 return;
1935 }
1936 // If the view for tracking keyboard animation is nil, means it is not
1937 // created, bail out.
1938 if (!self.keyboardAnimationView) {
1939 return;
1940 }
1941 // If keyboardAnimationVSyncClient is nil, means the animation ends.
1942 // And should bail out.
1943 if (!self.keyboardAnimationVSyncClient) {
1944 return;
1945 }
1946
1947 if (!self.keyboardAnimationView.superview) {
1948 // Ensure the keyboardAnimationView is in view hierarchy when animation running.
1949 [self.view addSubview:self.keyboardAnimationView];
1950 }
1951
1952 if (!self.keyboardSpringAnimation) {
1953 if (self.keyboardAnimationView.layer.presentationLayer) {
1954 self->_viewportMetrics.physical_view_inset_bottom =
1955 self.keyboardAnimationView.layer.presentationLayer.frame.origin.y;
1956 [self updateViewportMetricsIfNeeded];
1957 }
1958 } else {
1959 fml::TimeDelta timeElapsed = targetTime - self.keyboardAnimationStartTime;
1960 self->_viewportMetrics.physical_view_inset_bottom =
1961 [self.keyboardSpringAnimation curveFunction:timeElapsed.ToSecondsF()];
1962 [self updateViewportMetricsIfNeeded];
1963 }
1964 }
1965
1966 - (void)setUpKeyboardAnimationVsyncClient:
1967 (FlutterKeyboardAnimationCallback)keyboardAnimationCallback {
1968 if (!keyboardAnimationCallback) {
1969 return;
1970 }
1971 NSAssert(_keyboardAnimationVSyncClient == nil,
1972 @"_keyboardAnimationVSyncClient must be nil when setting up.");
1973
1974 // Make sure the new viewport metrics get sent after the begin frame event has processed.
1975 FlutterKeyboardAnimationCallback animationCallback = [keyboardAnimationCallback copy];
1976 auto uiCallback = [animationCallback](std::unique_ptr<flutter::FrameTimingsRecorder> recorder) {
1977 fml::TimeDelta frameInterval = recorder->GetVsyncTargetTime() - recorder->GetVsyncStartTime();
1978 fml::TimePoint targetTime = recorder->GetVsyncTargetTime() + frameInterval;
1979 dispatch_async(dispatch_get_main_queue(), ^(void) {
1980 animationCallback(targetTime);
1981 });
1982 };
1983
1984 _keyboardAnimationVSyncClient = [[VSyncClient alloc] initWithTaskRunner:self.engine.uiTaskRunner
1985 callback:uiCallback];
1986 _keyboardAnimationVSyncClient.allowPauseAfterVsync = NO;
1987 [_keyboardAnimationVSyncClient await];
1988 }
1989
1990 - (void)invalidateKeyboardAnimationVSyncClient {
1991 [_keyboardAnimationVSyncClient invalidate];
1992 _keyboardAnimationVSyncClient = nil;
1993 }
1994
1995 - (void)removeKeyboardAnimationView {
1996 if (self.keyboardAnimationView.superview != nil) {
1997 [self.keyboardAnimationView removeFromSuperview];
1998 }
1999 }
2000
2001 - (void)ensureViewportMetricsIsCorrect {
2002 if (_viewportMetrics.physical_view_inset_bottom != self.targetViewInsetBottom) {
2003 // Make sure the `physical_view_inset_bottom` is the target value.
2004 _viewportMetrics.physical_view_inset_bottom = self.targetViewInsetBottom;
2005 [self updateViewportMetricsIfNeeded];
2006 }
2007 }
2008
2009 - (void)handlePressEvent:(FlutterUIPressProxy*)press
2010 nextAction:(void (^)())next API_AVAILABLE(ios(13.4)) {
2011 if (@available(iOS 13.4, *)) {
2012 } else {
2013 next();
2014 return;
2015 }
2016 [self.keyboardManager handlePress:press nextAction:next];
2017 }
2018
2019 // The documentation for presses* handlers (implemented below) is entirely
2020 // unclear about how to handle the case where some, but not all, of the presses
2021 // are handled here. I've elected to call super separately for each of the
2022 // presses that aren't handled, but it's not clear if this is correct. It may be
2023 // that iOS intends for us to either handle all or none of the presses, and pass
2024 // the original set to super. I have not yet seen multiple presses in the set in
2025 // the wild, however, so I suspect that the API is built for a tvOS remote or
2026 // something, and perhaps only one ever appears in the set on iOS from a
2027 // keyboard.
2028 //
2029 // We define separate superPresses* overrides to avoid implicitly capturing self in the blocks
2030 // passed to the presses* methods below.
2031
2032 - (void)superPressesBegan:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
2033 [super pressesBegan:presses withEvent:event];
2034 }
2035
2036 - (void)superPressesChanged:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
2037 [super pressesChanged:presses withEvent:event];
2038 }
2039
2040 - (void)superPressesEnded:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
2041 [super pressesEnded:presses withEvent:event];
2042 }
2043
2044 - (void)superPressesCancelled:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
2045 [super pressesCancelled:presses withEvent:event];
2046 }
2047
2048 // If you substantially change these presses overrides, consider also changing
2049 // the similar ones in FlutterTextInputPlugin. They need to be overridden in
2050 // both places to capture keys both inside and outside of a text field, but have
2051 // slightly different implementations.
2052
2053 - (void)pressesBegan:(NSSet<UIPress*>*)presses
2054 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2055 if (@available(iOS 13.4, *)) {
2056 __weak FlutterViewController* weakSelf = self;
2057 for (UIPress* press in presses) {
2058 [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
2059 nextAction:^() {
2060 [weakSelf superPressesBegan:[NSSet setWithObject:press] withEvent:event];
2061 }];
2062 }
2063 } else {
2064 [super pressesBegan:presses withEvent:event];
2065 }
2066 }
2067
2068 - (void)pressesChanged:(NSSet<UIPress*>*)presses
2069 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2070 if (@available(iOS 13.4, *)) {
2071 __weak FlutterViewController* weakSelf = self;
2072 for (UIPress* press in presses) {
2073 [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
2074 nextAction:^() {
2075 [weakSelf superPressesChanged:[NSSet setWithObject:press] withEvent:event];
2076 }];
2077 }
2078 } else {
2079 [super pressesChanged:presses withEvent:event];
2080 }
2081 }
2082
2083 - (void)pressesEnded:(NSSet<UIPress*>*)presses
2084 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2085 if (@available(iOS 13.4, *)) {
2086 __weak FlutterViewController* weakSelf = self;
2087 for (UIPress* press in presses) {
2088 [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
2089 nextAction:^() {
2090 [weakSelf superPressesEnded:[NSSet setWithObject:press] withEvent:event];
2091 }];
2092 }
2093 } else {
2094 [super pressesEnded:presses withEvent:event];
2095 }
2096 }
2097
2098 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
2099 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
2100 if (@available(iOS 13.4, *)) {
2101 __weak FlutterViewController* weakSelf = self;
2102 for (UIPress* press in presses) {
2103 [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
2104 nextAction:^() {
2105 [weakSelf superPressesCancelled:[NSSet setWithObject:press] withEvent:event];
2106 }];
2107 }
2108 } else {
2109 [super pressesCancelled:presses withEvent:event];
2110 }
2111 }
2112
2113 #pragma mark - Orientation updates
2114
2115 - (void)onOrientationPreferencesUpdated:(NSNotification*)notification {
2116 // Notifications may not be on the iOS UI thread
2117 __weak FlutterViewController* weakSelf = self;
2118 dispatch_async(dispatch_get_main_queue(), ^{
2119 NSDictionary* info = notification.userInfo;
2120 NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)];
2121 if (update == nil) {
2122 return;
2123 }
2124 [weakSelf performOrientationUpdate:update.unsignedIntegerValue];
2125 });
2126 }
2127
2128 - (void)requestGeometryUpdateForWindowScenes:(NSSet<UIScene*>*)windowScenes
2129 API_AVAILABLE(ios(16.0)) {
2130 for (UIScene* windowScene in windowScenes) {
2131 FML_DCHECK([windowScene isKindOfClass:[UIWindowScene class]]);
2132 UIWindowSceneGeometryPreferencesIOS* preference = [[UIWindowSceneGeometryPreferencesIOS alloc]
2133 initWithInterfaceOrientations:self.orientationPreferences];
2134 [(UIWindowScene*)windowScene
2135 requestGeometryUpdateWithPreferences:preference
2136 errorHandler:^(NSError* error) {
2137 os_log_error(OS_LOG_DEFAULT,
2138 "Failed to change device orientation: %@", error);
2139 }];
2140 [self setNeedsUpdateOfSupportedInterfaceOrientations];
2141 }
2142 }
2143
2144 - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
2145 if (new_preferences != self.orientationPreferences) {
2146 self.orientationPreferences = new_preferences;
2147
2148 if (@available(iOS 16.0, *)) {
2149 UIApplication* flutterApplication = FlutterSharedApplication.application;
2150 NSSet<UIScene*>* scenes = [NSSet set];
2151 if (flutterApplication) {
2152 scenes = [flutterApplication.connectedScenes
2153 filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
2154 id scene, NSDictionary* bindings) {
2155 return [scene isKindOfClass:[UIWindowScene class]];
2156 }]];
2157 } else if (self.flutterWindowSceneIfViewLoaded) {
2158 scenes = [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded];
2159 }
2160 [self requestGeometryUpdateForWindowScenes:scenes];
2161 } else {
2162 UIInterfaceOrientationMask currentInterfaceOrientation = 0;
2163 UIWindowScene* windowScene = self.flutterWindowSceneIfViewLoaded;
2164 if (!windowScene) {
2165 [FlutterLogger
2166 logWarning:
2167 @"Accessing the interface orientation when the window scene is unavailable."];
2168 return;
2169 }
2170 currentInterfaceOrientation = 1 << windowScene.interfaceOrientation;
2171 if (!(self.orientationPreferences & currentInterfaceOrientation)) {
2172 [UIViewController attemptRotationToDeviceOrientation];
2173 // Force orientation switch if the current orientation is not allowed
2174 if (self.orientationPreferences & UIInterfaceOrientationMaskPortrait) {
2175 // This is no official API but more like a workaround / hack (using
2176 // key-value coding on a read-only property). This might break in
2177 // the future, but currently it´s the only way to force an orientation change
2178 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait)
2179 forKey:@"orientation"];
2180 } else if (self.orientationPreferences & UIInterfaceOrientationMaskPortraitUpsideDown) {
2181 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortraitUpsideDown)
2182 forKey:@"orientation"];
2183 } else if (self.orientationPreferences & UIInterfaceOrientationMaskLandscapeLeft) {
2184 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeLeft)
2185 forKey:@"orientation"];
2186 } else if (self.orientationPreferences & UIInterfaceOrientationMaskLandscapeRight) {
2187 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeRight)
2188 forKey:@"orientation"];
2189 }
2190 }
2191 }
2192 }
2193 }
2194
2195 - (void)onHideHomeIndicatorNotification:(NSNotification*)notification {
2196 self.isHomeIndicatorHidden = YES;
2197 }
2198
2199 - (void)onShowHomeIndicatorNotification:(NSNotification*)notification {
2200 self.isHomeIndicatorHidden = NO;
2201 }
2202
2203 - (void)setIsHomeIndicatorHidden:(BOOL)hideHomeIndicator {
2204 if (hideHomeIndicator != _isHomeIndicatorHidden) {
2205 _isHomeIndicatorHidden = hideHomeIndicator;
2206 [self setNeedsUpdateOfHomeIndicatorAutoHidden];
2207 }
2208 }
2209
2210 - (BOOL)prefersHomeIndicatorAutoHidden {
2211 return self.isHomeIndicatorHidden;
2212 }
2213
2214 - (BOOL)shouldAutorotate {
2215 return YES;
2216 }
2217
2218 - (NSUInteger)supportedInterfaceOrientations {
2219 return self.orientationPreferences;
2220 }
2221
2222 #pragma mark - Accessibility
2223
2224 - (void)onAccessibilityStatusChanged:(NSNotification*)notification {
2225 if (!self.engine) {
2226 return;
2227 }
2228 BOOL enabled = NO;
2229 int32_t flags = self.accessibilityFlags;
2230 #if TARGET_OS_SIMULATOR
2231 // There doesn't appear to be any way to determine whether the accessibility
2232 // inspector is enabled on the simulator. We conservatively always turn on the
2233 // accessibility bridge in the simulator, but never assistive technology.
2234 enabled = YES;
2235 #else
2236 _isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning();
2237 enabled = _isVoiceOverRunning || UIAccessibilityIsSwitchControlRunning();
2238 if (enabled) {
2239 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kAccessibleNavigation);
2240 }
2241 enabled |= UIAccessibilityIsSpeakScreenEnabled();
2242 #endif
2243 [self.engine enableSemantics:enabled withFlags:flags];
2244 }
2245
2246 - (int32_t)accessibilityFlags {
2247 int32_t flags = 0;
2248 if (UIAccessibilityIsInvertColorsEnabled()) {
2249 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kInvertColors);
2250 }
2251 if (UIAccessibilityIsReduceMotionEnabled()) {
2252 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kReduceMotion);
2253 }
2254 if (UIAccessibilityIsBoldTextEnabled()) {
2255 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kBoldText);
2256 }
2257 if (UIAccessibilityDarkerSystemColorsEnabled()) {
2258 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kHighContrast);
2259 }
2260 if ([FlutterViewController accessibilityIsOnOffSwitchLabelsEnabled]) {
2261 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels);
2262 }
2263
2264 return flags;
2265 }
2266
2267 - (BOOL)accessibilityPerformEscape {
2268 FlutterMethodChannel* navigationChannel = self.engine.navigationChannel;
2269 if (navigationChannel) {
2270 [self popRoute];
2271 return YES;
2272 }
2273 return NO;
2274 }
2275
2276 + (BOOL)accessibilityIsOnOffSwitchLabelsEnabled {
2277 return UIAccessibilityIsOnOffSwitchLabelsEnabled();
2278 }
2279
2280 #pragma mark - Set user settings
2281
2282 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
2283 [super traitCollectionDidChange:previousTraitCollection];
2284 [self onUserSettingsChanged:nil];
2285
2286 // Since this method can get triggered by changes in device orientation, reset and recalculate the
2287 // instrinsic size.
2288 if (self.isAutoResizable) {
2289 [self.flutterView resetIntrinsicContentSize];
2290 }
2291 }
2292
2293 - (void)onUserSettingsChanged:(NSNotification*)notification {
2294 [self.engine.settingsChannel sendMessage:@{
2295 @"textScaleFactor" : @(self.textScaleFactor),
2296 @"alwaysUse24HourFormat" : @(FlutterHourFormat.isAlwaysUse24HourFormat),
2297 @"platformBrightness" : self.brightnessMode,
2298 @"platformContrast" : self.contrastMode,
2299 @"nativeSpellCheckServiceDefined" : @YES,
2300 @"supportsShowingSystemContextMenu" : @(self.supportsShowingSystemContextMenu)
2301 }];
2302 }
2303
2304 - (CGFloat)textScaleFactor {
2305 UIApplication* flutterApplication = FlutterSharedApplication.application;
2306 if (flutterApplication == nil) {
2307 [FlutterLogger logWarning:@"Dynamic content size update is not supported in app extension."];
2308 return 1.0;
2309 }
2310
2311 UIContentSizeCategory category = flutterApplication.preferredContentSizeCategory;
2312 // The delta is computed by approximating Apple's typography guidelines:
2313 // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/
2314 //
2315 // Specifically:
2316 // Non-accessibility sizes for "body" text are:
2317 const CGFloat xs = 14;
2318 const CGFloat s = 15;
2319 const CGFloat m = 16;
2320 const CGFloat l = 17;
2321 const CGFloat xl = 19;
2322 const CGFloat xxl = 21;
2323 const CGFloat xxxl = 23;
2324
2325 // Accessibility sizes for "body" text are:
2326 const CGFloat ax1 = 28;
2327 const CGFloat ax2 = 33;
2328 const CGFloat ax3 = 40;
2329 const CGFloat ax4 = 47;
2330 const CGFloat ax5 = 53;
2331
2332 // We compute the scale as relative difference from size L (large, the default size), where
2333 // L is assumed to have scale 1.0.
2334 if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) {
2335 return xs / l;
2336 } else if ([category isEqualToString:UIContentSizeCategorySmall]) {
2337 return s / l;
2338 } else if ([category isEqualToString:UIContentSizeCategoryMedium]) {
2339 return m / l;
2340 } else if ([category isEqualToString:UIContentSizeCategoryLarge]) {
2341 return 1.0;
2342 } else if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) {
2343 return xl / l;
2344 } else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) {
2345 return xxl / l;
2346 } else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) {
2347 return xxxl / l;
2348 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) {
2349 return ax1 / l;
2350 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) {
2351 return ax2 / l;
2352 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) {
2353 return ax3 / l;
2354 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) {
2355 return ax4 / l;
2356 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) {
2357 return ax5 / l;
2358 } else {
2359 return 1.0;
2360 }
2361 }
2362
2363 - (BOOL)supportsShowingSystemContextMenu {
2364 if (@available(iOS 16.0, *)) {
2365 return YES;
2366 } else {
2367 return NO;
2368 }
2369 }
2370
2371 // The brightness mode of the platform, e.g., light or dark, expressed as a string that
2372 // is understood by the Flutter framework. See the settings
2373 // system channel for more information.
2374 - (NSString*)brightnessMode {
2375 UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;
2376
2377 if (style == UIUserInterfaceStyleDark) {
2378 return @"dark";
2379 } else {
2380 return @"light";
2381 }
2382 }
2383
2384 // The contrast mode of the platform, e.g., normal or high, expressed as a string that is
2385 // understood by the Flutter framework. See the settings system channel for more
2386 // information.
2387 - (NSString*)contrastMode {
2388 UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast;
2389
2390 if (contrast == UIAccessibilityContrastHigh) {
2391 return @"high";
2392 } else {
2393 return @"normal";
2394 }
2395 }
2396
2397 #pragma mark - Status bar style
2398
2399 - (UIStatusBarStyle)preferredStatusBarStyle {
2400 return self.statusBarStyle;
2401 }
2402
2403 - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
2404 // Notifications may not be on the iOS UI thread
2405 __weak FlutterViewController* weakSelf = self;
2406 dispatch_async(dispatch_get_main_queue(), ^{
2407 FlutterViewController* strongSelf = weakSelf;
2408 if (!strongSelf) {
2409 return;
2410 }
2411
2412 NSDictionary* info = notification.userInfo;
2413 NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)];
2414 if (update == nil) {
2415 return;
2416 }
2417
2418 UIStatusBarStyle style = static_cast<UIStatusBarStyle>(update.integerValue);
2419 if (style != strongSelf.statusBarStyle) {
2420 strongSelf.statusBarStyle = style;
2421 [strongSelf setNeedsStatusBarAppearanceUpdate];
2422 }
2423 });
2424 }
2425
2426 - (void)setPrefersStatusBarHidden:(BOOL)hidden {
2427 if (hidden != self.flutterPrefersStatusBarHidden) {
2428 self.flutterPrefersStatusBarHidden = hidden;
2429 [self setNeedsStatusBarAppearanceUpdate];
2430 }
2431 }
2432
2433 - (BOOL)prefersStatusBarHidden {
2434 return self.flutterPrefersStatusBarHidden;
2435 }
2436
2437 #pragma mark - Platform views
2438
2439 - (FlutterPlatformViewsController*)platformViewsController {
2440 return self.engine.platformViewsController;
2441 }
2442
2443 - (NSObject<FlutterBinaryMessenger>*)binaryMessenger {
2444 return self.engine.binaryMessenger;
2445 }
2446
2447 #pragma mark - FlutterBinaryMessenger
2448
2449 - (void)sendOnChannel:(NSString*)channel message:(NSData*)message {
2450 [self.engine.binaryMessenger sendOnChannel:channel message:message];
2451 }
2452
2453 - (void)sendOnChannel:(NSString*)channel
2454 message:(NSData*)message
2455 binaryReply:(FlutterBinaryReply)callback {
2456 NSAssert(channel, @"The channel must not be null");
2457 [self.engine.binaryMessenger sendOnChannel:channel message:message binaryReply:callback];
2458 }
2459
2460 - (NSObject<FlutterTaskQueue>*)makeBackgroundTaskQueue {
2461 return [self.engine.binaryMessenger makeBackgroundTaskQueue];
2462 }
2463
2464 - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channel
2465 binaryMessageHandler:
2466 (FlutterBinaryMessageHandler)handler {
2467 return [self setMessageHandlerOnChannel:channel binaryMessageHandler:handler taskQueue:nil];
2468 }
2469
2470 - (FlutterBinaryMessengerConnection)
2471 setMessageHandlerOnChannel:(NSString*)channel
2472 binaryMessageHandler:(FlutterBinaryMessageHandler _Nullable)handler
2473 taskQueue:(NSObject<FlutterTaskQueue>* _Nullable)taskQueue {
2474 NSAssert(channel, @"The channel must not be null");
2475 return [self.engine.binaryMessenger setMessageHandlerOnChannel:channel
2476 binaryMessageHandler:handler
2477 taskQueue:taskQueue];
2478 }
2479
2480 - (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection {
2481 [self.engine.binaryMessenger cleanUpConnection:connection];
2482 }
2483
2484 #pragma mark - FlutterTextureRegistry
2485
2486 - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture {
2487 return [self.engine.textureRegistry registerTexture:texture];
2488 }
2489
2490 - (void)unregisterTexture:(int64_t)textureId {
2491 [self.engine.textureRegistry unregisterTexture:textureId];
2492 }
2493
2494 - (void)textureFrameAvailable:(int64_t)textureId {
2495 [self.engine.textureRegistry textureFrameAvailable:textureId];
2496 }
2497
2498 - (NSString*)lookupKeyForAsset:(NSString*)asset {
2499 return [FlutterDartProject lookupKeyForAsset:asset];
2500 }
2501
2502 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
2503 return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
2504 }
2505
2506 - (id<FlutterPluginRegistry>)pluginRegistry {
2507 return self.engine;
2508 }
2509
2510 + (BOOL)isUIAccessibilityIsVoiceOverRunning {
2511 return UIAccessibilityIsVoiceOverRunning();
2512 }
2513
2514 #pragma mark - FlutterPluginRegistry
2515
2516 - (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
2517 return [self.engine registrarForPlugin:pluginKey];
2518 }
2519
2520 - (BOOL)hasPlugin:(NSString*)pluginKey {
2521 return [self.engine hasPlugin:pluginKey];
2522 }
2523
2524 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
2525 return [self.engine valuePublishedByPlugin:pluginKey];
2526 }
2527
2528 - (void)presentViewController:(UIViewController*)viewControllerToPresent
2529 animated:(BOOL)flag
2530 completion:(void (^)(void))completion {
2531 self.isPresentingViewControllerAnimating = YES;
2532 __weak FlutterViewController* weakSelf = self;
2533 [super presentViewController:viewControllerToPresent
2534 animated:flag
2535 completion:^{
2536 weakSelf.isPresentingViewControllerAnimating = NO;
2537 if (completion) {
2538 completion();
2539 }
2540 }];
2541 }
2542
2543 - (BOOL)isPresentingViewController {
2544 return self.presentedViewController != nil || self.isPresentingViewControllerAnimating;
2545 }
2546
2547 - (flutter::PointerData)updateMousePointerDataFrom:(UIGestureRecognizer*)gestureRecognizer
2548 API_AVAILABLE(ios(13.4)) {
2549 CGPoint location = [gestureRecognizer locationInView:self.view];
2550 CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2551 _mouseState.location = {location.x * scale, location.y * scale};
2552 flutter::PointerData pointer_data;
2553 pointer_data.Clear();
2554 pointer_data.time_stamp = [[NSProcessInfo processInfo] systemUptime] * kMicrosecondsPerSecond;
2555 pointer_data.physical_x = _mouseState.location.x;
2556 pointer_data.physical_y = _mouseState.location.y;
2557 return pointer_data;
2558 }
2559
2560 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2561 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
2562 API_AVAILABLE(ios(13.4)) {
2563 return YES;
2564 }
2565
2566 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2567 shouldReceiveEvent:(UIEvent*)event API_AVAILABLE(ios(13.4)) {
2568 if (gestureRecognizer == _continuousScrollingPanGestureRecognizer &&
2569 event.type == UIEventTypeScroll) {
2570 // Events with type UIEventTypeScroll are only received when running on macOS under emulation.
2571 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:gestureRecognizer];
2572 pointer_data.device = reinterpret_cast<int64_t>(_continuousScrollingPanGestureRecognizer);
2573 pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2574 pointer_data.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel;
2575 pointer_data.view_id = self.viewIdentifier;
2576
2577 if (event.timestamp < self.scrollInertiaEventAppKitDeadline) {
2578 // Only send the event if it occured before the expected natural end of gesture momentum.
2579 // If received after the deadline, it's not likely the event is from a user-initiated cancel.
2580 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2581 packet->SetPointerData(/*i=*/0, pointer_data);
2582 [self.engine dispatchPointerDataPacket:std::move(packet)];
2583 self.scrollInertiaEventAppKitDeadline = 0;
2584 }
2585 }
2586 // This method is also called for UITouches, should return YES to process all touches.
2587 return YES;
2588 }
2589
2590 - (void)hoverEvent:(UIHoverGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2591 CGPoint oldLocation = _mouseState.location;
2592
2593 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2594 pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2595 pointer_data.kind = flutter::PointerData::DeviceKind::kMouse;
2596 pointer_data.view_id = self.viewIdentifier;
2597
2598 switch (_hoverGestureRecognizer.state) {
2599 case UIGestureRecognizerStateBegan:
2600 pointer_data.change = flutter::PointerData::Change::kAdd;
2601 break;
2602 case UIGestureRecognizerStateChanged:
2603 pointer_data.change = flutter::PointerData::Change::kHover;
2604 break;
2605 case UIGestureRecognizerStateEnded:
2606 case UIGestureRecognizerStateCancelled:
2607 pointer_data.change = flutter::PointerData::Change::kRemove;
2608 break;
2609 default:
2610 // Sending kHover is the least harmful thing to do here
2611 // But this state is not expected to ever be reached.
2612 pointer_data.change = flutter::PointerData::Change::kHover;
2613 break;
2614 }
2615
2616 NSTimeInterval time = [NSProcessInfo processInfo].systemUptime;
2617 BOOL isRunningOnMac = NO;
2618 if (@available(iOS 14.0, *)) {
2619 // This "stationary pointer" heuristic is not reliable when running within macOS.
2620 // We instead receive a scroll cancel event directly from AppKit.
2621 // See gestureRecognizer:shouldReceiveEvent:
2622 isRunningOnMac = [NSProcessInfo processInfo].iOSAppOnMac;
2623 }
2624 if (!isRunningOnMac && CGPointEqualToPoint(oldLocation, _mouseState.location) &&
2625 time > self.scrollInertiaEventStartline) {
2626 // iPadOS reports trackpad movements events with high (sub-pixel) precision. When an event
2627 // is received with the same position as the previous one, it can only be from a finger
2628 // making or breaking contact with the trackpad surface.
2629 auto packet = std::make_unique<flutter::PointerDataPacket>(2);
2630 packet->SetPointerData(/*i=*/0, pointer_data);
2631 flutter::PointerData inertia_cancel = pointer_data;
2632 inertia_cancel.device = reinterpret_cast<int64_t>(_continuousScrollingPanGestureRecognizer);
2633 inertia_cancel.kind = flutter::PointerData::DeviceKind::kTrackpad;
2634 inertia_cancel.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel;
2635 inertia_cancel.view_id = self.viewIdentifier;
2636 packet->SetPointerData(/*i=*/1, inertia_cancel);
2637 [self.engine dispatchPointerDataPacket:std::move(packet)];
2638 self.scrollInertiaEventStartline = DBL_MAX;
2639 } else {
2640 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2641 packet->SetPointerData(/*i=*/0, pointer_data);
2642 [self.engine dispatchPointerDataPacket:std::move(packet)];
2643 }
2644 }
2645
2646 - (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2647 CGPoint translation = [recognizer translationInView:self.view];
2648 const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2649
2650 translation.x *= scale;
2651 translation.y *= scale;
2652
2653 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2654 pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2655 pointer_data.kind = flutter::PointerData::DeviceKind::kMouse;
2656 pointer_data.signal_kind = flutter::PointerData::SignalKind::kScroll;
2657 pointer_data.scroll_delta_x = (translation.x - _mouseState.last_translation.x);
2658 pointer_data.scroll_delta_y = -(translation.y - _mouseState.last_translation.y);
2659 pointer_data.view_id = self.viewIdentifier;
2660
2661 // The translation reported by UIPanGestureRecognizer is the total translation
2662 // generated by the pan gesture since the gesture began. We need to be able
2663 // to keep track of the last translation value in order to generate the deltaX
2664 // and deltaY coordinates for each subsequent scroll event.
2665 if (recognizer.state != UIGestureRecognizerStateEnded) {
2666 _mouseState.last_translation = translation;
2667 } else {
2668 _mouseState.last_translation = CGPointZero;
2669 }
2670
2671 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2672 packet->SetPointerData(/*i=*/0, pointer_data);
2673 [self.engine dispatchPointerDataPacket:std::move(packet)];
2674 }
2675
2676 - (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2677 CGPoint translation = [recognizer translationInView:self.view];
2678 const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2679
2680 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2681 pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2682 pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2683 pointer_data.view_id = self.viewIdentifier;
2684 switch (recognizer.state) {
2685 case UIGestureRecognizerStateBegan:
2686 pointer_data.change = flutter::PointerData::Change::kPanZoomStart;
2687 break;
2688 case UIGestureRecognizerStateChanged:
2689 pointer_data.change = flutter::PointerData::Change::kPanZoomUpdate;
2690 pointer_data.pan_x = translation.x * scale;
2691 pointer_data.pan_y = translation.y * scale;
2692 pointer_data.pan_delta_x = 0; // Delta will be generated in pointer_data_packet_converter.cc.
2693 pointer_data.pan_delta_y = 0; // Delta will be generated in pointer_data_packet_converter.cc.
2694 pointer_data.scale = 1;
2695 break;
2696 case UIGestureRecognizerStateEnded:
2697 case UIGestureRecognizerStateCancelled:
2698 self.scrollInertiaEventStartline =
2699 [[NSProcessInfo processInfo] systemUptime] +
2700 0.1; // Time to lift fingers off trackpad (experimentally determined)
2701 // When running an iOS app on an Apple Silicon Mac, AppKit will send an event
2702 // of type UIEventTypeScroll when trackpad scroll momentum has ended. This event
2703 // is sent whether the momentum ended normally or was cancelled by a trackpad touch.
2704 // Since Flutter scrolling inertia will likely not match the system inertia, we should
2705 // only send a PointerScrollInertiaCancel event for user-initiated cancellations.
2706 // The following (curve-fitted) calculation provides a cutoff point after which any
2707 // UIEventTypeScroll event will likely be from the system instead of the user.
2708 // See https://github.com/flutter/engine/pull/34929.
2709 self.scrollInertiaEventAppKitDeadline =
2710 [[NSProcessInfo processInfo] systemUptime] +
2711 (0.1821 * log(fmax([recognizer velocityInView:self.view].x,
2712 [recognizer velocityInView:self.view].y))) -
2713 0.4825;
2714 pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
2715 break;
2716 default:
2717 // continuousScrollEvent: should only ever be triggered with the above phases
2718 NSAssert(NO, @"Trackpad pan event occured with unexpected phase 0x%lx",
2719 (long)recognizer.state);
2720 break;
2721 }
2722
2723 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2724 packet->SetPointerData(/*i=*/0, pointer_data);
2725 [self.engine dispatchPointerDataPacket:std::move(packet)];
2726 }
2727
2728 - (void)pinchEvent:(UIPinchGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2729 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2730 pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2731 pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2732 pointer_data.view_id = self.viewIdentifier;
2733 switch (recognizer.state) {
2734 case UIGestureRecognizerStateBegan:
2735 pointer_data.change = flutter::PointerData::Change::kPanZoomStart;
2736 break;
2737 case UIGestureRecognizerStateChanged:
2738 pointer_data.change = flutter::PointerData::Change::kPanZoomUpdate;
2739 pointer_data.scale = recognizer.scale;
2740 pointer_data.rotation = _rotationGestureRecognizer.rotation;
2741 break;
2742 case UIGestureRecognizerStateEnded:
2743 case UIGestureRecognizerStateCancelled:
2744 pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
2745 break;
2746 default:
2747 // pinchEvent: should only ever be triggered with the above phases
2748 NSAssert(NO, @"Trackpad pinch event occured with unexpected phase 0x%lx",
2749 (long)recognizer.state);
2750 break;
2751 }
2752
2753 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2754 packet->SetPointerData(/*i=*/0, pointer_data);
2755 [self.engine dispatchPointerDataPacket:std::move(packet)];
2756 }
2757
2758 #pragma mark - State Restoration
2759
2760 - (void)encodeRestorableStateWithCoder:(NSCoder*)coder {
2761 NSData* restorationData = [self.engine.restorationPlugin restorationData];
2762 [coder encodeBytes:(const unsigned char*)restorationData.bytes
2763 length:restorationData.length
2764 forKey:kFlutterRestorationStateAppData];
2765 [super encodeRestorableStateWithCoder:coder];
2766 }
2767
2768 - (void)decodeRestorableStateWithCoder:(NSCoder*)coder {
2769 NSUInteger restorationDataLength;
2770 const unsigned char* restorationBytes = [coder decodeBytesForKey:kFlutterRestorationStateAppData
2771 returnedLength:&restorationDataLength];
2772 NSData* restorationData = [NSData dataWithBytes:restorationBytes length:restorationDataLength];
2773 [self.engine.restorationPlugin setRestorationData:restorationData];
2774 }
2775
2776 - (FlutterRestorationPlugin*)restorationPlugin {
2777 return self.engine.restorationPlugin;
2778 }
2779
2780 - (FlutterTextInputPlugin*)textInputPlugin {
2781 return self.engine.textInputPlugin;
2782 }
2783
2784 @end
FlutterAppDelegate_Internal.h
FlutterBinaryReply
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
FlutterBinaryMessageHandler
void(^ FlutterBinaryMessageHandler)(NSData *_Nullable message, FlutterBinaryReply reply)
Definition: FlutterBinaryMessenger.h:30
FlutterBinaryMessengerConnection
int64_t FlutterBinaryMessengerConnection
Definition: FlutterBinaryMessenger.h:32
FlutterBinaryMessengerRelay.h
FlutterChannelKeyResponder.h
FlutterEmbedderKeyResponder.h
FlutterSendKeyEvent
void(^ FlutterSendKeyEvent)(const FlutterKeyEvent &, _Nullable FlutterKeyEventCallback, void *_Nullable)
Definition: FlutterEmbedderKeyResponder.h:13
FlutterEngine_Internal.h
FlutterKeyPrimaryResponder.h
FlutterKeyboardManager.h
FlutterLaunchEngine.h
FlutterPlatformPlugin.h
FlutterPlatformViews_Internal.h
FlutterPluginAppLifeCycleDelegate_internal.h
FlutterSharedApplication.h
FlutterTextInputDelegate.h
FlutterTextInputPlugin.h
API_AVAILABLE
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
initWithCoder
instancetype initWithCoder
Definition: FlutterTextInputPlugin.h:178
textInputPlugin
FlutterTextInputPlugin * textInputPlugin
Definition: FlutterTextInputPluginTest.mm:138
engine
id engine
Definition: FlutterTextInputPluginTest.mm:137
self
return self
Definition: FlutterTextureRegistryRelay.mm:19
FlutterView.h
FlutterViewControllerHideHomeIndicator
NSNotificationName const FlutterViewControllerHideHomeIndicator
Definition: FlutterViewController.mm:50
kFlutterRestorationStateAppData
static NSString *const kFlutterRestorationStateAppData
Definition: FlutterViewController.mm:46
FlutterViewControllerShowHomeIndicator
NSNotificationName const FlutterViewControllerShowHomeIndicator
Definition: FlutterViewController.mm:52
FlutterSemanticsUpdateNotification
NSNotificationName const FlutterSemanticsUpdateNotification
Definition: FlutterViewController.mm:48
MouseState
struct MouseState MouseState
kScrollViewContentSize
static constexpr CGFloat kScrollViewContentSize
Definition: FlutterViewController.mm:44
FlutterViewControllerWillDealloc
NSNotificationName const FlutterViewControllerWillDealloc
Definition: FlutterViewController.mm:49
kMicrosecondsPerSecond
static constexpr FLUTTER_ASSERT_ARC int kMicrosecondsPerSecond
Definition: FlutterViewController.mm:43
_mouseState
MouseState _mouseState
Definition: FlutterViewController.mm:167
FlutterViewController_Internal.h
FlutterKeyboardAnimationCallback
void(^ FlutterKeyboardAnimationCallback)(fml::TimePoint)
Definition: FlutterViewController_Internal.h:43
UIViewController+FlutterScreenAndSceneIfLoaded.h
-[FlutterViewController() API_AVAILABLE]
UIPanGestureRecognizer *continuousScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4))
-[FlutterViewController() API_AVAILABLE]
UIPanGestureRecognizer *discreteScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4))
-[FlutterViewController() API_AVAILABLE]
UIPinchGestureRecognizer *pinchGestureRecognizer API_AVAILABLE(ios(13.4))
-[FlutterViewController() API_AVAILABLE]
UIHoverGestureRecognizer *hoverGestureRecognizer API_AVAILABLE(ios(13.4))
FlutterDartProject
Definition: FlutterDartProject.mm:268
+[FlutterDartProject lookupKeyForAsset:fromPackage:]
NSString * lookupKeyForAsset:fromPackage:(NSString *asset,[fromPackage] NSString *package)
Definition: FlutterDartProject.mm:403
+[FlutterDartProject lookupKeyForAsset:]
NSString * lookupKeyForAsset:(NSString *asset)
Definition: FlutterDartProject.mm:394
DisplayLinkManager
Definition: FlutterMetalLayer.mm:18
DisplayLinkManager::displayRefreshRate
double displayRefreshRate
The display refresh rate used for reporting purposes. The engine does not care about this for frame s...
Definition: vsync_waiter_ios.h:43
FlutterAppDelegate
Definition: FlutterAppDelegate.h:28
FlutterAutoResizeLayoutConstraint
Definition: FlutterView.h:47
FlutterChannelKeyResponder
Definition: FlutterChannelKeyResponder.h:20
FlutterEmbedderKeyResponder
Definition: FlutterEmbedderKeyResponder.h:23
FlutterEngine
Definition: FlutterEngine.h:61
FlutterEngine::viewController
FlutterViewController * viewController
Definition: FlutterEngine.h:327
FlutterHourFormat
Definition: FlutterHourFormat.h:11
FlutterKeyboardManager
Definition: FlutterKeyboardManager.h:54
FlutterMethodChannel
Definition: FlutterChannels.h:221
FlutterPlatformViewsController
Definition: FlutterPlatformViewsController.h:31
FlutterRestorationPlugin
Definition: FlutterRestorationPlugin.h:13
FlutterSharedApplication
Definition: FlutterSharedApplication.h:13
FlutterSharedApplication::application
UIApplication * application
Definition: FlutterSharedApplication.h:27
FlutterSharedApplication::hasSceneDelegate
BOOL hasSceneDelegate
Definition: FlutterSharedApplication.h:32
FlutterSharedApplication::isAvailable
BOOL isAvailable
Definition: FlutterSharedApplication.h:22
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:34
-[FlutterTextInputPlugin setUpIndirectScribbleInteraction:]
void setUpIndirectScribbleInteraction:(id< FlutterViewResponder > viewResponder)
Definition: FlutterTextInputPlugin.mm:3221
FlutterTexture
Definition: FlutterMetalLayer.mm:61
FlutterViewController
Definition: FlutterViewController.h:57
FlutterViewController::displayingFlutterUI
BOOL displayingFlutterUI
Definition: FlutterViewController.h:198
FlutterViewController::splashScreenView
UIView * splashScreenView
Definition: FlutterViewController.h:211
FlutterView
Definition: FlutterView.h:50
VSyncClient
Definition: vsync_waiter_ios.h:48
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
flutter
Definition: accessibility_bridge.h:26
fml
Definition: profiler_metrics_ios.mm:41
platform_message_response_darwin.h
platform_view_ios.h
FlutterBinaryMessenger-p
Definition: FlutterBinaryMessenger.h:49
FlutterPluginRegistrar-p
Definition: FlutterPlugin.h:349
FlutterTaskQueue-p
Definition: FlutterBinaryMessenger.h:34
MouseState
Definition: FlutterViewController.mm:56
MouseState::location
CGPoint location
Definition: FlutterViewController.mm:58
MouseState::last_translation
CGPoint last_translation
Definition: FlutterViewController.mm:61
vsync_waiter_ios.h