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