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