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