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