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