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