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