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