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