5 #define FML_USED_ON_EMBEDDER
12 #include "flutter/common/constants.h"
13 #include "flutter/fml/memory/weak_ptr.h"
14 #include "flutter/fml/message_loop.h"
15 #include "flutter/fml/platform/darwin/platform_version.h"
16 #include "flutter/runtime/ptrace_check.h"
17 #include "flutter/shell/common/thread_host.h"
18 #import "flutter/shell/platform/darwin/common/InternalFlutterSwiftCommon/InternalFlutterSwiftCommon.h"
39 #import "flutter/shell/platform/embedder/embedder.h"
40 #import "flutter/third_party/spring_animation/spring_animation.h"
52 @"FlutterViewControllerHideHomeIndicator";
54 @"FlutterViewControllerShowHomeIndicator";
74 @property(nonatomic, readonly) int64_t viewIdentifier;
79 @property(nonatomic, strong)
void (^flutterViewRenderedCallback)(void);
81 @property(nonatomic, strong) FlutterSplashScreenManager* splashScreenManager;
83 @property(nonatomic, assign) UIInterfaceOrientationMask orientationPreferences;
84 @property(nonatomic, assign) UIStatusBarStyle statusBarStyle;
85 @property(nonatomic, assign) BOOL initialized;
86 @property(nonatomic, assign) BOOL engineNeedsLaunch;
87 @property(nonatomic, assign) BOOL awokenFromNib;
90 @property(nonatomic, assign) BOOL isHomeIndicatorHidden;
91 @property(nonatomic, assign) BOOL isPresentingViewControllerAnimating;
94 @property(nonatomic, assign) BOOL flutterPrefersStatusBarHidden;
96 @property(nonatomic, strong) NSMutableSet<NSNumber*>* ongoingTouches;
101 @property(nonatomic, strong) UIScrollView* scrollView;
106 @property(nonatomic, assign) BOOL shouldIgnoreViewportMetricsUpdatesDuringRotation;
112 @property(nonatomic, assign) NSTimeInterval scrollInertiaEventStartline;
120 @property(nonatomic, assign) NSTimeInterval scrollInertiaEventAppKitDeadline;
128 @property(nonatomic, strong) FlutterVSyncClient* touchRateCorrectionVSyncClient;
132 @property(nonatomic, assign) CGSize sizeBeforeAutoResized;
138 @property(nonatomic, strong)
141 @property(nonatomic, strong)
142 UIPanGestureRecognizer* discreteScrollingPanGestureRecognizer
API_AVAILABLE(ios(13.4));
144 @property(nonatomic, strong)
145 UIPanGestureRecognizer* continuousScrollingPanGestureRecognizer
API_AVAILABLE(ios(13.4));
147 @property(nonatomic, strong)
150 @property(nonatomic, strong)
151 UIRotationGestureRecognizer* rotationGestureRecognizer
API_AVAILABLE(ios(13.4));
154 - (void)addInternalPlugins;
155 - (void)deregisterNotifications;
158 - (void)onFirstFrameRendered;
165 flutter::ViewportMetrics _viewportMetrics;
171 @synthesize viewOpaque = _viewOpaque;
172 @synthesize displayingFlutterUI = _displayingFlutterUI;
174 - (FlutterSplashScreenManager*)splashScreenManager {
184 @dynamic viewIdentifier;
186 #pragma mark - Manage and override all designated initializers
189 nibName:(nullable NSString*)nibName
190 bundle:(nullable NSBundle*)nibBundle {
191 FML_CHECK(
engine) <<
"initWithEngine:nibName:bundle: must be called with non-nil engine";
192 self = [
super initWithNibName:nibName bundle:nibBundle];
195 if (
engine.viewController) {
196 NSString* errorMessage =
197 [NSString stringWithFormat:
198 @"The supplied FlutterEngine %@ is already used with FlutterViewController "
199 "instance %@. One instance of the FlutterEngine can only be attached to "
200 "one FlutterViewController at a time. Set FlutterEngine.viewController to "
201 "nil before attaching it to another FlutterViewController.",
202 engine.description, engine.viewController.description];
203 [FlutterLogger logError:errorMessage];
206 _engineNeedsLaunch = NO;
207 _flutterView = [[
FlutterView alloc] initWithDelegate:_engine
208 opaque:self.isViewOpaque
209 enableWideGamut:engine.project.isWideGamutEnabled];
210 _ongoingTouches = [[NSMutableSet alloc] init];
214 [
self performCommonViewControllerInitialization];
215 [engine setViewController:self];
222 nibName:(NSString*)nibName
223 bundle:(NSBundle*)nibBundle {
224 self = [
super initWithNibName:nibName bundle:nibBundle];
228 [
self sharedSetupWithProject:project initialRoute:nil];
235 initialRoute:(NSString*)initialRoute
236 nibName:(NSString*)nibName
237 bundle:(NSBundle*)nibBundle {
238 self = [
super initWithNibName:nibName bundle:nibBundle];
242 [
self sharedSetupWithProject:project initialRoute:initialRoute];
248 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
249 return [
self initWithProject:nil nibName:nil bundle:nil];
253 self = [
super initWithCoder:aDecoder];
257 - (void)awakeFromNib {
258 [
super awakeFromNib];
259 self.awokenFromNib = YES;
261 [
self sharedSetupWithProject:nil initialRoute:nil];
265 - (instancetype)init {
266 return [
self initWithProject:nil nibName:nil bundle:nil];
270 initialRoute:(nullable NSString*)initialRoute {
273 if ([appDelegate respondsToSelector:
@selector(takeLaunchEngine)]) {
278 engine = [appDelegate takeLaunchEngine];
283 [appDelegate takeLaunchEngine];
295 allowHeadlessExecution:self.engineAllowHeadlessExecution
296 restorationEnabled:self.restorationIdentifier != nil];
304 _flutterView = [[
FlutterView alloc] initWithDelegate:_engine
306 enableWideGamut:engine.project.isWideGamutEnabled];
307 [_engine createShell:nil libraryURI:nil initialRoute:initialRoute];
313 BOOL performedCallback = [_engine performImplicitEngineCallback];
317 respondsToSelector:
@selector(pluginRegistrant)]) {
318 NSObject<FlutterPluginRegistrant>* pluginRegistrant =
320 [pluginRegistrant registerWithRegistry:self];
321 performedCallback = YES;
329 id applicationLifeCycleDelegate = ((
FlutterAppDelegate*)appDelegate).lifeCycleDelegate;
330 [applicationLifeCycleDelegate
331 sceneFallbackWillFinishLaunchingApplication:FlutterSharedApplication.application];
332 [applicationLifeCycleDelegate
333 sceneFallbackDidFinishLaunchingApplication:FlutterSharedApplication.application];
336 _engineNeedsLaunch = YES;
337 _ongoingTouches = [[NSMutableSet alloc] init];
341 [
self.splashScreenManager loadDefaultSplashScreenView];
342 [
self performCommonViewControllerInitialization];
345 - (BOOL)isViewOpaque {
349 - (void)setViewOpaque:(BOOL)value {
351 if (
self.flutterView.layer.opaque != value) {
352 self.flutterView.layer.opaque = value;
353 [
self.flutterView.layer setNeedsLayout];
357 #pragma mark - Common view controller initialization tasks
359 - (void)performCommonViewControllerInitialization {
365 _orientationPreferences = UIInterfaceOrientationMaskAll;
366 _statusBarStyle = UIStatusBarStyleDefault;
368 _accessibilityFeatures = [[FlutterAccessibilityFeatures alloc] init];
373 [
self setUpNotificationCenterObservers];
376 - (void)setUpNotificationCenterObservers {
377 NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
378 [center addObserver:self
379 selector:@selector(onOrientationPreferencesUpdated:)
380 name:@(flutter::kOrientationUpdateNotificationName)
383 [center addObserver:self
384 selector:@selector(onPreferredStatusBarStyleUpdated:)
385 name:@(flutter::kOverlayStyleUpdateNotificationName)
389 [
self setUpApplicationLifecycleNotifications:center];
391 [
self setUpSceneLifecycleNotifications:center];
394 [center addObserver:self
395 selector:@selector(keyboardWillChangeFrame:)
396 name:UIKeyboardWillChangeFrameNotification
399 [center addObserver:self
400 selector:@selector(keyboardWillShowNotification:)
401 name:UIKeyboardWillShowNotification
404 [center addObserver:self
405 selector:@selector(keyboardWillBeHidden:)
406 name:UIKeyboardWillHideNotification
409 for (NSString* notification in [
self.accessibilityFeatures observedNotificationNames]) {
410 [center addObserver:self
411 selector:@selector(onAccessibilityStatusChanged:)
416 [center addObserver:self
417 selector:@selector(onUserSettingsChanged:)
418 name:UIContentSizeCategoryDidChangeNotification
421 [center addObserver:self
422 selector:@selector(onHideHomeIndicatorNotification:)
423 name:FlutterViewControllerHideHomeIndicator
426 [center addObserver:self
427 selector:@selector(onShowHomeIndicatorNotification:)
428 name:FlutterViewControllerShowHomeIndicator
432 - (void)setUpSceneLifecycleNotifications:(NSNotificationCenter*)center API_AVAILABLE(ios(13.0)) {
433 [center addObserver:self
434 selector:@selector(sceneBecameActive:)
435 name:UISceneDidActivateNotification
438 [center addObserver:self
439 selector:@selector(sceneWillResignActive:)
440 name:UISceneWillDeactivateNotification
443 [center addObserver:self
444 selector:@selector(sceneWillDisconnect:)
445 name:UISceneDidDisconnectNotification
448 [center addObserver:self
449 selector:@selector(sceneDidEnterBackground:)
450 name:UISceneDidEnterBackgroundNotification
453 [center addObserver:self
454 selector:@selector(sceneWillEnterForeground:)
455 name:UISceneWillEnterForegroundNotification
459 - (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center {
460 [center addObserver:self
461 selector:@selector(applicationBecameActive:)
462 name:UIApplicationDidBecomeActiveNotification
465 [center addObserver:self
466 selector:@selector(applicationWillResignActive:)
467 name:UIApplicationWillResignActiveNotification
470 [center addObserver:self
471 selector:@selector(applicationWillTerminate:)
472 name:UIApplicationWillTerminateNotification
475 [center addObserver:self
476 selector:@selector(applicationDidEnterBackground:)
477 name:UIApplicationDidEnterBackgroundNotification
480 [center addObserver:self
481 selector:@selector(applicationWillEnterForeground:)
482 name:UIApplicationWillEnterForegroundNotification
486 - (void)setInitialRoute:(NSString*)route {
487 [
self.engine.navigationChannel invokeMethod:@"setInitialRoute" arguments:route];
491 [
self.engine.navigationChannel invokeMethod:@"popRoute" arguments:nil];
494 - (void)pushRoute:(NSString*)route {
495 [
self.engine.navigationChannel invokeMethod:@"pushRoute" arguments:route];
498 #pragma mark - Loading the view
500 static UIView* GetViewOrPlaceholder(UIView* existing_view) {
502 return existing_view;
505 auto placeholder = [[UIView alloc] init];
507 placeholder.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
508 placeholder.backgroundColor = UIColor.systemBackgroundColor;
509 placeholder.autoresizesSubviews = YES;
514 if (flutter::GetTracingResult() == flutter::TracingResult::kDisabled) {
515 auto messageLabel = [[UILabel alloc] init];
516 messageLabel.numberOfLines = 0u;
517 messageLabel.textAlignment = NSTextAlignmentCenter;
518 messageLabel.autoresizingMask =
519 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
521 @"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling, "
522 @"IDEs with Flutter plugins or from Xcode.\n\nAlternatively, build in profile or release "
523 @"modes to enable launching from the home screen.";
524 [placeholder addSubview:messageLabel];
531 self.view = GetViewOrPlaceholder(
self.flutterView);
532 self.view.multipleTouchEnabled = YES;
533 self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
535 [
self installSplashScreenViewIfNecessary];
538 UIScrollView* scrollView = [[UIScrollView alloc] init];
539 scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
541 scrollView.backgroundColor = UIColor.whiteColor;
542 scrollView.delegate =
self;
548 [
self.view addSubview:scrollView];
549 self.scrollView = scrollView;
552 - (
flutter::PointerData)generatePointerDataForFake {
553 flutter::PointerData pointer_data;
554 pointer_data.Clear();
555 pointer_data.kind = flutter::PointerData::DeviceKind::kTouch;
563 - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
567 if (
self.isViewLoaded) {
569 [
self.engine onStatusBarTap];
574 #pragma mark - Managing launch views
576 - (void)installSplashScreenViewIfNecessary {
582 if (
self.splashScreenView && (
self.isBeingPresented ||
self.isMovingToParentViewController)) {
584 [
self.splashScreenView removeFromSuperview];
585 self.splashScreenView = nil;
590 [
self.splashScreenManager installSplashScreenViewAsSubviewOf:self.view];
593 + (BOOL)automaticallyNotifiesObserversOfDisplayingFlutterUI {
597 - (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI {
598 if (_displayingFlutterUI != displayingFlutterUI) {
599 if (displayingFlutterUI == YES) {
600 if (!
self.viewIfLoaded.window) {
604 [
self willChangeValueForKey:@"displayingFlutterUI"];
605 _displayingFlutterUI = displayingFlutterUI;
606 [
self didChangeValueForKey:@"displayingFlutterUI"];
610 - (void)callViewRenderedCallback {
611 self.displayingFlutterUI = YES;
612 if (
self.flutterViewRenderedCallback) {
613 self.flutterViewRenderedCallback();
614 self.flutterViewRenderedCallback = nil;
618 - (void)onFirstFrameRendered {
619 if (
self.splashScreenView) {
621 [
self.splashScreenManager removeSplashScreenWithCompletion:^{
622 [weakSelf callViewRenderedCallback];
625 [
self callViewRenderedCallback];
629 - (void)installFirstFrameCallback {
634 [
self.engine installFirstFrameCallback:^{
635 [weakSelf onFirstFrameRendered];
639 #pragma mark - Properties
641 - (int64_t)viewIdentifier {
644 return flutter::kFlutterImplicitViewId;
647 - (BOOL)loadDefaultSplashScreenView {
648 return [
self.splashScreenManager loadDefaultSplashScreenView];
651 - (UIView*)splashScreenView {
655 - (void)setSplashScreenView:(UIView*)view {
656 self.splashScreenManager.splashScreenView = view;
659 - (void)setFlutterViewDidRenderCallback:(
void (^)(
void))callback {
660 _flutterViewRenderedCallback = callback;
663 - (UISceneActivationState)activationState {
664 return self.flutterWindowSceneIfViewLoaded.activationState;
667 - (BOOL)stateIsActive {
670 BOOL isActive = flutterApplication
671 ? [
self isApplicationStateMatching:UIApplicationStateActive
672 withApplication:flutterApplication]
673 : [
self isSceneStateMatching:UISceneActivationStateForegroundActive];
677 - (BOOL)stateIsBackground {
680 return flutterApplication ? [
self isApplicationStateMatching:UIApplicationStateBackground
681 withApplication:flutterApplication]
682 : [
self isSceneStateMatching:UISceneActivationStateBackground];
685 - (BOOL)shouldHandleSceneNotification:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
686 if (notification.object == nil) {
689 UIWindowScene* scene =
self.flutterWindowSceneIfViewLoaded;
693 return notification.object == scene;
696 - (BOOL)isApplicationStateMatching:(UIApplicationState)match
697 withApplication:(UIApplication*)application {
698 switch (application.applicationState) {
699 case UIApplicationStateActive:
700 case UIApplicationStateInactive:
701 case UIApplicationStateBackground:
702 return application.applicationState == match;
706 - (BOOL)isSceneStateMatching:(UISceneActivationState)match API_AVAILABLE(ios(13.0)) {
707 switch (
self.activationState) {
708 case UISceneActivationStateForegroundActive:
709 case UISceneActivationStateUnattached:
710 case UISceneActivationStateForegroundInactive:
711 case UISceneActivationStateBackground:
712 return self.activationState == match;
716 #pragma mark - Surface creation and teardown updates
718 - (void)surfaceUpdated:(BOOL)appeared {
726 [
self installFirstFrameCallback];
727 self.platformViewsController.flutterView =
self.flutterView;
728 self.platformViewsController.flutterViewController =
self;
729 [
self.engine notifyViewCreated];
731 self.displayingFlutterUI = NO;
732 [
self.engine notifyViewDestroyed];
733 self.platformViewsController.flutterView = nil;
734 self.platformViewsController.flutterViewController = nil;
738 #pragma mark - UIViewController lifecycle notifications
740 - (void)viewDidLoad {
741 TRACE_EVENT0(
"flutter",
"viewDidLoad");
743 if (
self.
engine &&
self.engineNeedsLaunch) {
744 [
self.engine launchEngine:nil libraryURI:nil entrypointArgs:nil];
745 [
self.engine setViewController:self];
746 self.engineNeedsLaunch = NO;
747 }
else if (
self.
engine.viewController ==
self) {
748 [
self.engine attachView];
752 [
self addInternalPlugins];
755 [
self createTouchRateCorrectionVSyncClientIfNeeded];
757 if (@available(iOS 13.4, *)) {
758 _hoverGestureRecognizer =
759 [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)];
760 _hoverGestureRecognizer.delegate =
self;
761 [
self.flutterView addGestureRecognizer:_hoverGestureRecognizer];
763 _discreteScrollingPanGestureRecognizer =
764 [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(discreteScrollEvent:)];
765 _discreteScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
770 _discreteScrollingPanGestureRecognizer.allowedTouchTypes = @[];
771 _discreteScrollingPanGestureRecognizer.delegate =
self;
772 [
self.flutterView addGestureRecognizer:_discreteScrollingPanGestureRecognizer];
773 _continuousScrollingPanGestureRecognizer =
774 [[UIPanGestureRecognizer alloc] initWithTarget:self
775 action:@selector(continuousScrollEvent:)];
776 _continuousScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
777 _continuousScrollingPanGestureRecognizer.allowedTouchTypes = @[];
778 _continuousScrollingPanGestureRecognizer.delegate =
self;
779 [
self.flutterView addGestureRecognizer:_continuousScrollingPanGestureRecognizer];
780 _pinchGestureRecognizer =
781 [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchEvent:)];
782 _pinchGestureRecognizer.allowedTouchTypes = @[];
783 _pinchGestureRecognizer.delegate =
self;
784 [
self.flutterView addGestureRecognizer:_pinchGestureRecognizer];
785 _rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] init];
786 _rotationGestureRecognizer.allowedTouchTypes = @[];
787 _rotationGestureRecognizer.delegate =
self;
788 [
self.flutterView addGestureRecognizer:_rotationGestureRecognizer];
794 - (void)addInternalPlugins {
798 ^(
const FlutterKeyEvent& event, FlutterKeyEventCallback callback,
void* userData) {
799 [weakSelf.engine sendKeyEvent:event callback:callback userData:userData];
801 [
self.keyboardManager
805 [
self.keyboardManager addPrimaryResponder:responder];
808 [
self.keyboardManager addSecondaryResponder:textInputPlugin];
810 if (
self.
engine.viewController ==
self) {
815 - (void)removeInternalPlugins {
816 self.keyboardManager = nil;
819 - (void)viewWillAppear:(BOOL)animated {
820 TRACE_EVENT0(
"flutter",
"viewWillAppear");
821 if (
self.
engine.viewController ==
self) {
823 [
self onUserSettingsChanged:nil];
827 if (_viewportMetrics.physical_width) {
828 [
self surfaceUpdated:YES];
830 [
self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"];
831 [
self.engine.restorationPlugin markRestorationComplete];
834 [
super viewWillAppear:animated];
837 - (void)viewDidAppear:(BOOL)animated {
838 TRACE_EVENT0(
"flutter",
"viewDidAppear");
839 if (
self.
engine.viewController ==
self) {
840 [
self onUserSettingsChanged:nil];
841 [
self onAccessibilityStatusChanged:nil];
843 if (
self.stateIsActive) {
844 [
self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.resumed"];
847 [
super viewDidAppear:animated];
850 - (void)viewWillDisappear:(BOOL)animated {
851 TRACE_EVENT0(
"flutter",
"viewWillDisappear");
852 if (
self.
engine.viewController ==
self) {
853 [
self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"];
855 [
super viewWillDisappear:animated];
858 - (void)viewDidDisappear:(BOOL)animated {
859 TRACE_EVENT0(
"flutter",
"viewDidDisappear");
860 if (
self.
engine.viewController ==
self) {
861 [
self.keyboardInsetManager hideKeyboardImmediately];
862 [
self surfaceUpdated:NO];
863 [
self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.paused"];
864 [
self flushOngoingTouches];
865 [
self.engine notifyLowMemory];
868 [
super viewDidDisappear:animated];
871 - (void)viewWillTransitionToSize:(CGSize)size
872 withTransitionCoordinator:(
id<UIViewControllerTransitionCoordinator>)coordinator {
873 [
super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
883 NSTimeInterval transitionDuration = coordinator.transitionDuration;
885 if (transitionDuration == 0) {
890 _shouldIgnoreViewportMetricsUpdatesDuringRotation = YES;
891 dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
892 static_cast<int64_t
>(transitionDuration / 2.0 * NSEC_PER_SEC)),
893 dispatch_get_main_queue(), ^{
901 strongSelf.shouldIgnoreViewportMetricsUpdatesDuringRotation = NO;
902 [strongSelf updateViewportMetricsIfNeeded];
906 - (void)flushOngoingTouches {
907 if (
self.
engine &&
self.ongoingTouches.count > 0) {
908 auto packet = std::make_unique<flutter::PointerDataPacket>(
self.ongoingTouches.count);
909 size_t pointer_index = 0;
912 for (NSNumber* device in
self.ongoingTouches) {
914 flutter::PointerData pointer_data = [
self generatePointerDataForFake];
916 pointer_data.change = flutter::PointerData::Change::kCancel;
917 pointer_data.device = device.longLongValue;
918 pointer_data.pointer_identifier = 0;
919 pointer_data.view_id =
self.viewIdentifier;
922 pointer_data.physical_x = 0;
923 pointer_data.physical_y = 0;
924 pointer_data.physical_delta_x = 0.0;
925 pointer_data.physical_delta_y = 0.0;
926 pointer_data.pressure = 1.0;
927 pointer_data.pressure_max = 1.0;
929 packet->SetPointerData(pointer_index++, pointer_data);
932 [
self.ongoingTouches removeAllObjects];
933 [
self.engine dispatchPointerDataPacket:std::move(packet)];
937 - (void)deregisterNotifications {
938 [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerWillDealloc
941 [[NSNotificationCenter defaultCenter] removeObserver:self];
947 [
self removeInternalPlugins];
948 [
self deregisterNotifications];
950 [
self.keyboardInsetManager invalidate];
951 [
self invalidateTouchRateCorrectionVSyncClient];
955 _scrollView.delegate = nil;
956 _hoverGestureRecognizer.delegate = nil;
957 _discreteScrollingPanGestureRecognizer.delegate = nil;
958 _continuousScrollingPanGestureRecognizer.delegate = nil;
959 _pinchGestureRecognizer.delegate = nil;
960 _rotationGestureRecognizer.delegate = nil;
963 #pragma mark - Application lifecycle notifications
965 - (void)applicationBecameActive:(NSNotification*)notification {
966 TRACE_EVENT0(
"flutter",
"applicationBecameActive");
967 [
self appOrSceneBecameActive];
970 - (void)applicationWillResignActive:(NSNotification*)notification {
971 TRACE_EVENT0(
"flutter",
"applicationWillResignActive");
972 [
self appOrSceneWillResignActive];
975 - (void)applicationWillTerminate:(NSNotification*)notification {
976 [
self appOrSceneWillTerminate];
979 - (void)applicationDidEnterBackground:(NSNotification*)notification {
980 TRACE_EVENT0(
"flutter",
"applicationDidEnterBackground");
981 [
self appOrSceneDidEnterBackground];
984 - (void)applicationWillEnterForeground:(NSNotification*)notification {
985 TRACE_EVENT0(
"flutter",
"applicationWillEnterForeground");
986 [
self appOrSceneWillEnterForeground];
989 #pragma mark - Scene lifecycle notifications
991 - (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
992 if (![
self shouldHandleSceneNotification:notification]) {
995 TRACE_EVENT0(
"flutter",
"sceneBecameActive");
996 [
self appOrSceneBecameActive];
999 - (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1000 if (![
self shouldHandleSceneNotification:notification]) {
1003 TRACE_EVENT0(
"flutter",
"sceneWillResignActive");
1004 [
self appOrSceneWillResignActive];
1007 - (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1008 if (![
self shouldHandleSceneNotification:notification]) {
1011 [
self appOrSceneWillTerminate];
1014 - (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1015 if (![
self shouldHandleSceneNotification:notification]) {
1018 TRACE_EVENT0(
"flutter",
"sceneDidEnterBackground");
1019 [
self appOrSceneDidEnterBackground];
1022 - (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)) {
1023 if (![
self shouldHandleSceneNotification:notification]) {
1026 TRACE_EVENT0(
"flutter",
"sceneWillEnterForeground");
1027 [
self appOrSceneWillEnterForeground];
1030 #pragma mark - Lifecycle shared
1032 - (void)appOrSceneBecameActive {
1033 self.keyboardInsetManager.isKeyboardInOrTransitioningFromBackground = NO;
1034 if (_viewportMetrics.physical_width) {
1035 [
self surfaceUpdated:YES];
1037 [
self performSelector:@selector(goToApplicationLifecycle:)
1038 withObject:@"AppLifecycleState.resumed"
1042 - (void)appOrSceneWillResignActive {
1043 [NSObject cancelPreviousPerformRequestsWithTarget:self
1044 selector:@selector(goToApplicationLifecycle:)
1045 object:@"AppLifecycleState.resumed"];
1046 [
self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
1049 - (void)appOrSceneWillTerminate {
1050 [
self goToApplicationLifecycle:@"AppLifecycleState.detached"];
1051 [
self.engine destroyContext];
1054 - (void)appOrSceneDidEnterBackground {
1055 self.keyboardInsetManager.isKeyboardInOrTransitioningFromBackground = YES;
1056 [
self surfaceUpdated:NO];
1057 [
self goToApplicationLifecycle:@"AppLifecycleState.paused"];
1060 - (void)appOrSceneWillEnterForeground {
1061 [
self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
1065 - (void)goToApplicationLifecycle:(nonnull NSString*)state {
1068 if (
self.viewIfLoaded.window) {
1069 [
self.engine.lifecycleChannel sendMessage:state];
1073 #pragma mark - Touch event handling
1075 static flutter::PointerData::Change PointerDataChangeFromUITouchPhase(UITouchPhase phase) {
1077 case UITouchPhaseBegan:
1078 return flutter::PointerData::Change::kDown;
1079 case UITouchPhaseMoved:
1080 case UITouchPhaseStationary:
1083 return flutter::PointerData::Change::kMove;
1084 case UITouchPhaseEnded:
1085 return flutter::PointerData::Change::kUp;
1086 case UITouchPhaseCancelled:
1087 return flutter::PointerData::Change::kCancel;
1090 FML_DLOG(INFO) <<
"Unhandled touch phase: " << phase;
1094 return flutter::PointerData::Change::kCancel;
1097 static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) {
1098 switch (touch.type) {
1099 case UITouchTypeDirect:
1100 case UITouchTypeIndirect:
1101 return flutter::PointerData::DeviceKind::kTouch;
1102 case UITouchTypeStylus:
1103 return flutter::PointerData::DeviceKind::kStylus;
1104 case UITouchTypeIndirectPointer:
1105 return flutter::PointerData::DeviceKind::kMouse;
1107 FML_DLOG(INFO) <<
"Unhandled touch type: " << touch.type;
1111 return flutter::PointerData::DeviceKind::kTouch;
1118 - (void)dispatchTouches:(NSSet*)touches
1119 pointerDataChangeOverride:(
flutter::PointerData::Change*)overridden_change
1120 event:(UIEvent*)event {
1145 NSUInteger touches_to_remove_count = 0;
1146 for (UITouch* touch in touches) {
1147 if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
1148 touches_to_remove_count++;
1153 [
self triggerTouchRateCorrectionIfNeeded:touches];
1155 const CGFloat scale =
self.flutterScreenIfViewLoaded.scale;
1157 std::make_unique<flutter::PointerDataPacket>(touches.count + touches_to_remove_count);
1159 size_t pointer_index = 0;
1161 for (UITouch* touch in touches) {
1162 CGPoint windowCoordinates = [touch locationInView:self.view];
1164 flutter::PointerData pointer_data;
1165 pointer_data.Clear();
1170 pointer_data.change = overridden_change !=
nullptr
1171 ? *overridden_change
1172 : PointerDataChangeFromUITouchPhase(touch.phase);
1174 pointer_data.kind = DeviceKindFromTouchType(touch);
1176 pointer_data.device =
reinterpret_cast<int64_t
>(touch);
1178 pointer_data.view_id =
self.viewIdentifier;
1181 pointer_data.pointer_identifier = 0;
1183 pointer_data.physical_x = windowCoordinates.x * scale;
1184 pointer_data.physical_y = windowCoordinates.y * scale;
1187 pointer_data.physical_delta_x = 0.0;
1188 pointer_data.physical_delta_y = 0.0;
1190 NSNumber* deviceKey = [NSNumber numberWithLongLong:pointer_data.device];
1193 switch (pointer_data.change) {
1194 case flutter::PointerData::Change::kDown:
1195 [
self.ongoingTouches addObject:deviceKey];
1197 case flutter::PointerData::Change::kCancel:
1198 case flutter::PointerData::Change::kUp:
1199 [
self.ongoingTouches removeObject:deviceKey];
1201 case flutter::PointerData::Change::kHover:
1202 case flutter::PointerData::Change::kMove:
1205 case flutter::PointerData::Change::kAdd:
1206 case flutter::PointerData::Change::kRemove:
1209 case flutter::PointerData::Change::kPanZoomStart:
1210 case flutter::PointerData::Change::kPanZoomUpdate:
1211 case flutter::PointerData::Change::kPanZoomEnd:
1217 pointer_data.pressure = touch.force;
1218 pointer_data.pressure_max = touch.maximumPossibleForce;
1219 pointer_data.radius_major = touch.majorRadius;
1220 pointer_data.radius_min = touch.majorRadius - touch.majorRadiusTolerance;
1221 pointer_data.radius_max = touch.majorRadius + touch.majorRadiusTolerance;
1236 pointer_data.tilt = M_PI_2 - touch.altitudeAngle;
1256 pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2;
1258 if (@available(iOS 13.4, *)) {
1259 if (event !=
nullptr) {
1260 pointer_data.buttons = (((
event.buttonMask & UIEventButtonMaskPrimary) > 0)
1261 ? flutter::PointerButtonMouse::kPointerButtonMousePrimary
1263 (((event.buttonMask & UIEventButtonMaskSecondary) > 0)
1264 ? flutter::PointerButtonMouse::kPointerButtonMouseSecondary
1269 packet->SetPointerData(pointer_index++, pointer_data);
1271 if (touch.phase == UITouchPhaseEnded || touch.phase == UITouchPhaseCancelled) {
1272 flutter::PointerData remove_pointer_data = pointer_data;
1273 remove_pointer_data.change = flutter::PointerData::Change::kRemove;
1274 packet->SetPointerData(pointer_index++, remove_pointer_data);
1278 [
self.engine dispatchPointerDataPacket:std::move(packet)];
1281 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
1282 [
self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1285 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
1286 [
self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1289 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
1290 [
self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1293 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1294 [
self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1297 - (void)forceTouchesCancelled:(NSSet*)touches {
1298 flutter::PointerData::Change cancel = flutter::PointerData::Change::kCancel;
1299 [
self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
1302 - (BOOL)platformViewShouldAcceptTouchAtTouchBeganLocation:(CGPoint)location {
1303 flutter::PointData point{location.x, location.y};
1304 return [
self.engine platformViewShouldAcceptTouchAtTouchBeganLocation:point
1305 viewId:self.viewIdentifier];
1308 #pragma mark - Touch events rate correction
1310 - (void)createTouchRateCorrectionVSyncClientIfNeeded {
1311 if (_touchRateCorrectionVSyncClient != nil) {
1315 double displayRefreshRate = FlutterDisplayLinkManager.displayRefreshRate;
1316 const double epsilon = 0.1;
1317 if (displayRefreshRate < 60.0 + epsilon) {
1325 void (^callback)(CFTimeInterval, CFTimeInterval) =
1326 ^(CFTimeInterval startTime, CFTimeInterval targetTime) {
1329 _touchRateCorrectionVSyncClient = [[FlutterVSyncClient alloc]
1330 initWithTaskRunner:self.engine.platformTaskRunner
1331 isVariableRefreshRateEnabled:FlutterDisplayLinkManager.maxRefreshRateEnabledOnIPhone
1332 maxRefreshRate:FlutterDisplayLinkManager.displayRefreshRate
1334 _touchRateCorrectionVSyncClient.allowPauseAfterVsync = NO;
1337 - (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches {
1338 if (_touchRateCorrectionVSyncClient == nil) {
1346 BOOL isUserInteracting = NO;
1347 for (UITouch* touch in touches) {
1348 if (touch.phase == UITouchPhaseBegan || touch.phase == UITouchPhaseMoved) {
1349 isUserInteracting = YES;
1354 if (isUserInteracting &&
self.
engine.viewController ==
self) {
1355 [_touchRateCorrectionVSyncClient await];
1357 [_touchRateCorrectionVSyncClient pause];
1361 - (void)invalidateTouchRateCorrectionVSyncClient {
1362 [_touchRateCorrectionVSyncClient invalidate];
1363 _touchRateCorrectionVSyncClient = nil;
1366 #pragma mark - Handle view resizing
1368 - (void)updateViewportMetricsIfNeeded {
1369 if (_shouldIgnoreViewportMetricsUpdatesDuringRotation) {
1372 if (
self.
engine.viewController ==
self) {
1373 [
self.engine updateViewportMetrics:_viewportMetrics];
1377 - (void)viewDidLayoutSubviews {
1378 CGRect viewBounds =
self.view.bounds;
1379 CGFloat scale =
self.flutterScreenIfViewLoaded.scale;
1382 self.scrollView.frame = CGRectMake(0.0, 0.0, viewBounds.size.width, 0.0);
1386 bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
1387 _viewportMetrics.device_pixel_ratio = scale;
1388 [
self setViewportMetricsSize];
1389 [
self checkAndUpdateAutoResizeConstraints];
1390 [
self setViewportMetricsPaddings];
1391 [
self updateViewportMetricsIfNeeded];
1398 if (firstViewBoundsUpdate &&
self.stateIsActive &&
self.
engine) {
1399 [
self surfaceUpdated:YES];
1400 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
1401 NSTimeInterval timeout = 0.2;
1403 NSTimeInterval timeout = 0.1;
1406 waitForFirstFrameSync:timeout
1407 callback:^(BOOL didTimeout) {
1409 [FlutterLogger logInfo:@"Timeout waiting for the first frame to render. "
1410 "This may happen in unoptimized builds. If this is"
1411 "a release build, you should load a less complex "
1412 "frame to avoid the timeout."];
1418 - (BOOL)isAutoResizable {
1419 return self.flutterView.autoResizable;
1422 - (void)setAutoResizable:(BOOL)value {
1423 self.flutterView.autoResizable = value;
1424 self.flutterView.contentMode = UIViewContentModeCenter;
1427 - (void)checkAndUpdateAutoResizeConstraints {
1428 if (!
self.isAutoResizable) {
1432 [
self updateAutoResizeConstraints];
1456 - (void)updateAutoResizeConstraints {
1457 BOOL hasBeenAutoResized = NO;
1458 for (NSLayoutConstraint* constraint in
self.view.constraints) {
1460 hasBeenAutoResized = YES;
1464 if (!hasBeenAutoResized) {
1465 self.sizeBeforeAutoResized =
self.view.frame.size;
1468 CGFloat maxWidth =
self.sizeBeforeAutoResized.width;
1469 CGFloat maxHeight =
self.sizeBeforeAutoResized.height;
1470 CGFloat minWidth =
self.sizeBeforeAutoResized.width;
1471 CGFloat minHeight =
self.sizeBeforeAutoResized.height;
1475 if (maxWidth == 0) {
1476 maxWidth = CGFLOAT_MAX;
1479 @"Warning: The outermost widget in the autoresizable Flutter view is unsized or has "
1480 @"ambiguous dimensions, causing the host native view's width to be 0. The autoresizing "
1481 @"logic is setting the viewport constraint to unbounded DBL_MAX to prevent "
1482 @"rendering failure. Please ensure your top-level Flutter widget has explicit "
1483 @"constraints (e.g., using SizedBox or Container)."];
1485 if (maxHeight == 0) {
1486 maxHeight = CGFLOAT_MAX;
1489 @"Warning: The outermost widget in the autoresizable Flutter view is unsized or has "
1490 @"ambiguous dimensions, causing the host native view's width to be 0. The autoresizing "
1491 @"logic is setting the viewport constraint to unbounded DBL_MAX to prevent "
1492 @"rendering failure. Please ensure your top-level Flutter widget has explicit "
1493 @"constraints (e.g., using SizedBox or Container)."];
1495 _viewportMetrics.physical_min_width_constraint = minWidth * _viewportMetrics.device_pixel_ratio;
1496 _viewportMetrics.physical_max_width_constraint = maxWidth * _viewportMetrics.device_pixel_ratio;
1497 _viewportMetrics.physical_min_height_constraint = minHeight * _viewportMetrics.device_pixel_ratio;
1498 _viewportMetrics.physical_max_height_constraint = maxHeight * _viewportMetrics.device_pixel_ratio;
1501 - (void)viewSafeAreaInsetsDidChange {
1502 [
self setViewportMetricsPaddings];
1503 [
self updateViewportMetricsIfNeeded];
1504 [
super viewSafeAreaInsetsDidChange];
1508 - (void)setViewportMetricsSize {
1509 UIScreen* screen =
self.flutterScreenIfViewLoaded;
1514 CGFloat scale = screen.scale;
1515 _viewportMetrics.physical_width =
self.view.bounds.size.width * scale;
1516 _viewportMetrics.physical_height =
self.view.bounds.size.height * scale;
1518 _viewportMetrics.physical_min_width_constraint = _viewportMetrics.physical_width;
1519 _viewportMetrics.physical_max_width_constraint = _viewportMetrics.physical_width;
1520 _viewportMetrics.physical_min_height_constraint = _viewportMetrics.physical_height;
1521 _viewportMetrics.physical_max_height_constraint = _viewportMetrics.physical_height;
1527 - (void)setViewportMetricsPaddings {
1528 UIScreen* screen =
self.flutterScreenIfViewLoaded;
1533 CGFloat scale = screen.scale;
1534 _viewportMetrics.physical_padding_top =
self.view.safeAreaInsets.top * scale;
1535 _viewportMetrics.physical_padding_left =
self.view.safeAreaInsets.left * scale;
1536 _viewportMetrics.physical_padding_right =
self.view.safeAreaInsets.right * scale;
1537 _viewportMetrics.physical_padding_bottom =
self.view.safeAreaInsets.bottom * scale;
1540 #pragma mark - Keyboard events
1542 - (void)keyboardWillShowNotification:(NSNotification*)notification {
1547 [
self.keyboardInsetManager handleKeyboardNotification:notification];
1550 - (void)keyboardWillChangeFrame:(NSNotification*)notification {
1555 [
self.keyboardInsetManager handleKeyboardNotification:notification];
1558 - (void)keyboardWillBeHidden:(NSNotification*)notification {
1562 [
self.keyboardInsetManager handleKeyboardNotification:notification];
1565 - (void)handlePressEvent:(FlutterUIPressProxy*)press
1566 nextAction:(
void (^)())next API_AVAILABLE(ios(13.4)) {
1567 if (@available(iOS 13.4, *)) {
1572 [
self.keyboardManager handlePress:press nextAction:next];
1588 - (void)superPressesBegan:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1589 [
super pressesBegan:presses withEvent:event];
1592 - (void)superPressesChanged:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1593 [
super pressesChanged:presses withEvent:event];
1596 - (void)superPressesEnded:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1597 [
super pressesEnded:presses withEvent:event];
1600 - (void)superPressesCancelled:(NSSet<UIPress*>*)presses withEvent:(UIPressesEvent*)event {
1601 [
super pressesCancelled:presses withEvent:event];
1609 - (void)pressesBegan:(NSSet<UIPress*>*)presses
1610 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1611 if (@available(iOS 13.4, *)) {
1613 for (UIPress* press in presses) {
1614 [
self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1616 [weakSelf superPressesBegan:[NSSet setWithObject:press] withEvent:event];
1620 [
super pressesBegan:presses withEvent:event];
1624 - (void)pressesChanged:(NSSet<UIPress*>*)presses
1625 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1626 if (@available(iOS 13.4, *)) {
1628 for (UIPress* press in presses) {
1629 [
self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1631 [weakSelf superPressesChanged:[NSSet setWithObject:press] withEvent:event];
1635 [
super pressesChanged:presses withEvent:event];
1639 - (void)pressesEnded:(NSSet<UIPress*>*)presses
1640 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1641 if (@available(iOS 13.4, *)) {
1643 for (UIPress* press in presses) {
1644 [
self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1646 [weakSelf superPressesEnded:[NSSet setWithObject:press] withEvent:event];
1650 [
super pressesEnded:presses withEvent:event];
1654 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
1655 withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1656 if (@available(iOS 13.4, *)) {
1658 for (UIPress* press in presses) {
1659 [
self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press event:event]
1661 [weakSelf superPressesCancelled:[NSSet setWithObject:press] withEvent:event];
1665 [
super pressesCancelled:presses withEvent:event];
1669 #pragma mark - Orientation updates
1671 - (void)onOrientationPreferencesUpdated:(NSNotification*)notification {
1674 dispatch_async(dispatch_get_main_queue(), ^{
1675 NSDictionary* info = notification.userInfo;
1676 NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)];
1677 if (update == nil) {
1680 [weakSelf performOrientationUpdate:update.unsignedIntegerValue];
1684 - (void)requestGeometryUpdateForWindowScenes:(NSSet<UIScene*>*)windowScenes
1685 API_AVAILABLE(ios(16.0)) {
1686 for (UIScene* windowScene in windowScenes) {
1687 FML_DCHECK([windowScene isKindOfClass:[UIWindowScene
class]]);
1688 UIWindowSceneGeometryPreferencesIOS* preference = [[UIWindowSceneGeometryPreferencesIOS alloc]
1689 initWithInterfaceOrientations:self.orientationPreferences];
1690 [(UIWindowScene*)windowScene
1691 requestGeometryUpdateWithPreferences:preference
1692 errorHandler:^(NSError* error) {
1693 os_log_error(OS_LOG_DEFAULT,
1694 "Failed to change device orientation: %@", error);
1696 [
self setNeedsUpdateOfSupportedInterfaceOrientations];
1700 - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
1701 if (new_preferences !=
self.orientationPreferences) {
1702 self.orientationPreferences = new_preferences;
1704 if (@available(iOS 16.0, *)) {
1706 NSSet<UIScene*>* scenes = [NSSet set];
1707 if (flutterApplication) {
1708 scenes = [flutterApplication.connectedScenes
1709 filteredSetUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
1710 id scene, NSDictionary* bindings) {
1711 return [scene isKindOfClass:[UIWindowScene class]];
1713 }
else if (
self.flutterWindowSceneIfViewLoaded) {
1714 scenes = [NSSet setWithObject:self.flutterWindowSceneIfViewLoaded];
1716 [
self requestGeometryUpdateForWindowScenes:scenes];
1718 UIInterfaceOrientationMask currentInterfaceOrientation = 0;
1719 UIWindowScene* windowScene =
self.flutterWindowSceneIfViewLoaded;
1723 @"Accessing the interface orientation when the window scene is unavailable."];
1726 currentInterfaceOrientation = 1 << windowScene.interfaceOrientation;
1727 if (!(
self.orientationPreferences & currentInterfaceOrientation)) {
1728 [UIViewController attemptRotationToDeviceOrientation];
1730 if (
self.orientationPreferences & UIInterfaceOrientationMaskPortrait) {
1734 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait)
1735 forKey:@"orientation"];
1736 }
else if (
self.orientationPreferences & UIInterfaceOrientationMaskPortraitUpsideDown) {
1737 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortraitUpsideDown)
1738 forKey:@"orientation"];
1739 }
else if (
self.orientationPreferences & UIInterfaceOrientationMaskLandscapeLeft) {
1740 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeLeft)
1741 forKey:@"orientation"];
1742 }
else if (
self.orientationPreferences & UIInterfaceOrientationMaskLandscapeRight) {
1743 [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeRight)
1744 forKey:@"orientation"];
1751 - (void)onHideHomeIndicatorNotification:(NSNotification*)notification {
1752 self.isHomeIndicatorHidden = YES;
1755 - (void)onShowHomeIndicatorNotification:(NSNotification*)notification {
1756 self.isHomeIndicatorHidden = NO;
1759 - (void)setIsHomeIndicatorHidden:(BOOL)hideHomeIndicator {
1760 if (hideHomeIndicator != _isHomeIndicatorHidden) {
1761 _isHomeIndicatorHidden = hideHomeIndicator;
1762 [
self setNeedsUpdateOfHomeIndicatorAutoHidden];
1766 - (BOOL)prefersHomeIndicatorAutoHidden {
1767 return self.isHomeIndicatorHidden;
1770 - (BOOL)shouldAutorotate {
1774 - (NSUInteger)supportedInterfaceOrientations {
1775 return self.orientationPreferences;
1778 #pragma mark - Accessibility
1780 - (void)onAccessibilityStatusChanged:(NSNotification*)notification {
1785 int32_t flags = [
self.accessibilityFeatures flags];
1786 #if TARGET_OS_SIMULATOR
1792 _isVoiceOverRunning = [
self.accessibilityFeatures isVoiceOverRunning];
1793 enabled = _isVoiceOverRunning || [
self.accessibilityFeatures isSwitchControlRunning] ||
1794 [
self.accessibilityFeatures isSpeakScreenEnabled];
1796 [
self.engine enableSemantics:enabled withFlags:flags];
1799 - (BOOL)accessibilityPerformEscape {
1801 if (navigationChannel) {
1808 #pragma mark - Set user settings
1810 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
1811 [
super traitCollectionDidChange:previousTraitCollection];
1812 [
self onUserSettingsChanged:nil];
1816 if (
self.isAutoResizable) {
1817 [
self.flutterView resetIntrinsicContentSize];
1821 - (void)onUserSettingsChanged:(NSNotification*)notification {
1822 [
self.engine.settingsChannel sendMessage:@{
1823 @"textScaleFactor" : @(
self.textScaleFactor),
1825 @"platformBrightness" :
self.brightnessMode,
1826 @"platformContrast" : self.contrastMode,
1827 @"nativeSpellCheckServiceDefined" : @YES,
1828 @"supportsShowingSystemContextMenu" : @(self.supportsShowingSystemContextMenu)
1832 - (CGFloat)textScaleFactor {
1834 if (flutterApplication == nil) {
1835 [FlutterLogger logWarning:@"Dynamic content size update is not supported in app extension."];
1839 UIContentSizeCategory category = flutterApplication.preferredContentSizeCategory;
1845 const CGFloat xs = 14;
1846 const CGFloat s = 15;
1847 const CGFloat m = 16;
1848 const CGFloat l = 17;
1849 const CGFloat xl = 19;
1850 const CGFloat xxl = 21;
1851 const CGFloat xxxl = 23;
1854 const CGFloat ax1 = 28;
1855 const CGFloat ax2 = 33;
1856 const CGFloat ax3 = 40;
1857 const CGFloat ax4 = 47;
1858 const CGFloat ax5 = 53;
1862 if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) {
1864 }
else if ([category isEqualToString:UIContentSizeCategorySmall]) {
1866 }
else if ([category isEqualToString:UIContentSizeCategoryMedium]) {
1868 }
else if ([category isEqualToString:UIContentSizeCategoryLarge]) {
1870 }
else if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) {
1872 }
else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) {
1874 }
else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) {
1876 }
else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) {
1878 }
else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) {
1880 }
else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) {
1882 }
else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) {
1884 }
else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) {
1891 - (BOOL)supportsShowingSystemContextMenu {
1892 if (@available(iOS 16.0, *)) {
1902 - (NSString*)brightnessMode {
1903 UIUserInterfaceStyle style =
self.traitCollection.userInterfaceStyle;
1905 if (style == UIUserInterfaceStyleDark) {
1915 - (NSString*)contrastMode {
1916 UIAccessibilityContrast contrast =
self.traitCollection.accessibilityContrast;
1918 if (contrast == UIAccessibilityContrastHigh) {
1925 #pragma mark - Status bar style
1927 - (UIStatusBarStyle)preferredStatusBarStyle {
1928 return self.statusBarStyle;
1931 - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
1934 dispatch_async(dispatch_get_main_queue(), ^{
1940 NSDictionary* info = notification.userInfo;
1941 NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)];
1942 if (update == nil) {
1946 UIStatusBarStyle style =
static_cast<UIStatusBarStyle
>(update.integerValue);
1947 if (style != strongSelf.statusBarStyle) {
1948 strongSelf.statusBarStyle = style;
1949 [strongSelf setNeedsStatusBarAppearanceUpdate];
1954 - (void)setPrefersStatusBarHidden:(BOOL)hidden {
1955 if (hidden !=
self.flutterPrefersStatusBarHidden) {
1956 self.flutterPrefersStatusBarHidden = hidden;
1957 [
self setNeedsStatusBarAppearanceUpdate];
1961 - (BOOL)prefersStatusBarHidden {
1962 return self.flutterPrefersStatusBarHidden;
1965 #pragma mark - Platform views
1968 return self.engine.platformViewsController;
1972 return self.engine.binaryMessenger;
1975 #pragma mark - FlutterBinaryMessenger
1977 - (void)sendOnChannel:(NSString*)channel message:(NSData*)message {
1978 [
self.engine.binaryMessenger sendOnChannel:channel message:message];
1981 - (void)sendOnChannel:(NSString*)channel
1982 message:(NSData*)message
1984 NSAssert(channel,
@"The channel must not be null");
1985 [
self.engine.binaryMessenger sendOnChannel:channel message:message binaryReply:callback];
1989 return [
self.engine.binaryMessenger makeBackgroundTaskQueue];
1993 binaryMessageHandler:
1995 return [
self setMessageHandlerOnChannel:channel binaryMessageHandler:handler taskQueue:nil];
1999 setMessageHandlerOnChannel:(NSString*)channel
2002 NSAssert(channel,
@"The channel must not be null");
2003 return [
self.engine.binaryMessenger setMessageHandlerOnChannel:channel
2004 binaryMessageHandler:handler
2005 taskQueue:taskQueue];
2009 [
self.engine.binaryMessenger cleanUpConnection:connection];
2012 #pragma mark - FlutterTextureRegistry
2015 return [
self.engine.textureRegistry registerTexture:texture];
2018 - (void)unregisterTexture:(int64_t)textureId {
2019 [
self.engine.textureRegistry unregisterTexture:textureId];
2022 - (void)textureFrameAvailable:(int64_t)textureId {
2023 [
self.engine.textureRegistry textureFrameAvailable:textureId];
2026 - (NSString*)lookupKeyForAsset:(NSString*)asset {
2030 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
2034 - (id<FlutterPluginRegistry>)pluginRegistry {
2038 + (BOOL)isUIAccessibilityIsVoiceOverRunning {
2039 return UIAccessibilityIsVoiceOverRunning();
2042 #pragma mark - FlutterPluginRegistry
2045 return [
self.engine registrarForPlugin:pluginKey];
2048 - (BOOL)hasPlugin:(NSString*)pluginKey {
2049 return [
self.engine hasPlugin:pluginKey];
2052 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
2053 return [
self.engine valuePublishedByPlugin:pluginKey];
2056 - (void)presentViewController:(UIViewController*)viewControllerToPresent
2058 completion:(
void (^)(
void))completion {
2059 self.isPresentingViewControllerAnimating = YES;
2061 [
super presentViewController:viewControllerToPresent
2064 weakSelf.isPresentingViewControllerAnimating = NO;
2071 - (BOOL)isPresentingViewController {
2072 return self.presentedViewController != nil ||
self.isPresentingViewControllerAnimating;
2075 - (
flutter::PointerData)updateMousePointerDataFrom:(UIGestureRecognizer*)gestureRecognizer
2076 API_AVAILABLE(ios(13.4)) {
2077 CGPoint location = [gestureRecognizer locationInView:self.view];
2078 CGFloat scale =
self.flutterScreenIfViewLoaded.scale;
2080 flutter::PointerData pointer_data;
2081 pointer_data.Clear();
2085 return pointer_data;
2088 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2089 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer
2090 API_AVAILABLE(ios(13.4)) {
2094 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
2095 shouldReceiveEvent:(UIEvent*)event API_AVAILABLE(ios(13.4)) {
2096 if (gestureRecognizer == _continuousScrollingPanGestureRecognizer &&
2097 event.type == UIEventTypeScroll) {
2099 flutter::PointerData pointer_data = [
self updateMousePointerDataFrom:gestureRecognizer];
2100 pointer_data.device =
reinterpret_cast<int64_t
>(_continuousScrollingPanGestureRecognizer);
2101 pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2102 pointer_data.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel;
2103 pointer_data.view_id =
self.viewIdentifier;
2105 if (event.timestamp <
self.scrollInertiaEventAppKitDeadline) {
2108 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2109 packet->SetPointerData(0, pointer_data);
2110 [
self.engine dispatchPointerDataPacket:std::move(packet)];
2111 self.scrollInertiaEventAppKitDeadline = 0;
2118 - (void)hoverEvent:(UIHoverGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2121 flutter::PointerData pointer_data = [
self updateMousePointerDataFrom:recognizer];
2122 pointer_data.device =
reinterpret_cast<int64_t
>(recognizer);
2123 pointer_data.kind = flutter::PointerData::DeviceKind::kMouse;
2124 pointer_data.view_id =
self.viewIdentifier;
2126 switch (_hoverGestureRecognizer.state) {
2127 case UIGestureRecognizerStateBegan:
2128 pointer_data.change = flutter::PointerData::Change::kAdd;
2130 case UIGestureRecognizerStateChanged:
2131 pointer_data.change = flutter::PointerData::Change::kHover;
2133 case UIGestureRecognizerStateEnded:
2134 case UIGestureRecognizerStateCancelled:
2135 pointer_data.change = flutter::PointerData::Change::kRemove;
2140 pointer_data.change = flutter::PointerData::Change::kHover;
2144 NSTimeInterval time = [NSProcessInfo processInfo].systemUptime;
2145 BOOL isRunningOnMac = NO;
2149 isRunningOnMac = [NSProcessInfo processInfo].iOSAppOnMac;
2151 time >
self.scrollInertiaEventStartline) {
2155 auto packet = std::make_unique<flutter::PointerDataPacket>(2);
2156 packet->SetPointerData(0, pointer_data);
2157 flutter::PointerData inertia_cancel = pointer_data;
2158 inertia_cancel.device =
reinterpret_cast<int64_t
>(_continuousScrollingPanGestureRecognizer);
2159 inertia_cancel.kind = flutter::PointerData::DeviceKind::kTrackpad;
2160 inertia_cancel.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel;
2161 inertia_cancel.view_id =
self.viewIdentifier;
2162 packet->SetPointerData(1, inertia_cancel);
2163 [
self.engine dispatchPointerDataPacket:std::move(packet)];
2164 self.scrollInertiaEventStartline = DBL_MAX;
2166 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2167 packet->SetPointerData(0, pointer_data);
2168 [
self.engine dispatchPointerDataPacket:std::move(packet)];
2172 - (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2173 CGPoint translation = [recognizer translationInView:self.view];
2174 const CGFloat scale =
self.flutterScreenIfViewLoaded.scale;
2176 translation.x *= scale;
2177 translation.y *= scale;
2179 flutter::PointerData pointer_data = [
self updateMousePointerDataFrom:recognizer];
2180 pointer_data.device =
reinterpret_cast<int64_t
>(recognizer);
2181 pointer_data.kind = flutter::PointerData::DeviceKind::kMouse;
2182 pointer_data.signal_kind = flutter::PointerData::SignalKind::kScroll;
2183 pointer_data.scroll_delta_x = (translation.x -
_mouseState.last_translation.x);
2184 pointer_data.scroll_delta_y = -(translation.y -
_mouseState.last_translation.y);
2185 pointer_data.view_id =
self.viewIdentifier;
2191 if (recognizer.state != UIGestureRecognizerStateEnded) {
2197 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2198 packet->SetPointerData(0, pointer_data);
2199 [
self.engine dispatchPointerDataPacket:std::move(packet)];
2202 - (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2203 CGPoint translation = [recognizer translationInView:self.view];
2204 const CGFloat scale =
self.flutterScreenIfViewLoaded.scale;
2206 flutter::PointerData pointer_data = [
self updateMousePointerDataFrom:recognizer];
2207 pointer_data.device =
reinterpret_cast<int64_t
>(recognizer);
2208 pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2209 pointer_data.view_id =
self.viewIdentifier;
2210 switch (recognizer.state) {
2211 case UIGestureRecognizerStateBegan:
2212 pointer_data.change = flutter::PointerData::Change::kPanZoomStart;
2214 case UIGestureRecognizerStateChanged:
2215 pointer_data.change = flutter::PointerData::Change::kPanZoomUpdate;
2216 pointer_data.pan_x = translation.x * scale;
2217 pointer_data.pan_y = translation.y * scale;
2218 pointer_data.pan_delta_x = 0;
2219 pointer_data.pan_delta_y = 0;
2220 pointer_data.scale = 1;
2222 case UIGestureRecognizerStateEnded:
2223 case UIGestureRecognizerStateCancelled:
2224 self.scrollInertiaEventStartline =
2225 [[NSProcessInfo processInfo] systemUptime] +
2235 self.scrollInertiaEventAppKitDeadline =
2236 [[NSProcessInfo processInfo] systemUptime] +
2237 (0.1821 * log(fmax([recognizer velocityInView:self.view].x,
2238 [recognizer velocityInView:self.view].y))) -
2240 pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
2244 NSAssert(NO,
@"Trackpad pan event occured with unexpected phase 0x%lx",
2245 (
long)recognizer.state);
2249 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2250 packet->SetPointerData(0, pointer_data);
2251 [
self.engine dispatchPointerDataPacket:std::move(packet)];
2254 - (void)pinchEvent:(UIPinchGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
2255 flutter::PointerData pointer_data = [
self updateMousePointerDataFrom:recognizer];
2256 pointer_data.device =
reinterpret_cast<int64_t
>(recognizer);
2257 pointer_data.kind = flutter::PointerData::DeviceKind::kTrackpad;
2258 pointer_data.view_id =
self.viewIdentifier;
2259 switch (recognizer.state) {
2260 case UIGestureRecognizerStateBegan:
2261 pointer_data.change = flutter::PointerData::Change::kPanZoomStart;
2263 case UIGestureRecognizerStateChanged:
2264 pointer_data.change = flutter::PointerData::Change::kPanZoomUpdate;
2265 pointer_data.scale = recognizer.scale;
2266 pointer_data.rotation = _rotationGestureRecognizer.rotation;
2268 case UIGestureRecognizerStateEnded:
2269 case UIGestureRecognizerStateCancelled:
2270 pointer_data.change = flutter::PointerData::Change::kPanZoomEnd;
2274 NSAssert(NO,
@"Trackpad pinch event occured with unexpected phase 0x%lx",
2275 (
long)recognizer.state);
2279 auto packet = std::make_unique<flutter::PointerDataPacket>(1);
2280 packet->SetPointerData(0, pointer_data);
2281 [
self.engine dispatchPointerDataPacket:std::move(packet)];
2284 #pragma mark - State Restoration
2286 - (void)encodeRestorableStateWithCoder:(NSCoder*)coder {
2287 NSData* restorationData = [
self.engine.restorationPlugin restorationData];
2288 [coder encodeBytes:(const unsigned char*)restorationData.bytes
2289 length:restorationData.length
2290 forKey:kFlutterRestorationStateAppData];
2291 [
super encodeRestorableStateWithCoder:coder];
2294 - (void)decodeRestorableStateWithCoder:(NSCoder*)coder {
2295 NSUInteger restorationDataLength;
2296 const unsigned char* restorationBytes = [coder decodeBytesForKey:kFlutterRestorationStateAppData
2297 returnedLength:&restorationDataLength];
2298 NSData* restorationData = [NSData dataWithBytes:restorationBytes length:restorationDataLength];
2299 [
self.engine.restorationPlugin setRestorationData:restorationData];
2303 return self.engine.restorationPlugin;
2307 return self.engine.textInputPlugin;
2310 #pragma mark - FlutterKeyboardInsetManagerDelegate
2312 - (void)updateViewportMetricsWithInset:(CGFloat)inset {
2313 _viewportMetrics.physical_view_inset_bottom = inset;
2314 [
self updateViewportMetricsIfNeeded];
2317 - (CGFloat)physicalViewInsetBottom {
2318 return _viewportMetrics.physical_view_inset_bottom;
2321 - (BOOL)isPadInSlideOverOrStageManagerMode {
2322 if (
self.view.traitCollection.userInterfaceIdiom == UIUserInterfaceIdiomPad &&
2323 self.view.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact &&
2324 self.view.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassRegular) {
2330 - (CGRect)convertViewRectToScreen:(CGRect)rect {
2331 return [
self.view convertRect:rect
2332 toCoordinateSpace:self.flutterScreenIfViewLoaded.coordinateSpace];
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
void(^ FlutterBinaryMessageHandler)(NSData *_Nullable message, FlutterBinaryReply reply)
int64_t FlutterBinaryMessengerConnection
void(^ FlutterSendKeyEvent)(const FlutterKeyEvent &, _Nullable FlutterKeyEventCallback, void *_Nullable)
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
instancetype initWithCoder
FlutterTextInputPlugin * textInputPlugin
NSNotificationName const FlutterViewControllerHideHomeIndicator
FlutterSplashScreenManager * _splashScreenManager
static NSString *const kFlutterRestorationStateAppData
NSNotificationName const FlutterViewControllerShowHomeIndicator
NSNotificationName const FlutterSemanticsUpdateNotification
struct MouseState MouseState
static constexpr CGFloat kScrollViewContentSize
NSNotificationName const FlutterViewControllerWillDealloc
static constexpr FLUTTER_ASSERT_ARC int kMicrosecondsPerSecond
UIPanGestureRecognizer *continuousScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4))
UIPanGestureRecognizer *discreteScrollingPanGestureRecognizer API_AVAILABLE(ios(13.4))
UIPinchGestureRecognizer *pinchGestureRecognizer API_AVAILABLE(ios(13.4))
UIHoverGestureRecognizer *hoverGestureRecognizer API_AVAILABLE(ios(13.4))
NSString * lookupKeyForAsset:fromPackage:(NSString *asset,[fromPackage] NSString *package)
NSString * lookupKeyForAsset:(NSString *asset)
Coordinates the animation of the bottom viewport inset in response to system keyboard visibility chan...
UIApplication * application
void setUpIndirectScribbleInteraction:(id< FlutterViewResponder > viewResponder)
UIView * splashScreenView
Manages the calculations and animations for software keyboard insets.