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