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