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