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