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