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