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