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