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)isApplicationStateMatching:(UIApplicationState)match
686 withApplication:(UIApplication*)application {
687 switch (application.applicationState) {
688 case UIApplicationStateActive:
689 case UIApplicationStateInactive:
690 case UIApplicationStateBackground:
691 return application.applicationState == match;
692 }
693}
694
695- (BOOL)isSceneStateMatching:(UISceneActivationState)match API_AVAILABLE(ios(13.0)) {
696 switch (self.activationState) {
697 case UISceneActivationStateForegroundActive:
698 case UISceneActivationStateUnattached:
699 case UISceneActivationStateForegroundInactive:
700 case UISceneActivationStateBackground:
701 return self.activationState == match;
702 }
703}
704
705#pragma mark - Surface creation and teardown updates
706
707- (void)surfaceUpdated:(BOOL)appeared {
708 if (!self.engine) {
709 return;
710 }
711
712 // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and raster
713 // thread.
714 if (appeared) {
715 [self installFirstFrameCallback];
716 self.platformViewsController.flutterView = self.flutterView;
717 self.platformViewsController.flutterViewController = self;
718 [self.engine notifyViewCreated];
719 } else {
720 self.displayingFlutterUI = NO;
721 [self.engine notifyViewDestroyed];
722 self.platformViewsController.flutterView = nil;
723 self.platformViewsController.flutterViewController = nil;
724 }
725}
726
727#pragma mark - UIViewController lifecycle notifications
728
729- (void)viewDidLoad {
730 TRACE_EVENT0("flutter", "viewDidLoad");
731
732 if (self.engine && self.engineNeedsLaunch) {
733 [self.engine launchEngine:nil libraryURI:nil entrypointArgs:nil];
734 [self.engine setViewController:self];
735 self.engineNeedsLaunch = NO;
736 } else if (self.engine.viewController == self) {
737 [self.engine attachView];
738 }
739
740 // Register internal plugins.
741 [self addInternalPlugins];
742
743 // Create a vsync client to correct delivery frame rate of touch events if needed.
744 [self createTouchRateCorrectionVSyncClientIfNeeded];
745
746 if (@available(iOS 13.4, *)) {
747 _hoverGestureRecognizer =
748 [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)];
749 _hoverGestureRecognizer.delegate = self;
750 [self.flutterView addGestureRecognizer:_hoverGestureRecognizer];
751
752 _discreteScrollingPanGestureRecognizer =
753 [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(discreteScrollEvent:)];
754 _discreteScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
755 // Disallowing all touch types. If touch events are allowed here, touches to the screen will be
756 // consumed by the UIGestureRecognizer instead of being passed through to flutter via
757 // touchesBegan. Trackpad and mouse scrolls are sent by the platform as scroll events rather
758 // than touch events, so they will still be received.
759 _discreteScrollingPanGestureRecognizer.allowedTouchTypes = @[];
760 _discreteScrollingPanGestureRecognizer.delegate = self;
761 [self.flutterView addGestureRecognizer:_discreteScrollingPanGestureRecognizer];
762 _continuousScrollingPanGestureRecognizer =
763 [[UIPanGestureRecognizer alloc] initWithTarget:self
764 action:@selector(continuousScrollEvent:)];
765 _continuousScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
766 _continuousScrollingPanGestureRecognizer.allowedTouchTypes = @[];
767 _continuousScrollingPanGestureRecognizer.delegate = self;
768 [self.flutterView addGestureRecognizer:_continuousScrollingPanGestureRecognizer];
769 _pinchGestureRecognizer =
770 [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchEvent:)];
771 _pinchGestureRecognizer.allowedTouchTypes = @[];
772 _pinchGestureRecognizer.delegate = self;
773 [self.flutterView addGestureRecognizer:_pinchGestureRecognizer];
774 _rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] init];
775 _rotationGestureRecognizer.allowedTouchTypes = @[];
776 _rotationGestureRecognizer.delegate = self;
777 [self.flutterView addGestureRecognizer:_rotationGestureRecognizer];
778 }
779
780 [super viewDidLoad];
781}
782
783- (void)addInternalPlugins {
784 self.keyboardManager = [[FlutterKeyboardManager alloc] init];
785 __weak FlutterViewController* weakSelf = self;
786 FlutterSendKeyEvent sendEvent =
787 ^(const FlutterKeyEvent& event, FlutterKeyEventCallback callback, void* userData) {
788 [weakSelf.engine sendKeyEvent:event callback:callback userData:userData];
789 };
790 [self.keyboardManager
791 addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] initWithSendEvent:sendEvent]];
792 FlutterChannelKeyResponder* responder =
793 [[FlutterChannelKeyResponder alloc] initWithChannel:self.engine.keyEventChannel];
794 [self.keyboardManager addPrimaryResponder:responder];
795 FlutterTextInputPlugin* textInputPlugin = self.engine.textInputPlugin;
796 if (textInputPlugin != nil) {
797 [self.keyboardManager addSecondaryResponder:textInputPlugin];
798 }
799 if (self.engine.viewController == self) {
800 [textInputPlugin setUpIndirectScribbleInteraction:self];
801 }
802}
803
804- (void)removeInternalPlugins {
805 self.keyboardManager = nil;
806}
807
808- (void)viewWillAppear:(BOOL)animated {
809 TRACE_EVENT0("flutter", "viewWillAppear");
810 if (self.engine.viewController == self) {
811 // Send platform settings to Flutter, e.g., platform brightness.
812 [self onUserSettingsChanged:nil];
813
814 // Only recreate surface on subsequent appearances when viewport metrics are known.
815 // First time surface creation is done on viewDidLayoutSubviews.
816 if (_viewportMetrics.physical_width) {
817 [self surfaceUpdated:YES];
818 }
819 [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"];
820 [self.engine.restorationPlugin markRestorationComplete];
821 }
822
823 [super viewWillAppear:animated];
824}
825
826- (void)viewDidAppear:(BOOL)animated {
827 TRACE_EVENT0("flutter", "viewDidAppear");
828 if (self.engine.viewController == self) {
829 [self onUserSettingsChanged:nil];
830 [self onAccessibilityStatusChanged:nil];
831
832 if (self.stateIsActive) {
833 [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.resumed"];
834 }
835 }
836 [super viewDidAppear:animated];
837}
838
839- (void)viewWillDisappear:(BOOL)animated {
840 TRACE_EVENT0("flutter", "viewWillDisappear");
841 if (self.engine.viewController == self) {
842 [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"];
843 }
844 [super viewWillDisappear:animated];
845}
846
847- (void)viewDidDisappear:(BOOL)animated {
848 TRACE_EVENT0("flutter", "viewDidDisappear");
849 if (self.engine.viewController == self) {
850 [self.keyboardInsetManager hideKeyboardImmediately];
851 [self surfaceUpdated:NO];
852 [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.paused"];
853 [self flushOngoingTouches];
854 [self.engine notifyLowMemory];
855 }
856
857 [super viewDidDisappear:animated];
858}
859
860- (void)viewWillTransitionToSize:(CGSize)size
861 withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
862 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
863
864 // We delay the viewport metrics update for half of rotation transition duration, to address
865 // a bug with distorted aspect ratio.
866 // See: https://github.com/flutter/flutter/issues/16322
867 //
868 // This approach does not fully resolve all distortion problem. But instead, it reduces the
869 // rotation distortion roughly from 4x to 2x. The most distorted frames occur in the middle
870 // of the transition when it is rotating the fastest, making it hard to notice.
871
872 NSTimeInterval transitionDuration = coordinator.transitionDuration;
873 // Do not delay viewport metrics update if zero transition duration.
874 if (transitionDuration == 0) {
875 return;
876 }
877
878 __weak FlutterViewController* weakSelf = self;
879 _shouldIgnoreViewportMetricsUpdatesDuringRotation = YES;
880 dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
881 static_cast<int64_t>(transitionDuration / 2.0 * NSEC_PER_SEC)),
882 dispatch_get_main_queue(), ^{
883 FlutterViewController* strongSelf = weakSelf;
884 if (!strongSelf) {
885 return;
886 }
887
888 // `viewWillTransitionToSize` is only called after the previous rotation is
889 // complete. So there won't be race condition for this flag.
890 strongSelf.shouldIgnoreViewportMetricsUpdatesDuringRotation = NO;
891 [strongSelf updateViewportMetricsIfNeeded];
892 });
893}
894
895- (void)flushOngoingTouches {
896 if (self.engine && self.ongoingTouches.count > 0) {
897 auto packet = std::make_unique<flutter::PointerDataPacket>(self.ongoingTouches.count);
898 size_t pointer_index = 0;
899 // If the view controller is going away, we want to flush cancel all the ongoing
900 // touches to the framework so nothing gets orphaned.
901 for (NSNumber* device in self.ongoingTouches) {
902 // Create fake PointerData to balance out each previously started one for the framework.
903 flutter::PointerData pointer_data = [self generatePointerDataForFake];
904
906 pointer_data.device = device.longLongValue;
907 pointer_data.pointer_identifier = 0;
908 pointer_data.view_id = self.viewIdentifier;
909
910 // Anything we put here will be arbitrary since there are no touches.
911 pointer_data.physical_x = 0;
912 pointer_data.physical_y = 0;
913 pointer_data.physical_delta_x = 0.0;
914 pointer_data.physical_delta_y = 0.0;
915 pointer_data.pressure = 1.0;
916 pointer_data.pressure_max = 1.0;
917
918 packet->SetPointerData(pointer_index++, pointer_data);
919 }
920
921 [self.ongoingTouches removeAllObjects];
922 [self.engine dispatchPointerDataPacket:std::move(packet)];
923 }
924}
925
926- (void)deregisterNotifications {
927 [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerWillDealloc
928 object:self
929 userInfo:nil];
930 [[NSNotificationCenter defaultCenter] removeObserver:self];
931}
932
933- (void)dealloc {
934 // TODO(cbracken): https://github.com/flutter/flutter/issues/157140
935 // Eliminate method calls in initializers and dealloc.
936 [self removeInternalPlugins];
937 [self deregisterNotifications];
938
939 [self.keyboardInsetManager invalidate];
940 [self invalidateTouchRateCorrectionVSyncClient];
941
942 // TODO(cbracken): https://github.com/flutter/flutter/issues/156222
943 // Ensure all delegates are weak and remove this.
944 _scrollView.delegate = nil;
945 _hoverGestureRecognizer.delegate = nil;
946 _discreteScrollingPanGestureRecognizer.delegate = nil;
947 _continuousScrollingPanGestureRecognizer.delegate = nil;
948 _pinchGestureRecognizer.delegate = nil;
949 _rotationGestureRecognizer.delegate = nil;
950}
951
952#pragma mark - Application lifecycle notifications
953
954- (void)applicationBecameActive:(NSNotification*)notification {
955 TRACE_EVENT0("flutter", "applicationBecameActive");
956 [self appOrSceneBecameActive];
957}
958
959- (void)applicationWillResignActive:(NSNotification*)notification {
960 TRACE_EVENT0("flutter", "applicationWillResignActive");
961 [self appOrSceneWillResignActive];
962}
963
964- (void)applicationWillTerminate:(NSNotification*)notification {
965 [self appOrSceneWillTerminate];
966}
967
968- (void)applicationDidEnterBackground:(NSNotification*)notification {
969 TRACE_EVENT0("flutter", "applicationDidEnterBackground");
970 [self appOrSceneDidEnterBackground];
971}
972
973- (void)applicationWillEnterForeground:(NSNotification*)notification {
974 TRACE_EVENT0("flutter", "applicationWillEnterForeground");
975 [self appOrSceneWillEnterForeground];
976}
977
978#pragma mark - Scene lifecycle notifications
979
980- (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
981 TRACE_EVENT0("flutter", "sceneBecameActive");
982 [self appOrSceneBecameActive];
983}
984
985- (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
986 TRACE_EVENT0("flutter", "sceneWillResignActive");
987 [self appOrSceneWillResignActive];
988}
989
990- (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
991 [self appOrSceneWillTerminate];
992}
993
994- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
995 TRACE_EVENT0("flutter", "sceneDidEnterBackground");
996 [self appOrSceneDidEnterBackground];
997}
998
999- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1000 TRACE_EVENT0("flutter", "sceneWillEnterForeground");
1001 [self appOrSceneWillEnterForeground];
1002}
1003
1004#pragma mark - Lifecycle shared
1005
1006- (void)appOrSceneBecameActive {
1007 self.keyboardInsetManager.isKeyboardInOrTransitioningFromBackground = NO;
1008 if (_viewportMetrics.physical_width) {
1009 [self surfaceUpdated:YES];
1010 }
1011 [self performSelector:@selector(goToApplicationLifecycle:)
1012 withObject:@"AppLifecycleState.resumed"
1013 afterDelay:0.0f];
1014}
1015
1016- (void)appOrSceneWillResignActive {
1017 [NSObject cancelPreviousPerformRequestsWithTarget:self
1018 selector:@selector(goToApplicationLifecycle:)
1019 object:@"AppLifecycleState.resumed"];
1020 [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
1021}
1022
1023- (void)appOrSceneWillTerminate {
1024 [self goToApplicationLifecycle:@"AppLifecycleState.detached"];
1025 [self.engine destroyContext];
1026}
1027
1028- (void)appOrSceneDidEnterBackground {
1029 self.keyboardInsetManager.isKeyboardInOrTransitioningFromBackground = YES;
1030 [self surfaceUpdated:NO];
1031 [self goToApplicationLifecycle:@"AppLifecycleState.paused"];
1032}
1033
1034- (void)appOrSceneWillEnterForeground {
1035 [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
1036}
1037
1038// Make this transition only while this current view controller is visible.
1039- (void)goToApplicationLifecycle:(nonnull NSString*)state {
1040 // Accessing self.view will create the view. Instead use viewIfLoaded
1041 // to check whether the view is attached to window.
1042 if (self.viewIfLoaded.window) {
1043 [self.engine.lifecycleChannel sendMessage:state];
1044 }
1045}
1046
1047#pragma mark - Touch event handling
1048
1049static flutter::PointerData::Change PointerDataChangeFromUITouchPhase(UITouchPhase phase) {
1050 switch (phase) {
1051 case UITouchPhaseBegan:
1053 case UITouchPhaseMoved:
1054 case UITouchPhaseStationary:
1055 // There is no EVENT_TYPE_POINTER_STATIONARY. So we just pass a move type
1056 // with the same coordinates
1058 case UITouchPhaseEnded:
1060 case UITouchPhaseCancelled:
1062 default:
1063 // TODO(53695): Handle the `UITouchPhaseRegion`... enum values.
1064 FML_DLOG(INFO) << "Unhandled touch phase: " << phase;
1065 break;
1066 }
1067
1069}
1070
1071static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) {
1072 switch (touch.type) {
1073 case UITouchTypeDirect:
1074 case UITouchTypeIndirect:
1076 case UITouchTypeStylus:
1078 case UITouchTypeIndirectPointer:
1080 default:
1081 FML_DLOG(INFO) << "Unhandled touch type: " << touch.type;
1082 break;
1083 }
1084
1086}
1087
1088// Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined
1089// from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events
1090// in the status bar area are available to framework code. The change type (optional) of the faked
1091// touch is specified in the second argument.
1092- (void)dispatchTouches:(NSSet*)touches
1093 pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change
1094 event:(UIEvent*)event {
1095 if (!self.engine) {
1096 return;
1097 }
1098
1099 // If the UIApplicationSupportsIndirectInputEvents in Info.plist returns YES, then the platform
1100 // dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of
1101 // UITouchTypeIndirectPointer and different identifiers for each click. They are translated into
1102 // Flutter pointer events with type of kMouse and different device IDs. These devices must be
1103 // terminated with kRemove events when the touches end, otherwise they will keep triggering hover
1104 // events.
1105 //
1106 // If the UIApplicationSupportsIndirectInputEvents in Info.plist returns NO, then the platform
1107 // dispatches indirect pointer touches (trackpad clicks) as UITouch with a type of
1108 // UITouchTypeIndirectPointer and different identifiers for each click. They are translated into
1109 // Flutter pointer events with type of kTouch and different device IDs. Removing these devices is
1110 // neither necessary nor harmful.
1111 //
1112 // Therefore Flutter always removes these devices. The touches_to_remove_count tracks how many
1113 // remove events are needed in this group of touches to properly allocate space for the packet.
1114 // The remove event of a touch is synthesized immediately after its normal event.
1115 //
1116 // See also:
1117 // https://developer.apple.com/documentation/uikit/pointer_interactions?language=objc
1118 // https://developer.apple.com/documentation/bundleresources/information_property_list/uiapplicationsupportsindirectinputevents?language=objc
1119 NSUInteger touches_to_remove_count = 0;
1120 for (UITouch* touch in touches) {
1121 if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
1122 touches_to_remove_count++;
1123 }
1124 }
1125
1126 // Activate or pause the correction of delivery frame rate of touch events.
1127 [self triggerTouchRateCorrectionIfNeeded:touches];
1128
1129 const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
1130 auto packet =
1131 std::make_unique<flutter::PointerDataPacket>(touches.count + touches_to_remove_count);
1132
1133 size_t pointer_index = 0;
1134
1135 for (UITouch* touch in touches) {
1136 CGPoint windowCoordinates = [touch locationInView:self.view];
1137
1138 flutter::PointerData pointer_data;
1139 pointer_data.Clear();
1140
1141 constexpr int kMicrosecondsPerSecond = 1000 * 1000;
1142 pointer_data.time_stamp = touch.timestamp * kMicrosecondsPerSecond;
1143
1144 pointer_data.change = overridden_change != nullptr
1145 ? *overridden_change
1146 : PointerDataChangeFromUITouchPhase(touch.phase);
1147
1148 pointer_data.kind = DeviceKindFromTouchType(touch);
1149
1150 pointer_data.device = reinterpret_cast<int64_t>(touch);
1151
1152 pointer_data.view_id = self.viewIdentifier;
1153
1154 // Pointer will be generated in pointer_data_packet_converter.cc.
1155 pointer_data.pointer_identifier = 0;
1156
1157 pointer_data.physical_x = windowCoordinates.x * scale;
1158 pointer_data.physical_y = windowCoordinates.y * scale;
1159
1160 // Delta will be generated in pointer_data_packet_converter.cc.
1161 pointer_data.physical_delta_x = 0.0;
1162 pointer_data.physical_delta_y = 0.0;
1163
1164 NSNumber* deviceKey = [NSNumber numberWithLongLong:pointer_data.device];
1165 // Track touches that began and not yet stopped so we can flush them
1166 // if the view controller goes away.
1167 switch (pointer_data.change) {
1169 [self.ongoingTouches addObject:deviceKey];
1170 break;
1173 [self.ongoingTouches removeObject:deviceKey];
1174 break;
1177 // We're only tracking starts and stops.
1178 break;
1181 // We don't use kAdd/kRemove.
1182 break;
1186 // We don't send pan/zoom events here
1187 break;
1188 }
1189
1190 // pressure_min is always 0.0
1191 pointer_data.pressure = touch.force;
1192 pointer_data.pressure_max = touch.maximumPossibleForce;
1193 pointer_data.radius_major = touch.majorRadius;
1194 pointer_data.radius_min = touch.majorRadius - touch.majorRadiusTolerance;
1195 pointer_data.radius_max = touch.majorRadius + touch.majorRadiusTolerance;
1196
1197 // iOS Documentation: altitudeAngle
1198 // A value of 0 radians indicates that the stylus is parallel to the surface. The value of
1199 // this property is Pi/2 when the stylus is perpendicular to the surface.
1200 //
1201 // PointerData Documentation: tilt
1202 // The angle of the stylus, in radians in the range:
1203 // 0 <= tilt <= pi/2
1204 // giving the angle of the axis of the stylus, relative to the axis perpendicular to the input
1205 // surface (thus 0.0 indicates the stylus is orthogonal to the plane of the input surface,
1206 // while pi/2 indicates that the stylus is flat on that surface).
1207 //
1208 // Discussion:
1209 // The ranges are the same. Origins are swapped.
1210 pointer_data.tilt = M_PI_2 - touch.altitudeAngle;
1211
1212 // iOS Documentation: azimuthAngleInView:
1213 // With the tip of the stylus touching the screen, the value of this property is 0 radians
1214 // when the cap end of the stylus (that is, the end opposite of the tip) points along the
1215 // positive x axis of the device's screen. The azimuth angle increases as the user swings the
1216 // cap end of the stylus in a clockwise direction around the tip.
1217 //
1218 // PointerData Documentation: orientation
1219 // The angle of the stylus, in radians in the range:
1220 // -pi < orientation <= pi
1221 // giving the angle of the axis of the stylus projected onto the input surface, relative to
1222 // the positive y-axis of that surface (thus 0.0 indicates the stylus, if projected onto that
1223 // surface, would go from the contact point vertically up in the positive y-axis direction, pi
1224 // would indicate that the stylus would go down in the negative y-axis direction; pi/4 would
1225 // indicate that the stylus goes up and to the right, -pi/2 would indicate that the stylus
1226 // goes to the left, etc).
1227 //
1228 // Discussion:
1229 // Sweep direction is the same. Phase of M_PI_2.
1230 pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2;
1231
1232 if (@available(iOS 13.4, *)) {
1233 if (event != nullptr) {
1234 pointer_data.buttons = (((event.buttonMask & UIEventButtonMaskPrimary) > 0)
1236 : 0) |
1237 (((event.buttonMask & UIEventButtonMaskSecondary) > 0)
1239 : 0);
1240 }
1241 }
1242
1243 packet->SetPointerData(pointer_index++, pointer_data);
1244
1245 if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
1246 flutter::PointerData remove_pointer_data = pointer_data;
1247 remove_pointer_data.change = flutter::PointerData::Change::kRemove;
1248 packet->SetPointerData(pointer_index++, remove_pointer_data);
1249 }
1250 }
1251
1252 [self.engine dispatchPointerDataPacket:std::move(packet)];
1253}
1254
1255- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1256 [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1257}
1258
1259- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1260 [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1261}
1262
1263- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1264 [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1265}
1266
1267- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1268 [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1269}
1270
1271- (void)forceTouchesCancelled:(NSSet*)touches {
1273 [self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
1274}
1275
1276- (BOOL)platformViewShouldAcceptTouchAtTouchBeganLocation:(CGPoint)location {
1278 return [self.engine platformViewShouldAcceptTouchAtTouchBeganLocation:point
1279 viewId:self.viewIdentifier];
1280}
1281
1282#pragma mark - Touch events rate correction
1283
1284- (void)createTouchRateCorrectionVSyncClientIfNeeded {
1285 if (_touchRateCorrectionVSyncClient != nil) {
1286 return;
1287 }
1288
1289 double displayRefreshRate = FlutterDisplayLinkManager.displayRefreshRate;
1290 const double epsilon = 0.1;
1291 if (displayRefreshRate < 60.0 + epsilon) { // displayRefreshRate <= 60.0
1292
1293 // If current device's max frame rate is not larger than 60HZ, the delivery rate of touch events
1294 // is the same with render vsync rate. So it is unnecessary to create
1295 // _touchRateCorrectionVSyncClient to correct touch callback's rate.
1296 return;
1297 }
1298
1299 void (^callback)(CFTimeInterval, CFTimeInterval) =
1300 ^(CFTimeInterval startTime, CFTimeInterval targetTime) {
1301 // Do nothing in this block. Just trigger system to callback touch events with correct rate.
1302 };
1303 _touchRateCorrectionVSyncClient = [[FlutterVSyncClient alloc]
1304 initWithTaskRunner:self.engine.platformTaskRunner
1305 isVariableRefreshRateEnabled:FlutterDisplayLinkManager.maxRefreshRateEnabledOnIPhone
1306 maxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate
1307 callback:callback];
1308 _touchRateCorrectionVSyncClient.allowPauseAfterVsync = NO;
1309}
1310
1311- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches {
1312 if (_touchRateCorrectionVSyncClient == nil) {
1313 // If the _touchRateCorrectionVSyncClient is not created, means current devices doesn't
1314 // need to correct the touch rate. So just return.
1315 return;
1316 }
1317
1318 // As long as there is a touch's phase is UITouchPhaseBegan or UITouchPhaseMoved,
1319 // activate the correction. Otherwise pause the correction.
1320 BOOL isUserInteracting = NO;
1321 for (UITouch* touch in touches) {
1322 if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
1323 isUserInteracting = YES;
1324 break;
1325 }
1326 }
1327
1328 if (isUserInteracting && self.engine.viewController == self) {
1329 [_touchRateCorrectionVSyncClient await];
1330 } else {
1331 [_touchRateCorrectionVSyncClient pause];
1332 }
1333}
1334
1335- (void)invalidateTouchRateCorrectionVSyncClient {
1336 [_touchRateCorrectionVSyncClient invalidate];
1337 _touchRateCorrectionVSyncClient = nil;
1338}
1339
1340#pragma mark - Handle view resizing
1341
1342- (void)updateViewportMetricsIfNeeded {
1343 if (_shouldIgnoreViewportMetricsUpdatesDuringRotation) {
1344 return;
1345 }
1346 if (self.engine.viewController == self) {
1347 [self.engine updateViewportMetrics:_viewportMetrics];
1348 }
1349}
1350
1351- (void)viewDidLayoutSubviews {
1352 CGRect viewBounds = self.view.bounds;
1353 CGFloat scale = self.flutterScreenIfViewLoaded.scale;
1354
1355 // Purposefully place this not visible.
1356 self.scrollView.frame = CGRectMake(0.0, 0.0, viewBounds.size.width, 0.0);
1357 self.scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
1358
1359 // First time since creation that the dimensions of its view is known.
1360 bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
1361 _viewportMetrics.device_pixel_ratio = scale;
1362 [self setViewportMetricsSize];
1363 [self checkAndUpdateAutoResizeConstraints];
1364 [self setViewportMetricsPaddings];
1365 [self updateViewportMetricsIfNeeded];
1366
1367 // There is no guarantee that UIKit will layout subviews when the application/scene is active.
1368 // Creating the surface when inactive will cause GPU accesses from the background. Only wait for
1369 // the first frame to render when the application/scene is actually active.
1370 // This must run after updateViewportMetrics so that the surface creation tasks are queued after
1371 // the viewport metrics update tasks.
1372 if (firstViewBoundsUpdate && self.stateIsActive && self.engine) {
1373 [self surfaceUpdated:YES];
1374#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
1375 NSTimeInterval timeout = 0.2;
1376#else
1377 NSTimeInterval timeout = 0.1;
1378#endif
1379 [self.engine
1380 waitForFirstFrameSync:timeout
1381 callback:^(BOOL didTimeout) {
1382 if (didTimeout) {
1383 [FlutterLogger logInfo:@"Timeout waiting for the first frame to render. "
1384 "This may happen in unoptimized builds. If this is"
1385 "a release build, you should load a less complex "
1386 "frame to avoid the timeout."];
1387 }
1388 }];
1389 }
1390}
1391
1392- (BOOL)isAutoResizable {
1393 return self.flutterView.autoResizable;
1394}
1395
1396- (void)setAutoResizable:(BOOL)value {
1397 self.flutterView.autoResizable = value;
1398 self.flutterView.contentMode = UIViewContentModeCenter;
1399}
1400
1401- (void)checkAndUpdateAutoResizeConstraints {
1402 if (!self.isAutoResizable) {
1403 return;
1404 }
1405
1406 [self updateAutoResizeConstraints];
1407}
1408
1409/**
1410 * Updates the FlutterAutoResizeLayoutConstraints based on the view's
1411 * current frame.
1412 *
1413 * This method is invoked during viewDidLayoutSubviews, at which point the
1414 * view has completed its subview layout and applied any existing Auto Layout
1415 * constraints.
1416 *
1417 * Initially, the view's frame is used to determine the maximum size allowed
1418 * by the native layout system. This size is then used to establish the viewport
1419 * constraints for the Flutter engine.
1420 *
1421 * A critical consideration is that this initial frame-based sizing is only
1422 * applicable if FlutterAutoResizeLayoutConstraints have not yet been applied
1423 * by Flutter. Once Flutter applies its own FlutterAutoResizeLayoutConstraints,
1424 * these constraints will subsequently dictate the view's frame.
1425 *
1426 * This interaction imposes a limitation: native layout constraints that are
1427 * updated after Flutter has applied its auto-resize constraints may not
1428 * function as expected or properly influence the FlutterView's size.
1429 */
1430- (void)updateAutoResizeConstraints {
1431 BOOL hasBeenAutoResized = NO;
1432 for (NSLayoutConstraint* constraint in self.view.constraints) {
1433 if ([constraint isKindOfClass:[FlutterAutoResizeLayoutConstraint class]]) {
1434 hasBeenAutoResized = YES;
1435 break;
1436 }
1437 }
1438 if (!hasBeenAutoResized) {
1439 self.sizeBeforeAutoResized = self.view.frame.size;
1440 }
1441
1442 CGFloat maxWidth = self.sizeBeforeAutoResized.width;
1443 CGFloat maxHeight = self.sizeBeforeAutoResized.height;
1444 CGFloat minWidth = self.sizeBeforeAutoResized.width;
1445 CGFloat minHeight = self.sizeBeforeAutoResized.height;
1446
1447 // maxWidth or maxHeight may be 0 when the width/height are ambiguous, eg. for
1448 // unsized widgets
1449 if (maxWidth == 0) {
1450 maxWidth = CGFLOAT_MAX;
1451 [FlutterLogger
1452 logWarning:
1453 @"Warning: The outermost widget in the autoresizable Flutter view is unsized or has "
1454 @"ambiguous dimensions, causing the host native view's width to be 0. The autoresizing "
1455 @"logic is setting the viewport constraint to unbounded DBL_MAX to prevent "
1456 @"rendering failure. Please ensure your top-level Flutter widget has explicit "
1457 @"constraints (e.g., using SizedBox or Container)."];
1458 }
1459 if (maxHeight == 0) {
1460 maxHeight = CGFLOAT_MAX;
1461 [FlutterLogger
1462 logWarning:
1463 @"Warning: The outermost widget in the autoresizable Flutter view is unsized or has "
1464 @"ambiguous dimensions, causing the host native view's width to be 0. The autoresizing "
1465 @"logic is setting the viewport constraint to unbounded DBL_MAX to prevent "
1466 @"rendering failure. Please ensure your top-level Flutter widget has explicit "
1467 @"constraints (e.g., using SizedBox or Container)."];
1468 }
1469 _viewportMetrics.physical_min_width_constraint = minWidth * _viewportMetrics.device_pixel_ratio;
1470 _viewportMetrics.physical_max_width_constraint = maxWidth * _viewportMetrics.device_pixel_ratio;
1471 _viewportMetrics.physical_min_height_constraint = minHeight * _viewportMetrics.device_pixel_ratio;
1472 _viewportMetrics.physical_max_height_constraint = maxHeight * _viewportMetrics.device_pixel_ratio;
1473}
1474
1475- (void)viewSafeAreaInsetsDidChange {
1476 [self setViewportMetricsPaddings];
1477 [self updateViewportMetricsIfNeeded];
1478 [super viewSafeAreaInsetsDidChange];
1479}
1480
1481// Set _viewportMetrics physical size.
1482- (void)setViewportMetricsSize {
1483 UIScreen* screen = self.flutterScreenIfViewLoaded;
1484 if (!screen) {
1485 return;
1486 }
1487
1488 CGFloat scale = screen.scale;
1489 _viewportMetrics.physical_width = self.view.bounds.size.width * scale;
1490 _viewportMetrics.physical_height = self.view.bounds.size.height * scale;
1491 // TODO(louisehsu): update for https://github.com/flutter/flutter/issues/169147
1492 _viewportMetrics.physical_min_width_constraint = _viewportMetrics.physical_width;
1493 _viewportMetrics.physical_max_width_constraint = _viewportMetrics.physical_width;
1494 _viewportMetrics.physical_min_height_constraint = _viewportMetrics.physical_height;
1495 _viewportMetrics.physical_max_height_constraint = _viewportMetrics.physical_height;
1496}
1497
1498// Set _viewportMetrics physical paddings.
1499//
1500// Viewport paddings represent the iOS safe area insets.
1501- (void)setViewportMetricsPaddings {
1502 UIScreen* screen = self.flutterScreenIfViewLoaded;
1503 if (!screen) {
1504 return;
1505 }
1506
1507 CGFloat scale = screen.scale;
1508 _viewportMetrics.physical_padding_top = self.view.safeAreaInsets.top * scale;
1509 _viewportMetrics.physical_padding_left = self.view.safeAreaInsets.left * scale;
1510 _viewportMetrics.physical_padding_right = self.view.safeAreaInsets.right * scale;
1511 _viewportMetrics.physical_padding_bottom = self.view.safeAreaInsets.bottom * scale;
1512}
1513
1514#pragma mark - Keyboard events
1515
1516- (void)keyboardWillShowNotification:(NSNotification*)notification {
1517 // Immediately prior to a docked keyboard being shown or when a keyboard goes from
1518 // undocked/floating to docked, this notification is triggered. This notification also happens
1519 // when Minimized/Expanded Shortcuts bar is dropped after dragging (the keyboard's end frame will
1520 // be CGRectZero).
1521 [self.keyboardInsetManager handleKeyboardNotification:notification];
1522}
1523
1524- (void)keyboardWillChangeFrame:(NSNotification*)notification {
1525 // Immediately prior to a change in keyboard frame, this notification is triggered.
1526 // Sometimes when the keyboard is being hidden or undocked, this notification's keyboard's end
1527 // frame is not yet entirely out of screen, which is why we also use
1528 // UIKeyboardWillHideNotification.
1529 [self.keyboardInsetManager handleKeyboardNotification:notification];
1530}
1531
1532- (void)keyboardWillBeHidden:(NSNotification*)notification {
1533 // When keyboard is hidden or undocked, this notification will be triggered.
1534 // This notification might not occur when the keyboard is changed from docked to floating, which
1535 // is why we also use UIKeyboardWillChangeFrameNotification.
1536 [self.keyboardInsetManager handleKeyboardNotification:notification];
1537}
1538
1539- (void)handlePressEvent:(FlutterUIPressProxy*)press
1540 nextAction:(void (^)())next API_AVAILABLE(ios(13.4)) {
1541 if (@available(iOS 13.4, *)) {
1542 } else {
1543 next();
1544 return;
1545 }
1546 [self.keyboardManager handlePress:press nextAction:next];
1547}
1548
1549// The documentation for presses* handlers (implemented below) is entirely
1550// unclear about how to handle the case where some, but not all, of the presses
1551// are handled here. I've elected to call super separately for each of the
1552// presses that aren't handled, but it's not clear if this is correct. It may be
1553// that iOS intends for us to either handle all or none of the presses, and pass
1554// the original set to super. I have not yet seen multiple presses in the set in
1555// the wild, however, so I suspect that the API is built for a tvOS remote or
1556// something, and perhaps only one ever appears in the set on iOS from a
1557// keyboard.
1558//
1559// We define separate superPresses* overrides to avoid implicitly capturing self in the blocks
1560// passed to the presses* methods below.
1561
1562- (void)superPressesBegan:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1563 [super pressesBegan:presses withEvent:event];
1564}
1565
1566- (void)superPressesChanged:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1567 [super pressesChanged:presses withEvent:event];
1568}
1569
1570- (void)superPressesEnded:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1571 [super pressesEnded:presses withEvent:event];
1572}
1573
1574- (void)superPressesCancelled:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1575 [super pressesCancelled:presses withEvent:event];
1576}
1577
1578// If you substantially change these presses overrides, consider also changing
1579// the similar ones in FlutterTextInputPlugin. They need to be overridden in
1580// both places to capture keys both inside and outside of a text field, but have
1581// slightly different implementations.
1582
1583- (void)pressesBegan:(NSSet<UIPress*>*)presses
1584 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1585 if (@available(iOS 13.4, *)) {
1586 __weak FlutterViewController* weakSelf = self;
1587 for (UIPress* press in presses) {
1588 [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1589 nextAction:^() {
1590 [weakSelf superPressesBegan:[NSSet setWithObject:press] withEvent:event];
1591 }];
1592 }
1593 } else {
1594 [super pressesBegan:presses withEvent:event];
1595 }
1596}
1597
1598- (void)pressesChanged:(NSSet<UIPress*>*)presses
1599 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1600 if (@available(iOS 13.4, *)) {
1601 __weak FlutterViewController* weakSelf = self;
1602 for (UIPress* press in presses) {
1603 [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1604 nextAction:^() {
1605 [weakSelf superPressesChanged:[NSSet setWithObject:press] withEvent:event];
1606 }];
1607 }
1608 } else {
1609 [super pressesChanged:presses withEvent:event];
1610 }
1611}
1612
1613- (void)pressesEnded:(NSSet<UIPress*>*)presses
1614 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1615 if (@available(iOS 13.4, *)) {
1616 __weak FlutterViewController* weakSelf = self;
1617 for (UIPress* press in presses) {
1618 [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1619 nextAction:^() {
1620 [weakSelf superPressesEnded:[NSSet setWithObject:press] withEvent:event];
1621 }];
1622 }
1623 } else {
1624 [super pressesEnded:presses withEvent:event];
1625 }
1626}
1627
1628- (void)pressesCancelled:(NSSet<UIPress*>*)presses
1629 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1630 if (@available(iOS 13.4, *)) {
1631 __weak FlutterViewController* weakSelf = self;
1632 for (UIPress* press in presses) {
1633 [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1634 nextAction:^() {
1635 [weakSelf superPressesCancelled:[NSSet setWithObject:press] withEvent:event];
1636 }];
1637 }
1638 } else {
1639 [super pressesCancelled:presses withEvent:event];
1640 }
1641}
1642
1643#pragma mark - Orientation updates
1644
1645- (void)onOrientationPreferencesUpdated:(NSNotification*)notification {
1646 // Notifications may not be on the iOS UI thread
1647 __weak FlutterViewController* weakSelf = self;
1648 dispatch_async(dispatch_get_main_queue(), ^{
1649 NSDictionary* info = notification.userInfo;
1650 NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)];
1651 if (update == nil) {
1652 return;
1653 }
1654 [weakSelf performOrientationUpdate:update.unsignedIntegerValue];
1655 });
1656}
1657
1658- (void)requestGeometryUpdateForWindowScenes:(NSSet<UIScene*>*)windowScenes
1659 API_AVAILABLE(ios(16.0)) {
1660 for (UIScene* windowScene in windowScenes) {
1661 FML_DCHECK([windowScene isKindOfClass:[UIWindowScene class]]);
1662 UIWindowSceneGeometryPreferencesIOS* preference = [[UIWindowSceneGeometryPreferencesIOS alloc]
1663 initWithInterfaceOrientations:self.orientationPreferences];
1664 [(UIWindowScene*)windowScene
1665 requestGeometryUpdateWithPreferences:preference
1666 errorHandler:^(NSError* error) {
1667 os_log_error(OS_LOG_DEFAULT,
1668 "Failed to change device orientation: %@", error);
1669 }];
1670 [self setNeedsUpdateOfSupportedInterfaceOrientations];
1671 }
1672}
1673
1674- (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
1675 if (new_preferences != self.orientationPreferences) {
1676 self.orientationPreferences = new_preferences;
1677
1678 if (@available(iOS 16.0, *)) {
1679 UIApplication* flutterApplication = FlutterSharedApplication.application;
1680 NSSet<UIScene*>* scenes = [NSSet set];
1681 if (flutterApplication) {
1682 scenes = [flutterApplication.connectedScenes
1683 filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
1684 id scene, NSDictionary* bindings) {
1685 return [scene isKindOfClass:[UIWindowScene class]];
1686 }]];
1687 } else if (self.flutterWindowSceneIfViewLoaded) {
1688 scenes = [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded];
1689 }
1690 [self requestGeometryUpdateForWindowScenes:scenes];
1691 } else {
1692 UIInterfaceOrientationMask currentInterfaceOrientation = 0;
1693 UIWindowScene* windowScene = self.flutterWindowSceneIfViewLoaded;
1694 if (!windowScene) {
1695 [FlutterLogger
1696 logWarning:
1697 @"Accessing the interface orientation when the window scene is unavailable."];
1698 return;
1699 }
1700 currentInterfaceOrientation = 1 << windowScene.interfaceOrientation;
1701 if (!(self.orientationPreferences & currentInterfaceOrientation)) {
1702 [UIViewController attemptRotationToDeviceOrientation];
1703 // Force orientation switch if the current orientation is not allowed
1704 if (self.orientationPreferences & UIInterfaceOrientationMaskPortrait) {
1705 // This is no official API but more like a workaround / hack (using
1706 // key-value coding on a read-only property). This might break in
1707 // the future, but currently it´s the only way to force an orientation change
1708 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait)
1709 forKey:@"orientation"];
1710 } else if (self.orientationPreferences & UIInterfaceOrientationMaskPortraitUpsideDown) {
1711 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortraitUpsideDown)
1712 forKey:@"orientation"];
1713 } else if (self.orientationPreferences & UIInterfaceOrientationMaskLandscapeLeft) {
1714 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeLeft)
1715 forKey:@"orientation"];
1716 } else if (self.orientationPreferences & UIInterfaceOrientationMaskLandscapeRight) {
1717 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeRight)
1718 forKey:@"orientation"];
1719 }
1720 }
1721 }
1722 }
1723}
1724
1725- (void)onHideHomeIndicatorNotification:(NSNotification*)notification {
1726 self.isHomeIndicatorHidden = YES;
1727}
1728
1729- (void)onShowHomeIndicatorNotification:(NSNotification*)notification {
1730 self.isHomeIndicatorHidden = NO;
1731}
1732
1733- (void)setIsHomeIndicatorHidden:(BOOL)hideHomeIndicator {
1734 if (hideHomeIndicator != _isHomeIndicatorHidden) {
1735 _isHomeIndicatorHidden = hideHomeIndicator;
1736 [self setNeedsUpdateOfHomeIndicatorAutoHidden];
1737 }
1738}
1739
1740- (BOOL)prefersHomeIndicatorAutoHidden {
1741 return self.isHomeIndicatorHidden;
1742}
1743
1744- (BOOL)shouldAutorotate {
1745 return YES;
1746}
1747
1748- (NSUInteger)supportedInterfaceOrientations {
1749 return self.orientationPreferences;
1750}
1751
1752#pragma mark - Accessibility
1753
1754- (void)onAccessibilityStatusChanged:(NSNotification*)notification {
1755 if (!self.engine) {
1756 return;
1757 }
1758 BOOL enabled = NO;
1759 int32_t flags = [self.accessibilityFeatures flags];
1760#if TARGET_OS_SIMULATOR
1761 // There doesn't appear to be any way to determine whether the accessibility
1762 // inspector is enabled on the simulator. We conservatively always turn on the
1763 // accessibility bridge in the simulator, but never assistive technology.
1764 enabled = YES;
1765#else
1766 _isVoiceOverRunning = [self.accessibilityFeatures isVoiceOverRunning];
1767 enabled = _isVoiceOverRunning || [self.accessibilityFeatures isSwitchControlRunning] ||
1768 [self.accessibilityFeatures isSpeakScreenEnabled];
1769#endif
1770 [self.engine enableSemantics:enabled withFlags:flags];
1771}
1772
1773- (BOOL)accessibilityPerformEscape {
1774 FlutterMethodChannel* navigationChannel = self.engine.navigationChannel;
1775 if (navigationChannel) {
1776 [self popRoute];
1777 return YES;
1778 }
1779 return NO;
1780}
1781
1782#pragma mark - Set user settings
1783
1784- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
1785 [super traitCollectionDidChange:previousTraitCollection];
1786 [self onUserSettingsChanged:nil];
1787
1788 // Since this method can get triggered by changes in device orientation, reset and recalculate the
1789 // instrinsic size.
1790 if (self.isAutoResizable) {
1791 [self.flutterView resetIntrinsicContentSize];
1792 }
1793}
1794
1795- (void)onUserSettingsChanged:(NSNotification*)notification {
1796 [self.engine.settingsChannel sendMessage:@{
1797 @"textScaleFactor" : @(self.textScaleFactor),
1798 @"alwaysUse24HourFormat" : @(FlutterHourFormat.isAlwaysUse24HourFormat),
1799 @"platformBrightness" : self.brightnessMode,
1800 @"platformContrast" : self.contrastMode,
1801 @"nativeSpellCheckServiceDefined" : @YES,
1802 @"supportsShowingSystemContextMenu" : @(self.supportsShowingSystemContextMenu)
1803 }];
1804}
1805
1806- (CGFloat)textScaleFactor {
1807 UIApplication* flutterApplication = FlutterSharedApplication.application;
1808 if (flutterApplication == nil) {
1809 [FlutterLogger logWarning:@"Dynamic content size update is not supported in app extension."];
1810 return 1.0;
1811 }
1812
1813 UIContentSizeCategory category = flutterApplication.preferredContentSizeCategory;
1814 // The delta is computed by approximating Apple's typography guidelines:
1815 // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/
1816 //
1817 // Specifically:
1818 // Non-accessibility sizes for "body" text are:
1819 const CGFloat xs = 14;
1820 const CGFloat s = 15;
1821 const CGFloat m = 16;
1822 const CGFloat l = 17;
1823 const CGFloat xl = 19;
1824 const CGFloat xxl = 21;
1825 const CGFloat xxxl = 23;
1826
1827 // Accessibility sizes for "body" text are:
1828 const CGFloat ax1 = 28;
1829 const CGFloat ax2 = 33;
1830 const CGFloat ax3 = 40;
1831 const CGFloat ax4 = 47;
1832 const CGFloat ax5 = 53;
1833
1834 // We compute the scale as relative difference from size L (large, the default size), where
1835 // L is assumed to have scale 1.0.
1836 if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) {
1837 return xs / l;
1838 } else if ([category isEqualToString:UIContentSizeCategorySmall]) {
1839 return s / l;
1840 } else if ([category isEqualToString:UIContentSizeCategoryMedium]) {
1841 return m / l;
1842 } else if ([category isEqualToString:UIContentSizeCategoryLarge]) {
1843 return 1.0;
1844 } else if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) {
1845 return xl / l;
1846 } else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) {
1847 return xxl / l;
1848 } else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) {
1849 return xxxl / l;
1850 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) {
1851 return ax1 / l;
1852 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) {
1853 return ax2 / l;
1854 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) {
1855 return ax3 / l;
1856 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) {
1857 return ax4 / l;
1858 } else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) {
1859 return ax5 / l;
1860 } else {
1861 return 1.0;
1862 }
1863}
1864
1865- (BOOL)supportsShowingSystemContextMenu {
1866 if (@available(iOS 16.0, *)) {
1867 return YES;
1868 } else {
1869 return NO;
1870 }
1871}
1872
1873// The brightness mode of the platform, e.g., light or dark, expressed as a string that
1874// is understood by the Flutter framework. See the settings
1875// system channel for more information.
1876- (NSString*)brightnessMode {
1877 UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;
1878
1879 if (style == UIUserInterfaceStyleDark) {
1880 return @"dark";
1881 } else {
1882 return @"light";
1883 }
1884}
1885
1886// The contrast mode of the platform, e.g., normal or high, expressed as a string that is
1887// understood by the Flutter framework. See the settings system channel for more
1888// information.
1889- (NSString*)contrastMode {
1890 UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast;
1891
1892 if (contrast == UIAccessibilityContrastHigh) {
1893 return @"high";
1894 } else {
1895 return @"normal";
1896 }
1897}
1898
1899#pragma mark - Status bar style
1900
1901- (UIStatusBarStyle)preferredStatusBarStyle {
1902 return self.statusBarStyle;
1903}
1904
1905- (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
1906 // Notifications may not be on the iOS UI thread
1907 __weak FlutterViewController* weakSelf = self;
1908 dispatch_async(dispatch_get_main_queue(), ^{
1909 FlutterViewController* strongSelf = weakSelf;
1910 if (!strongSelf) {
1911 return;
1912 }
1913
1914 NSDictionary* info = notification.userInfo;
1915 NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)];
1916 if (update == nil) {
1917 return;
1918 }
1919
1920 UIStatusBarStyle style = static_cast<UIStatusBarStyle>(update.integerValue);
1921 if (style != strongSelf.statusBarStyle) {
1922 strongSelf.statusBarStyle = style;
1923 [strongSelf setNeedsStatusBarAppearanceUpdate];
1924 }
1925 });
1926}
1927
1928- (void)setPrefersStatusBarHidden:(BOOL)hidden {
1929 if (hidden != self.flutterPrefersStatusBarHidden) {
1930 self.flutterPrefersStatusBarHidden = hidden;
1931 [self setNeedsStatusBarAppearanceUpdate];
1932 }
1933}
1934
1935- (BOOL)prefersStatusBarHidden {
1936 return self.flutterPrefersStatusBarHidden;
1937}
1938
1939#pragma mark - Platform views
1940
1941- (FlutterPlatformViewsController*)platformViewsController {
1942 return self.engine.platformViewsController;
1943}
1944
1945- (NSObject<FlutterBinaryMessenger>*)binaryMessenger {
1946 return self.engine.binaryMessenger;
1947}
1948
1949#pragma mark - FlutterBinaryMessenger
1950
1951- (void)sendOnChannel:(NSString*)channel message:(NSData*)message {
1952 [self.engine.binaryMessenger sendOnChannel:channel message:message];
1953}
1954
1955- (void)sendOnChannel:(NSString*)channel
1956 message:(NSData*)message
1957 binaryReply:(FlutterBinaryReply)callback {
1958 NSAssert(channel, @"The channel must not be null");
1959 [self.engine.binaryMessenger sendOnChannel:channel message:message binaryReply:callback];
1960}
1961
1962- (NSObject<FlutterTaskQueue>*)makeBackgroundTaskQueue {
1963 return [self.engine.binaryMessenger makeBackgroundTaskQueue];
1964}
1965
1966- (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channel
1967 binaryMessageHandler:
1969 return [self setMessageHandlerOnChannel:channel binaryMessageHandler:handler taskQueue:nil];
1970}
1971
1973 setMessageHandlerOnChannel:(NSString*)channel
1974 binaryMessageHandler:(FlutterBinaryMessageHandler _Nullable)handler
1975 taskQueue:(NSObject<FlutterTaskQueue>* _Nullable)taskQueue {
1976 NSAssert(channel, @"The channel must not be null");
1977 return [self.engine.binaryMessenger setMessageHandlerOnChannel:channel
1978 binaryMessageHandler:handler
1979 taskQueue:taskQueue];
1980}
1981
1982- (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection {
1983 [self.engine.binaryMessenger cleanUpConnection:connection];
1984}
1985
1986#pragma mark - FlutterTextureRegistry
1987
1988- (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture {
1989 return [self.engine.textureRegistry registerTexture:texture];
1990}
1991
1992- (void)unregisterTexture:(int64_t)textureId {
1993 [self.engine.textureRegistry unregisterTexture:textureId];
1994}
1995
1996- (void)textureFrameAvailable:(int64_t)textureId {
1997 [self.engine.textureRegistry textureFrameAvailable:textureId];
1998}
1999
2000- (NSString*)lookupKeyForAsset:(NSString*)asset {
2002}
2003
2004- (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
2005 return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
2006}
2007
2008- (id<FlutterPluginRegistry>)pluginRegistry {
2009 return self.engine;
2010}
2011
2012+ (BOOL)isUIAccessibilityIsVoiceOverRunning {
2013 return UIAccessibilityIsVoiceOverRunning();
2014}
2015
2016#pragma mark - FlutterPluginRegistry
2017
2018- (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
2019 return [self.engine registrarForPlugin:pluginKey];
2020}
2021
2022- (BOOL)hasPlugin:(NSString*)pluginKey {
2023 return [self.engine hasPlugin:pluginKey];
2024}
2025
2026- (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
2027 return [self.engine valuePublishedByPlugin:pluginKey];
2028}
2029
2030- (void)presentViewController:(UIViewController*)viewControllerToPresent
2031 animated:(BOOL)flag
2032 completion:(void (^)(void))completion {
2033 self.isPresentingViewControllerAnimating = YES;
2034 __weak FlutterViewController* weakSelf = self;
2035 [super presentViewController:viewControllerToPresent
2036 animated:flag
2037 completion:^{
2038 weakSelf.isPresentingViewControllerAnimating = NO;
2039 if (completion) {
2040 completion();
2041 }
2042 }];
2043}
2044
2045- (BOOL)isPresentingViewController {
2046 return self.presentedViewController != nil || self.isPresentingViewControllerAnimating;
2047}
2048
2049- (flutter::PointerData)updateMousePointerDataFrom:(UIGestureRecognizer*)gestureRecognizer
2050 API_AVAILABLE(ios(13.4)) {
2051 CGPoint location = [gestureRecognizer locationInView:self.view];
2052 CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2053 _mouseState.location = {location.x * scale, location.y * scale};
2054 flutter::PointerData pointer_data;
2055 pointer_data.Clear();
2056 pointer_data.time_stamp = [[NSProcessInfo processInfo] systemUptime] * kMicrosecondsPerSecond;
2057 pointer_data.physical_x = _mouseState.location.x;
2058 pointer_data.physical_y = _mouseState.location.y;
2059 return pointer_data;
2060}
2061
2062- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2063 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
2064 API_AVAILABLE(ios(13.4)) {
2065 return YES;
2066}
2067
2068- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2069 shouldReceiveEvent:(UIEvent*)event API_AVAILABLE(ios(13.4)) {
2070 if (gestureRecognizer == _continuousScrollingPanGestureRecognizer &&
2071 event.type == UIEventTypeScroll) {
2072 // Events with type UIEventTypeScroll are only received when running on macOS under emulation.
2073 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:gestureRecognizer];
2074 pointer_data.device = reinterpret_cast<int64_t>(_continuousScrollingPanGestureRecognizer);
2077 pointer_data.view_id = self.viewIdentifier;
2078
2079 if (event.timestamp < self.scrollInertiaEventAppKitDeadline) {
2080 // Only send the event if it occured before the expected natural end of gesture momentum.
2081 // If received after the deadline, it's not likely the event is from a user-initiated cancel.
2082 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2083 packet->SetPointerData(/*i=*/0, pointer_data);
2084 [self.engine dispatchPointerDataPacket:std::move(packet)];
2085 self.scrollInertiaEventAppKitDeadline = 0;
2086 }
2087 }
2088 // This method is also called for UITouches, should return YES to process all touches.
2089 return YES;
2090}
2091
2092- (void)hoverEvent:(UIHoverGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2093 CGPoint oldLocation = _mouseState.location;
2094
2095 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2096 pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2098 pointer_data.view_id = self.viewIdentifier;
2099
2100 switch (_hoverGestureRecognizer.state) {
2101 case UIGestureRecognizerStateBegan:
2103 break;
2104 case UIGestureRecognizerStateChanged:
2106 break;
2107 case UIGestureRecognizerStateEnded:
2108 case UIGestureRecognizerStateCancelled:
2110 break;
2111 default:
2112 // Sending kHover is the least harmful thing to do here
2113 // But this state is not expected to ever be reached.
2115 break;
2116 }
2117
2118 NSTimeInterval time = [NSProcessInfo processInfo].systemUptime;
2119 BOOL isRunningOnMac = NO;
2120 if (@available(iOS 14.0, *)) {
2121 // This "stationary pointer" heuristic is not reliable when running within macOS.
2122 // We instead receive a scroll cancel event directly from AppKit.
2123 // See gestureRecognizer:shouldReceiveEvent:
2124 isRunningOnMac = [NSProcessInfo processInfo].iOSAppOnMac;
2125 }
2126 if (!isRunningOnMac && CGPointEqualToPoint(oldLocation, _mouseState.location) &&
2127 time > self.scrollInertiaEventStartline) {
2128 // iPadOS reports trackpad movements events with high (sub-pixel) precision. When an event
2129 // is received with the same position as the previous one, it can only be from a finger
2130 // making or breaking contact with the trackpad surface.
2131 auto packet = std::make_unique<flutter::PointerDataPacket>(2);
2132 packet->SetPointerData(/*i=*/0, pointer_data);
2133 flutter::PointerData inertia_cancel = pointer_data;
2134 inertia_cancel.device = reinterpret_cast<int64_t>(_continuousScrollingPanGestureRecognizer);
2137 inertia_cancel.view_id = self.viewIdentifier;
2138 packet->SetPointerData(/*i=*/1, inertia_cancel);
2139 [self.engine dispatchPointerDataPacket:std::move(packet)];
2140 self.scrollInertiaEventStartline = DBL_MAX;
2141 } else {
2142 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2143 packet->SetPointerData(/*i=*/0, pointer_data);
2144 [self.engine dispatchPointerDataPacket:std::move(packet)];
2145 }
2146}
2147
2148- (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2149 CGPoint translation = [recognizer translationInView:self.view];
2150 const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2151
2152 translation.x *= scale;
2153 translation.y *= scale;
2154
2155 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2156 pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2159 pointer_data.scroll_delta_x = (translation.x - _mouseState.last_translation.x);
2160 pointer_data.scroll_delta_y = -(translation.y - _mouseState.last_translation.y);
2161 pointer_data.view_id = self.viewIdentifier;
2162
2163 // The translation reported by UIPanGestureRecognizer is the total translation
2164 // generated by the pan gesture since the gesture began. We need to be able
2165 // to keep track of the last translation value in order to generate the deltaX
2166 // and deltaY coordinates for each subsequent scroll event.
2167 if (recognizer.state != UIGestureRecognizerStateEnded) {
2168 _mouseState.last_translation = translation;
2169 } else {
2170 _mouseState.last_translation = CGPointZero;
2171 }
2172
2173 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2174 packet->SetPointerData(/*i=*/0, pointer_data);
2175 [self.engine dispatchPointerDataPacket:std::move(packet)];
2176}
2177
2178- (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2179 CGPoint translation = [recognizer translationInView:self.view];
2180 const CGFloat scale = self.flutterScreenIfViewLoaded.scale;
2181
2182 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2183 pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2185 pointer_data.view_id = self.viewIdentifier;
2186 switch (recognizer.state) {
2187 case UIGestureRecognizerStateBegan:
2189 break;
2190 case UIGestureRecognizerStateChanged:
2192 pointer_data.pan_x = translation.x * scale;
2193 pointer_data.pan_y = translation.y * scale;
2194 pointer_data.pan_delta_x = 0; // Delta will be generated in pointer_data_packet_converter.cc.
2195 pointer_data.pan_delta_y = 0; // Delta will be generated in pointer_data_packet_converter.cc.
2196 pointer_data.scale = 1;
2197 break;
2198 case UIGestureRecognizerStateEnded:
2199 case UIGestureRecognizerStateCancelled:
2200 self.scrollInertiaEventStartline =
2201 [[NSProcessInfo processInfo] systemUptime] +
2202 0.1; // Time to lift fingers off trackpad (experimentally determined)
2203 // When running an iOS app on an Apple Silicon Mac, AppKit will send an event
2204 // of type UIEventTypeScroll when trackpad scroll momentum has ended. This event
2205 // is sent whether the momentum ended normally or was cancelled by a trackpad touch.
2206 // Since Flutter scrolling inertia will likely not match the system inertia, we should
2207 // only send a PointerScrollInertiaCancel event for user-initiated cancellations.
2208 // The following (curve-fitted) calculation provides a cutoff point after which any
2209 // UIEventTypeScroll event will likely be from the system instead of the user.
2210 // See https://github.com/flutter/engine/pull/34929.
2211 self.scrollInertiaEventAppKitDeadline =
2212 [[NSProcessInfo processInfo] systemUptime] +
2213 (0.1821 * log(fmax([recognizer velocityInView:self.view].x,
2214 [recognizer velocityInView:self.view].y))) -
2215 0.4825;
2217 break;
2218 default:
2219 // continuousScrollEvent: should only ever be triggered with the above phases
2220 NSAssert(NO, @"Trackpad pan event occured with unexpected phase 0x%lx",
2221 (long)recognizer.state);
2222 break;
2223 }
2224
2225 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2226 packet->SetPointerData(/*i=*/0, pointer_data);
2227 [self.engine dispatchPointerDataPacket:std::move(packet)];
2228}
2229
2230- (void)pinchEvent:(UIPinchGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2231 flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer];
2232 pointer_data.device = reinterpret_cast<int64_t>(recognizer);
2234 pointer_data.view_id = self.viewIdentifier;
2235 switch (recognizer.state) {
2236 case UIGestureRecognizerStateBegan:
2238 break;
2239 case UIGestureRecognizerStateChanged:
2241 pointer_data.scale = recognizer.scale;
2242 pointer_data.rotation = _rotationGestureRecognizer.rotation;
2243 break;
2244 case UIGestureRecognizerStateEnded:
2245 case UIGestureRecognizerStateCancelled:
2247 break;
2248 default:
2249 // pinchEvent: should only ever be triggered with the above phases
2250 NSAssert(NO, @"Trackpad pinch event occured with unexpected phase 0x%lx",
2251 (long)recognizer.state);
2252 break;
2253 }
2254
2255 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2256 packet->SetPointerData(/*i=*/0, pointer_data);
2257 [self.engine dispatchPointerDataPacket:std::move(packet)];
2258}
2259
2260#pragma mark - State Restoration
2261
2262- (void)encodeRestorableStateWithCoder:(NSCoder*)coder {
2263 NSData* restorationData = [self.engine.restorationPlugin restorationData];
2264 [coder encodeBytes:(const unsigned char*)restorationData.bytes
2265 length:restorationData.length
2266 forKey:kFlutterRestorationStateAppData];
2267 [super encodeRestorableStateWithCoder:coder];
2268}
2269
2270- (void)decodeRestorableStateWithCoder:(NSCoder*)coder {
2271 NSUInteger restorationDataLength;
2272 const unsigned char* restorationBytes = [coder decodeBytesForKey:kFlutterRestorationStateAppData
2273 returnedLength:&restorationDataLength];
2274 NSData* restorationData = [NSData dataWithBytes:restorationBytes length:restorationDataLength];
2275 [self.engine.restorationPlugin setRestorationData:restorationData];
2276}
2277
2278- (FlutterRestorationPlugin*)restorationPlugin {
2279 return self.engine.restorationPlugin;
2280}
2281
2283 return self.engine.textInputPlugin;
2284}
2285
2286#pragma mark - FlutterKeyboardInsetManagerDelegate
2287
2288- (void)updateViewportMetricsWithInset:(CGFloat)inset {
2289 _viewportMetrics.physical_view_inset_bottom = inset;
2290 [self updateViewportMetricsIfNeeded];
2291}
2292
2293- (CGFloat)physicalViewInsetBottom {
2294 return _viewportMetrics.physical_view_inset_bottom;
2295}
2296
2297- (BOOL)isPadInSlideOverOrStageManagerMode {
2298 if (self.view.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad &&
2299 self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
2300 self.view.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
2301 return YES;
2302 }
2303 return NO;
2304}
2305
2306- (CGRect)convertViewRectToScreen:(CGRect)rect {
2307 return [self.view convertRect:rect
2308 toCoordinateSpace:self.flutterScreenIfViewLoaded.coordinateSpace];
2309}
2310
2311@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