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