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