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