| Flutter iOS Embedder |

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