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