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