diff --git a/shell/platform/darwin/ios/BUILD.gn b/shell/platform/darwin/ios/BUILD.gn index 37f6b3fe65eb0..02ea92bc9d500 100644 --- a/shell/platform/darwin/ios/BUILD.gn +++ b/shell/platform/darwin/ios/BUILD.gn @@ -105,6 +105,8 @@ source_set("flutter_framework_source_arc") { "framework/Source/FlutterUndoManagerPlugin.mm", "framework/Source/FlutterView.h", "framework/Source/FlutterView.mm", + "framework/Source/FlutterViewController.mm", + "framework/Source/FlutterViewController_Internal.h", "framework/Source/FlutterViewResponder.h", "framework/Source/KeyCodeMap.g.mm", "framework/Source/KeyCodeMap_Internal.h", @@ -175,6 +177,7 @@ source_set("flutter_framework_source_arc") { "//flutter/shell/platform/darwin/graphics", "//flutter/shell/platform/embedder:embedder_as_internal_library", "//flutter/shell/profiling:profiling", + "//flutter/third_party/spring_animation", ] } @@ -189,8 +192,6 @@ source_set("flutter_framework_source") { # iOS embedder is migrating to ARC. # New files are highly encouraged to be in ARC. # To add new files in ARC, add them to the `flutter_framework_source_arc` target. - "framework/Source/FlutterViewController.mm", - "framework/Source/FlutterViewController_Internal.h", "platform_view_ios.h", "platform_view_ios.mm", ] @@ -210,7 +211,6 @@ source_set("flutter_framework_source") { "//flutter/shell/platform/darwin/common:framework_common", "//flutter/shell/platform/embedder:embedder_as_internal_library", "//flutter/shell/profiling:profiling", - "//flutter/third_party/spring_animation", ] public_configs = [ diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm index ebf48bdd1076a..2ef686974f793 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController.mm @@ -13,7 +13,6 @@ #include "flutter/fml/memory/weak_ptr.h" #include "flutter/fml/message_loop.h" #include "flutter/fml/platform/darwin/platform_version.h" -#include "flutter/fml/platform/darwin/scoped_nsobject.h" #include "flutter/runtime/ptrace_check.h" #include "flutter/shell/common/thread_host.h" #import "flutter/shell/platform/darwin/common/framework/Source/FlutterBinaryMessengerRelay.h" @@ -34,6 +33,8 @@ #import "flutter/shell/platform/embedder/embedder.h" #import "flutter/third_party/spring_animation/spring_animation.h" +FLUTTER_ASSERT_ARC + static constexpr int kMicrosecondsPerSecond = 1000 * 1000; static constexpr CGFloat kScrollViewContentSize = 2.0; @@ -64,10 +65,29 @@ @interface FlutterViewController () * ongoingTouches; +// This scroll view is a workaround to accommodate iOS 13 and higher. There isn't a way to get +// touches on the status bar to trigger scrolling to the top of a scroll view. We place a +// UIScrollView with height zero and a content offset so we can get those events. See also: +// https://github.com/flutter/flutter/issues/35050 +@property(nonatomic, strong) UIScrollView* scrollView; +@property(nonatomic, strong) UIView* keyboardAnimationView; +@property(nonatomic, strong) SpringAnimation* keyboardSpringAnimation; + /** * Whether we should ignore viewport metrics updates during rotation transition. */ @@ -78,80 +98,71 @@ @interface FlutterViewController () > _weakFactory; - fml::scoped_nsobject _engine; - - // We keep a separate reference to this and create it ahead of time because we want to be able to - // set up a shell along with its platform view before the view has to appear. - fml::scoped_nsobject _flutterView; - fml::scoped_nsobject _splashScreenView; - fml::ScopedBlock _flutterViewRenderedCallback; - UIInterfaceOrientationMask _orientationPreferences; - UIStatusBarStyle _statusBarStyle; + FlutterEngine* _engine; + flutter::ViewportMetrics _viewportMetrics; - BOOL _initialized; - BOOL _viewOpaque; - BOOL _engineNeedsLaunch; - fml::scoped_nsobject> _ongoingTouches; - // This scroll view is a workaround to accommodate iOS 13 and higher. There isn't a way to get - // touches on the status bar to trigger scrolling to the top of a scroll view. We place a - // UIScrollView with height zero and a content offset so we can get those events. See also: - // https://github.com/flutter/flutter/issues/35050 - fml::scoped_nsobject _scrollView; - fml::scoped_nsobject _keyboardAnimationView; - fml::scoped_nsobject _keyboardSpringAnimation; MouseState _mouseState; - // Timestamp after which a scroll inertia cancel event should be inferred. - NSTimeInterval _scrollInertiaEventStartline; - // When an iOS app is running in emulation on an Apple Silicon Mac, trackpad input goes through - // a translation layer, and events are not received with precise deltas. Due to this, we can't - // rely on checking for a stationary trackpad event. Fortunately, AppKit will send an event of - // type UIEventTypeScroll following a scroll when inertia should stop. This field is needed to - // estimate if such an event represents the natural end of scrolling inertia or a user-initiated - // cancellation. - NSTimeInterval _scrollInertiaEventAppKitDeadline; } +@synthesize viewOpaque = _viewOpaque; @synthesize displayingFlutterUI = _displayingFlutterUI; @synthesize prefersStatusBarHidden = _flutterPrefersStatusBarHidden; @dynamic viewIdentifier; @@ -173,14 +184,16 @@ - (instancetype)initWithEngine:(FlutterEngine*)engine "FlutterViewController at a time. Set FlutterEngine.viewController " "to nil before attaching it to another FlutterViewController."; } - _engine.reset([engine retain]); + _engine = engine; _engineNeedsLaunch = NO; - _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine - opaque:self.isViewOpaque - enableWideGamut:engine.project.isWideGamutEnabled]); + _flutterView = [[FlutterView alloc] initWithDelegate:_engine + opaque:self.isViewOpaque + enableWideGamut:engine.project.isWideGamutEnabled]; _weakFactory = std::make_unique>(self); - _ongoingTouches.reset([[NSMutableSet alloc] init]); + _ongoingTouches = [[NSMutableSet alloc] init]; + // TODO(cbracken): https://github.com/flutter/flutter/issues/157140 + // Eliminate method calls in initializers and dealloc. [self performCommonViewControllerInitialization]; [engine setViewController:self]; } @@ -193,6 +206,8 @@ - (instancetype)initWithProject:(FlutterDartProject*)project bundle:(NSBundle*)nibBundle { self = [super initWithNibName:nibName bundle:nibBundle]; if (self) { + // TODO(cbracken): https://github.com/flutter/flutter/issues/157140 + // Eliminate method calls in initializers and dealloc. [self sharedSetupWithProject:project initialRoute:nil]; } @@ -205,6 +220,8 @@ - (instancetype)initWithProject:(FlutterDartProject*)project bundle:(NSBundle*)nibBundle { self = [super initWithNibName:nibName bundle:nibBundle]; if (self) { + // TODO(cbracken): https://github.com/flutter/flutter/issues/157140 + // Eliminate method calls in initializers and dealloc. [self sharedSetupWithProject:project initialRoute:initialRoute]; } @@ -222,7 +239,7 @@ - (instancetype)initWithCoder:(NSCoder*)aDecoder { - (void)awakeFromNib { [super awakeFromNib]; - if (!_engine) { + if (!self.engine) { [self sharedSetupWithProject:nil initialRoute:nil]; } } @@ -236,28 +253,29 @@ - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project // Need the project to get settings for the view. Initializing it here means // the Engine class won't initialize it later. if (!project) { - project = [[[FlutterDartProject alloc] init] autorelease]; + project = [[FlutterDartProject alloc] init]; } FlutterView.forceSoftwareRendering = project.settings.enable_software_rendering; _weakFactory = std::make_unique>(self); - auto engine = fml::scoped_nsobject{[[FlutterEngine alloc] - initWithName:@"io.flutter" - project:project - allowHeadlessExecution:self.engineAllowHeadlessExecution - restorationEnabled:[self restorationIdentifier] != nil]}; - + FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"io.flutter" + project:project + allowHeadlessExecution:self.engineAllowHeadlessExecution + restorationEnabled:self.restorationIdentifier != nil]; if (!engine) { return; } _viewOpaque = YES; _engine = engine; - _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine - opaque:self.isViewOpaque - enableWideGamut:project.isWideGamutEnabled]); - [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute]; + _flutterView = [[FlutterView alloc] initWithDelegate:_engine + opaque:_viewOpaque + enableWideGamut:project.isWideGamutEnabled]; + [_engine createShell:nil libraryURI:nil initialRoute:initialRoute]; _engineNeedsLaunch = YES; - _ongoingTouches.reset([[NSMutableSet alloc] init]); + _ongoingTouches = [[NSMutableSet alloc] init]; + + // TODO(cbracken): https://github.com/flutter/flutter/issues/157140 + // Eliminate method calls in initializers and dealloc. [self loadDefaultSplashScreenView]; [self performCommonViewControllerInitialization]; } @@ -268,9 +286,9 @@ - (BOOL)isViewOpaque { - (void)setViewOpaque:(BOOL)value { _viewOpaque = value; - if (_flutterView.get().layer.opaque != value) { - _flutterView.get().layer.opaque = value; - [_flutterView.get().layer setNeedsLayout]; + if (self.flutterView.layer.opaque != value) { + self.flutterView.layer.opaque = value; + [self.flutterView.layer setNeedsLayout]; } } @@ -282,15 +300,16 @@ - (void)performCommonViewControllerInitialization { } _initialized = YES; - _orientationPreferences = UIInterfaceOrientationMaskAll; _statusBarStyle = UIStatusBarStyleDefault; + // TODO(cbracken): https://github.com/flutter/flutter/issues/157140 + // Eliminate method calls in initializers and dealloc. [self setUpNotificationCenterObservers]; } - (FlutterEngine*)engine { - return _engine.get(); + return _engine; } - (fml::WeakNSObject)getWeakNSObject { @@ -447,15 +466,15 @@ - (void)setUpApplicationLifecycleNotifications:(NSNotificationCenter*)center { } - (void)setInitialRoute:(NSString*)route { - [[_engine.get() navigationChannel] invokeMethod:@"setInitialRoute" arguments:route]; + [self.engine.navigationChannel invokeMethod:@"setInitialRoute" arguments:route]; } - (void)popRoute { - [[_engine.get() navigationChannel] invokeMethod:@"popRoute" arguments:nil]; + [self.engine.navigationChannel invokeMethod:@"popRoute" arguments:nil]; } - (void)pushRoute:(NSString*)route { - [[_engine.get() navigationChannel] invokeMethod:@"pushRoute" arguments:route]; + [self.engine.navigationChannel invokeMethod:@"pushRoute" arguments:route]; } #pragma mark - Loading the view @@ -465,7 +484,7 @@ - (void)pushRoute:(NSString*)route { return existing_view; } - auto placeholder = [[[UIView alloc] init] autorelease]; + auto placeholder = [[UIView alloc] init]; placeholder.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; if (@available(iOS 13.0, *)) { @@ -479,7 +498,7 @@ - (void)pushRoute:(NSString*)route { // Otherwise, a spurious warning will be shown in cases where an engine cannot be initialized for // other reasons. if (flutter::GetTracingResult() == flutter::TracingResult::kDisabled) { - auto messageLabel = [[[UILabel alloc] init] autorelease]; + auto messageLabel = [[UILabel alloc] init]; messageLabel.numberOfLines = 0u; messageLabel.textAlignment = NSTextAlignmentCenter; messageLabel.autoresizingMask = @@ -495,11 +514,13 @@ - (void)pushRoute:(NSString*)route { } - (void)loadView { - self.view = GetViewOrPlaceholder(_flutterView.get()); + self.view = GetViewOrPlaceholder(self.flutterView); self.view.multipleTouchEnabled = YES; self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self installSplashScreenViewIfNecessary]; + + // Create and set up the scroll view. UIScrollView* scrollView = [[UIScrollView alloc] init]; scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth; // The color shouldn't matter since it is offscreen. @@ -509,8 +530,9 @@ - (void)loadView { scrollView.contentSize = CGSizeMake(kScrollViewContentSize, kScrollViewContentSize); // This is an arbitrary offset that is not CGPointZero. scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize); + [self.view addSubview:scrollView]; - _scrollView.reset(scrollView); + self.scrollView = scrollView; } - (flutter::PointerData)generatePointerDataForFake { @@ -539,14 +561,14 @@ static void SendFakeTouchEvent(UIScreen* screen, } - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView { - if (!_engine) { + if (!self.engine) { return NO; } CGPoint statusBarPoint = CGPointZero; - UIScreen* screen = [self flutterScreenIfViewLoaded]; + UIScreen* screen = self.flutterScreenIfViewLoaded; if (screen) { - SendFakeTouchEvent(screen, _engine.get(), statusBarPoint, flutter::PointerData::Change::kDown); - SendFakeTouchEvent(screen, _engine.get(), statusBarPoint, flutter::PointerData::Change::kUp); + SendFakeTouchEvent(screen, self.engine, statusBarPoint, flutter::PointerData::Change::kDown); + SendFakeTouchEvent(screen, self.engine, statusBarPoint, flutter::PointerData::Change::kUp); } return NO; } @@ -556,9 +578,9 @@ - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView { - (void)installSplashScreenViewIfNecessary { // Show the launch screen view again on top of the FlutterView if available. // This launch screen view will be removed once the first Flutter frame is rendered. - if (_splashScreenView && (self.isBeingPresented || self.isMovingToParentViewController)) { - [_splashScreenView.get() removeFromSuperview]; - _splashScreenView.reset(); + if (self.splashScreenView && (self.isBeingPresented || self.isMovingToParentViewController)) { + [self.splashScreenView removeFromSuperview]; + self.splashScreenView = nil; return; } @@ -590,60 +612,58 @@ - (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI { - (void)callViewRenderedCallback { self.displayingFlutterUI = YES; - if (_flutterViewRenderedCallback != nil) { - _flutterViewRenderedCallback.get()(); - _flutterViewRenderedCallback.reset(); + if (self.flutterViewRenderedCallback) { + self.flutterViewRenderedCallback(); + self.flutterViewRenderedCallback = nil; } } -- (void)removeSplashScreenView:(dispatch_block_t _Nullable)onComplete { - NSAssert(_splashScreenView, @"The splash screen view must not be null"); - UIView* splashScreen = [_splashScreenView.get() retain]; - _splashScreenView.reset(); +- (void)removeSplashScreenWithCompletion:(dispatch_block_t _Nullable)onComplete { + NSAssert(self.splashScreenView, @"The splash screen view must not be nil"); + UIView* splashScreen = self.splashScreenView; + // setSplashScreenView calls this method. Assign directly to ivar to avoid an infinite loop. + _splashScreenView = nil; [UIView animateWithDuration:0.2 animations:^{ splashScreen.alpha = 0; } completion:^(BOOL finished) { [splashScreen removeFromSuperview]; - [splashScreen release]; if (onComplete) { onComplete(); } }]; } +- (void)onFirstFrameRendered { + if (self.splashScreenView) { + __weak FlutterViewController* weakSelf = self; + [self removeSplashScreenWithCompletion:^{ + [weakSelf callViewRenderedCallback]; + }]; + } else { + [self callViewRenderedCallback]; + } +} + - (void)installFirstFrameCallback { - if (!_engine) { + if (!self.engine) { return; } - fml::WeakPtr weakPlatformView = [_engine.get() platformView]; + fml::WeakPtr weakPlatformView = self.engine.platformView; if (!weakPlatformView) { return; } // Start on the platform thread. - weakPlatformView->SetNextFrameCallback([weakSelf = [self getWeakNSObject], - platformTaskRunner = [_engine.get() platformTaskRunner], - rasterTaskRunner = [_engine.get() rasterTaskRunner]]() { + __weak FlutterViewController* weakSelf = self; + weakPlatformView->SetNextFrameCallback([weakSelf, + platformTaskRunner = self.engine.platformTaskRunner, + rasterTaskRunner = self.engine.rasterTaskRunner]() { FML_DCHECK(rasterTaskRunner->RunsTasksOnCurrentThread()); // Get callback on raster thread and jump back to platform thread. - platformTaskRunner->PostTask([weakSelf]() { - if (weakSelf) { - fml::scoped_nsobject flutterViewController( - [(FlutterViewController*)weakSelf.get() retain]); - if (flutterViewController) { - if (flutterViewController.get()->_splashScreenView) { - [flutterViewController removeSplashScreenView:^{ - [flutterViewController callViewRenderedCallback]; - }]; - } else { - [flutterViewController callViewRenderedCallback]; - } - } - } - }); + platformTaskRunner->PostTask([weakSelf]() { [weakSelf onFirstFrameRendered]; }); }); } @@ -655,21 +675,6 @@ - (int64_t)viewIdentifier { return flutter::kFlutterImplicitViewId; } -- (UIView*)splashScreenView { - if (!_splashScreenView) { - return nil; - } - return _splashScreenView.get(); -} - -- (UIView*)keyboardAnimationView { - return _keyboardAnimationView.get(); -} - -- (SpringAnimation*)keyboardSpringAnimation { - return _keyboardSpringAnimation.get(); -} - - (BOOL)loadDefaultSplashScreenView { NSString* launchscreenName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"]; @@ -716,27 +721,31 @@ - (UIView*)splashScreenFromXib:(NSString*)name { } - (void)setSplashScreenView:(UIView*)view { + if (view == _splashScreenView) { + return; + } + + // Special case: user wants to remove the splash screen view. if (!view) { - // Special case: user wants to remove the splash screen view. if (_splashScreenView) { - [self removeSplashScreenView:nil]; + [self removeSplashScreenWithCompletion:nil]; } return; } - _splashScreenView.reset([view retain]); - _splashScreenView.get().autoresizingMask = + _splashScreenView = view; + _splashScreenView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; } - (void)setFlutterViewDidRenderCallback:(void (^)(void))callback { - _flutterViewRenderedCallback.reset(callback, fml::scoped_policy::OwnershipPolicy::kRetain); + _flutterViewRenderedCallback = callback; } #pragma mark - Surface creation and teardown updates - (void)surfaceUpdated:(BOOL)appeared { - if (!_engine) { + if (!self.engine) { return; } @@ -744,14 +753,14 @@ - (void)surfaceUpdated:(BOOL)appeared { // thread. if (appeared) { [self installFirstFrameCallback]; - [_engine.get() platformViewsController]->SetFlutterView(_flutterView.get()); - [_engine.get() platformViewsController]->SetFlutterViewController(self); - [_engine.get() iosPlatformView]->NotifyCreated(); + [self.engine platformViewsController]->SetFlutterView(self.flutterView); + [self.engine platformViewsController]->SetFlutterViewController(self); + [self.engine iosPlatformView]->NotifyCreated(); } else { self.displayingFlutterUI = NO; - [_engine.get() iosPlatformView]->NotifyDestroyed(); - [_engine.get() platformViewsController]->SetFlutterView(nullptr); - [_engine.get() platformViewsController]->SetFlutterViewController(nullptr); + [self.engine iosPlatformView]->NotifyDestroyed(); + [self.engine platformViewsController]->SetFlutterView(nullptr); + [self.engine platformViewsController]->SetFlutterViewController(nullptr); } } @@ -760,12 +769,12 @@ - (void)surfaceUpdated:(BOOL)appeared { - (void)viewDidLoad { TRACE_EVENT0("flutter", "viewDidLoad"); - if (_engine && _engineNeedsLaunch) { - [_engine.get() launchEngine:nil libraryURI:nil entrypointArgs:nil]; - [_engine.get() setViewController:self]; - _engineNeedsLaunch = NO; - } else if ([_engine.get() viewController] == self) { - [_engine.get() attachView]; + if (self.engine && self.engineNeedsLaunch) { + [self.engine launchEngine:nil libraryURI:nil entrypointArgs:nil]; + [self.engine setViewController:self]; + self.engineNeedsLaunch = NO; + } else if (self.engine.viewController == self) { + [self.engine attachView]; } // Register internal plugins. @@ -778,7 +787,7 @@ - (void)viewDidLoad { _hoverGestureRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverEvent:)]; _hoverGestureRecognizer.delegate = self; - [_flutterView.get() addGestureRecognizer:_hoverGestureRecognizer]; + [self.flutterView addGestureRecognizer:_hoverGestureRecognizer]; _discreteScrollingPanGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(discreteScrollEvent:)]; @@ -789,47 +798,45 @@ - (void)viewDidLoad { // than touch events, so they will still be received. _discreteScrollingPanGestureRecognizer.allowedTouchTypes = @[]; _discreteScrollingPanGestureRecognizer.delegate = self; - [_flutterView.get() addGestureRecognizer:_discreteScrollingPanGestureRecognizer]; + [self.flutterView addGestureRecognizer:_discreteScrollingPanGestureRecognizer]; _continuousScrollingPanGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(continuousScrollEvent:)]; _continuousScrollingPanGestureRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous; _continuousScrollingPanGestureRecognizer.allowedTouchTypes = @[]; _continuousScrollingPanGestureRecognizer.delegate = self; - [_flutterView.get() addGestureRecognizer:_continuousScrollingPanGestureRecognizer]; + [self.flutterView addGestureRecognizer:_continuousScrollingPanGestureRecognizer]; _pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(pinchEvent:)]; _pinchGestureRecognizer.allowedTouchTypes = @[]; _pinchGestureRecognizer.delegate = self; - [_flutterView.get() addGestureRecognizer:_pinchGestureRecognizer]; + [self.flutterView addGestureRecognizer:_pinchGestureRecognizer]; _rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] init]; _rotationGestureRecognizer.allowedTouchTypes = @[]; _rotationGestureRecognizer.delegate = self; - [_flutterView.get() addGestureRecognizer:_rotationGestureRecognizer]; + [self.flutterView addGestureRecognizer:_rotationGestureRecognizer]; } [super viewDidLoad]; } - (void)addInternalPlugins { - self.keyboardManager = [[[FlutterKeyboardManager alloc] init] autorelease]; - fml::WeakNSObject weakSelf = [self getWeakNSObject]; + self.keyboardManager = [[FlutterKeyboardManager alloc] init]; + __weak FlutterViewController* weakSelf = self; FlutterSendKeyEvent sendEvent = ^(const FlutterKeyEvent& event, FlutterKeyEventCallback callback, void* userData) { - if (weakSelf) { - [weakSelf.get()->_engine.get() sendKeyEvent:event callback:callback userData:userData]; - } + [weakSelf.engine sendKeyEvent:event callback:callback userData:userData]; }; - [self.keyboardManager addPrimaryResponder:[[[FlutterEmbedderKeyResponder alloc] - initWithSendEvent:sendEvent] autorelease]]; - FlutterChannelKeyResponder* responder = [[[FlutterChannelKeyResponder alloc] - initWithChannel:self.engine.keyEventChannel] autorelease]; + [self.keyboardManager + addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] initWithSendEvent:sendEvent]]; + FlutterChannelKeyResponder* responder = + [[FlutterChannelKeyResponder alloc] initWithChannel:self.engine.keyEventChannel]; [self.keyboardManager addPrimaryResponder:responder]; FlutterTextInputPlugin* textInputPlugin = self.engine.textInputPlugin; if (textInputPlugin != nil) { [self.keyboardManager addSecondaryResponder:textInputPlugin]; } - if ([_engine.get() viewController] == self) { + if (self.engine.viewController == self) { [textInputPlugin setUpIndirectScribbleInteraction:self]; } } @@ -840,7 +847,7 @@ - (void)removeInternalPlugins { - (void)viewWillAppear:(BOOL)animated { TRACE_EVENT0("flutter", "viewWillAppear"); - if ([_engine.get() viewController] == self) { + if (self.engine.viewController == self) { // Send platform settings to Flutter, e.g., platform brightness. [self onUserSettingsChanged:nil]; @@ -849,8 +856,8 @@ - (void)viewWillAppear:(BOOL)animated { if (_viewportMetrics.physical_width) { [self surfaceUpdated:YES]; } - [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"]; - [[_engine.get() restorationPlugin] markRestorationComplete]; + [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]; + [self.engine.restorationPlugin markRestorationComplete]; } [super viewWillAppear:animated]; @@ -858,7 +865,7 @@ - (void)viewWillAppear:(BOOL)animated { - (void)viewDidAppear:(BOOL)animated { TRACE_EVENT0("flutter", "viewDidAppear"); - if ([_engine.get() viewController] == self) { + if (self.engine.viewController == self) { [self onUserSettingsChanged:nil]; [self onAccessibilityStatusChanged:nil]; BOOL stateIsActive = YES; @@ -871,7 +878,7 @@ - (void)viewDidAppear:(BOOL)animated { stateIsActive = UIApplication.sharedApplication.applicationState == UIApplicationStateActive; #endif if (stateIsActive) { - [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"]; + [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.resumed"]; } } [super viewDidAppear:animated]; @@ -879,21 +886,21 @@ - (void)viewDidAppear:(BOOL)animated { - (void)viewWillDisappear:(BOOL)animated { TRACE_EVENT0("flutter", "viewWillDisappear"); - if ([_engine.get() viewController] == self) { - [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"]; + if (self.engine.viewController == self) { + [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]; } [super viewWillDisappear:animated]; } - (void)viewDidDisappear:(BOOL)animated { TRACE_EVENT0("flutter", "viewDidDisappear"); - if ([_engine.get() viewController] == self) { + if (self.engine.viewController == self) { [self invalidateKeyboardAnimationVSyncClient]; [self ensureViewportMetricsIsCorrect]; [self surfaceUpdated:NO]; - [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"]; + [self.engine.lifecycleChannel sendMessage:@"AppLifecycleState.paused"]; [self flushOngoingTouches]; - [_engine.get() notifyLowMemory]; + [self.engine notifyLowMemory]; } [super viewDidDisappear:animated]; @@ -917,24 +924,30 @@ - (void)viewWillTransitionToSize:(CGSize)size return; } + __weak FlutterViewController* weakSelf = self; _shouldIgnoreViewportMetricsUpdatesDuringRotation = YES; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, static_cast(transitionDuration / 2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + FlutterViewController* strongSelf = weakSelf; + if (!strongSelf) { + return; + } + // `viewWillTransitionToSize` is only called after the previous rotation is // complete. So there won't be race condition for this flag. - _shouldIgnoreViewportMetricsUpdatesDuringRotation = NO; - [self updateViewportMetricsIfNeeded]; + strongSelf.shouldIgnoreViewportMetricsUpdatesDuringRotation = NO; + [strongSelf updateViewportMetricsIfNeeded]; }); } - (void)flushOngoingTouches { - if (_engine && _ongoingTouches.get().count > 0) { - auto packet = std::make_unique(_ongoingTouches.get().count); + if (self.engine && self.ongoingTouches.count > 0) { + auto packet = std::make_unique(self.ongoingTouches.count); size_t pointer_index = 0; // If the view controller is going away, we want to flush cancel all the ongoing // touches to the framework so nothing gets orphaned. - for (NSNumber* device in _ongoingTouches.get()) { + for (NSNumber* device in self.ongoingTouches) { // Create fake PointerData to balance out each previously started one for the framework. flutter::PointerData pointer_data = [self generatePointerDataForFake]; @@ -954,8 +967,8 @@ - (void)flushOngoingTouches { packet->SetPointerData(pointer_index++, pointer_data); } - [_ongoingTouches removeAllObjects]; - [_engine.get() dispatchPointerDataPacket:std::move(packet)]; + [self.ongoingTouches removeAllObjects]; + [self.engine dispatchPointerDataPacket:std::move(packet)]; } } @@ -971,23 +984,22 @@ - (void)dealloc { // before any other members are destroyed. _weakFactory.reset(); + // TODO(cbracken): https://github.com/flutter/flutter/issues/157140 + // Eliminate method calls in initializers and dealloc. [self removeInternalPlugins]; [self deregisterNotifications]; [self invalidateKeyboardAnimationVSyncClient]; [self invalidateTouchRateCorrectionVSyncClient]; - _scrollView.get().delegate = nil; + + // TODO(cbracken): https://github.com/flutter/flutter/issues/156222 + // Ensure all delegates are weak and remove this. + _scrollView.delegate = nil; _hoverGestureRecognizer.delegate = nil; - [_hoverGestureRecognizer release]; _discreteScrollingPanGestureRecognizer.delegate = nil; - [_discreteScrollingPanGestureRecognizer release]; _continuousScrollingPanGestureRecognizer.delegate = nil; - [_continuousScrollingPanGestureRecognizer release]; _pinchGestureRecognizer.delegate = nil; - [_pinchGestureRecognizer release]; _rotationGestureRecognizer.delegate = nil; - [_rotationGestureRecognizer release]; - [super dealloc]; } #pragma mark - Application lifecycle notifications @@ -1081,7 +1093,7 @@ - (void)goToApplicationLifecycle:(nonnull NSString*)state { // Accessing self.view will create the view. Instead use viewIfLoaded // to check whether the view is attached to window. if (self.viewIfLoaded.window) { - [[_engine.get() lifecycleChannel] sendMessage:state]; + [self.engine.lifecycleChannel sendMessage:state]; } } @@ -1133,7 +1145,7 @@ - (void)goToApplicationLifecycle:(nonnull NSString*)state { - (void)dispatchTouches:(NSSet*)touches pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change event:(UIEvent*)event { - if (!_engine) { + if (!self.engine) { return; } @@ -1167,7 +1179,7 @@ - (void)dispatchTouches:(NSSet*)touches // Activate or pause the correction of delivery frame rate of touch events. [self triggerTouchRateCorrectionIfNeeded:touches]; - const CGFloat scale = [self flutterScreenIfViewLoaded].scale; + const CGFloat scale = self.flutterScreenIfViewLoaded.scale; auto packet = std::make_unique(touches.count + touches_to_remove_count); @@ -1207,11 +1219,11 @@ - (void)dispatchTouches:(NSSet*)touches // if the view controller goes away. switch (pointer_data.change) { case flutter::PointerData::Change::kDown: - [_ongoingTouches addObject:deviceKey]; + [self.ongoingTouches addObject:deviceKey]; break; case flutter::PointerData::Change::kCancel: case flutter::PointerData::Change::kUp: - [_ongoingTouches removeObject:deviceKey]; + [self.ongoingTouches removeObject:deviceKey]; break; case flutter::PointerData::Change::kHover: case flutter::PointerData::Change::kMove: @@ -1290,7 +1302,7 @@ - (void)dispatchTouches:(NSSet*)touches } } - [_engine.get() dispatchPointerDataPacket:std::move(packet)]; + [self.engine dispatchPointerDataPacket:std::move(packet)]; } - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { @@ -1331,7 +1343,7 @@ - (void)createTouchRateCorrectionVSyncClientIfNeeded { return; } - flutter::Shell& shell = [_engine.get() shell]; + flutter::Shell& shell = self.engine.shell; auto callback = [](std::unique_ptr recorder) { // Do nothing in this block. Just trigger system to callback touch events with correct rate. }; @@ -1358,7 +1370,7 @@ - (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches { } } - if (isUserInteracting && [_engine.get() viewController] == self) { + if (isUserInteracting && self.engine.viewController == self) { [_touchRateCorrectionVSyncClient await]; } else { [_touchRateCorrectionVSyncClient pause]; @@ -1367,7 +1379,6 @@ - (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches { - (void)invalidateTouchRateCorrectionVSyncClient { [_touchRateCorrectionVSyncClient invalidate]; - [_touchRateCorrectionVSyncClient release]; _touchRateCorrectionVSyncClient = nil; } @@ -1377,18 +1388,18 @@ - (void)updateViewportMetricsIfNeeded { if (_shouldIgnoreViewportMetricsUpdatesDuringRotation) { return; } - if ([_engine.get() viewController] == self) { - [_engine.get() updateViewportMetrics:_viewportMetrics]; + if (self.engine.viewController == self) { + [self.engine updateViewportMetrics:_viewportMetrics]; } } - (void)viewDidLayoutSubviews { CGRect viewBounds = self.view.bounds; - CGFloat scale = [self flutterScreenIfViewLoaded].scale; + CGFloat scale = self.flutterScreenIfViewLoaded.scale; // Purposefully place this not visible. - _scrollView.get().frame = CGRectMake(0.0, 0.0, viewBounds.size.width, 0.0); - _scrollView.get().contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize); + self.scrollView.frame = CGRectMake(0.0, 0.0, viewBounds.size.width, 0.0); + self.scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize); // First time since creation that the dimensions of its view is known. bool firstViewBoundsUpdate = !_viewportMetrics.physical_width; @@ -1413,10 +1424,10 @@ - (void)viewDidLayoutSubviews { // This must run after updateViewportMetrics so that the surface creation tasks are queued after // the viewport metrics update tasks. - if (firstViewBoundsUpdate && applicationOrSceneIsActive && _engine) { + if (firstViewBoundsUpdate && applicationOrSceneIsActive && self.engine) { [self surfaceUpdated:YES]; - flutter::Shell& shell = [_engine.get() shell]; + flutter::Shell& shell = self.engine.shell; fml::TimeDelta waitTime = #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG fml::TimeDelta::FromMilliseconds(200); @@ -1439,7 +1450,7 @@ - (void)viewSafeAreaInsetsDidChange { // Set _viewportMetrics physical size. - (void)setViewportMetricsSize { - UIScreen* screen = [self flutterScreenIfViewLoaded]; + UIScreen* screen = self.flutterScreenIfViewLoaded; if (!screen) { return; } @@ -1453,7 +1464,7 @@ - (void)setViewportMetricsSize { // // Viewport paddings represent the iOS safe area insets. - (void)setViewportMetricsPaddings { - UIScreen* screen = [self flutterScreenIfViewLoaded]; + UIScreen* screen = self.flutterScreenIfViewLoaded; if (!screen) { return; } @@ -1525,8 +1536,8 @@ - (void)handleKeyboardNotification:(NSNotification*)notification { if (!keyboardAnimationIsCompounding) { [self startKeyBoardAnimation:duration]; - } else if ([self keyboardSpringAnimation]) { - [self keyboardSpringAnimation].toValue = self.targetViewInsetBottom; + } else if (self.keyboardSpringAnimation) { + self.keyboardSpringAnimation.toValue = self.targetViewInsetBottom; } } @@ -1587,11 +1598,7 @@ - (BOOL)isKeyboardNotificationForDifferentView:(NSNotification*)notification { if (isLocal && ![isLocal boolValue]) { return YES; } - // Engine’s viewController is not current viewController. - if ([_engine.get() viewController] != self) { - return YES; - } - return NO; + return self.engine.viewController != self; } - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification { @@ -1619,7 +1626,7 @@ - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification return FlutterKeyboardModeHidden; } - CGRect screenRect = [self flutterScreenIfViewLoaded].bounds; + CGRect screenRect = self.flutterScreenIfViewLoaded.bounds; CGRect adjustedKeyboardFrame = keyboardFrame; adjustedKeyboardFrame.origin.y += [self calculateMultitaskingAdjustment:screenRect keyboardFrame:keyboardFrame]; @@ -1659,7 +1666,7 @@ - (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGR } CGRect viewRectRelativeToScreen = [self.viewIfLoaded convertRect:self.viewIfLoaded.frame - toCoordinateSpace:[self flutterScreenIfViewLoaded].coordinateSpace]; + toCoordinateSpace:self.flutterScreenIfViewLoaded.coordinateSpace]; CGFloat viewBottom = CGRectGetMaxY(viewRectRelativeToScreen); CGFloat offset = screenHeight - viewBottom; if (offset > 0) { @@ -1675,14 +1682,14 @@ - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(NSInteger) // Calculate how much of the keyboard intersects with the view. CGRect viewRectRelativeToScreen = [self.viewIfLoaded convertRect:self.viewIfLoaded.frame - toCoordinateSpace:[self flutterScreenIfViewLoaded].coordinateSpace]; + toCoordinateSpace:self.flutterScreenIfViewLoaded.coordinateSpace]; CGRect intersection = CGRectIntersection(keyboardFrame, viewRectRelativeToScreen); CGFloat portionOfKeyboardInView = CGRectGetHeight(intersection); // The keyboard is treated as an inset since we want to effectively reduce the window size by // the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming // bottom padding. - CGFloat scale = [self flutterScreenIfViewLoaded].scale; + CGFloat scale = self.flutterScreenIfViewLoaded.scale; return portionOfKeyboardInView * scale; } return 0; @@ -1696,21 +1703,21 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { // When this method is called for the first time, // initialize the keyboardAnimationView to get animation interpolation during animation. - if ([self keyboardAnimationView] == nil) { + if (!self.keyboardAnimationView) { UIView* keyboardAnimationView = [[UIView alloc] init]; - [keyboardAnimationView setHidden:YES]; - _keyboardAnimationView.reset(keyboardAnimationView); + keyboardAnimationView.hidden = YES; + self.keyboardAnimationView = keyboardAnimationView; } - if ([self keyboardAnimationView].superview == nil) { - [self.view addSubview:[self keyboardAnimationView]]; + if (!self.keyboardAnimationView.superview) { + [self.view addSubview:self.keyboardAnimationView]; } // Remove running animation when start another animation. - [[self keyboardAnimationView].layer removeAllAnimations]; + [self.keyboardAnimationView.layer removeAllAnimations]; // Set animation begin value and DisplayLink tracking values. - [self keyboardAnimationView].frame = + self.keyboardAnimationView.frame = CGRectMake(0, _viewportMetrics.physical_view_inset_bottom, 0, 0); self.keyboardAnimationStartTime = fml::TimePoint().Now(); self.originalViewInsetBottom = _viewportMetrics.physical_view_inset_bottom; @@ -1718,74 +1725,40 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { // Invalidate old vsync client if old animation is not completed. [self invalidateKeyboardAnimationVSyncClient]; - fml::WeakNSObject weakSelf = [self getWeakNSObject]; - FlutterKeyboardAnimationCallback keyboardAnimationCallback = ^( - fml::TimePoint keyboardAnimationTargetTime) { - if (!weakSelf) { - return; - } - fml::scoped_nsobject flutterViewController( - [(FlutterViewController*)weakSelf.get() retain]); - if (!flutterViewController) { - return; - } - - // If the view controller's view is not loaded, bail out. - if (!flutterViewController.get().isViewLoaded) { - return; - } - // If the view for tracking keyboard animation is nil, means it is not - // created, bail out. - if ([flutterViewController keyboardAnimationView] == nil) { - return; - } - // If keyboardAnimationVSyncClient is nil, means the animation ends. - // And should bail out. - if (flutterViewController.get().keyboardAnimationVSyncClient == nil) { - return; - } - - if ([flutterViewController keyboardAnimationView].superview == nil) { - // Ensure the keyboardAnimationView is in view hierarchy when animation running. - [flutterViewController.get().view addSubview:[flutterViewController keyboardAnimationView]]; - } - - if ([flutterViewController keyboardSpringAnimation] == nil) { - if (flutterViewController.get().keyboardAnimationView.layer.presentationLayer) { - flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = - flutterViewController.get() - .keyboardAnimationView.layer.presentationLayer.frame.origin.y; - [flutterViewController updateViewportMetricsIfNeeded]; - } - } else { - fml::TimeDelta timeElapsed = - keyboardAnimationTargetTime - flutterViewController.get().keyboardAnimationStartTime; - flutterViewController.get()->_viewportMetrics.physical_view_inset_bottom = - [[flutterViewController keyboardSpringAnimation] curveFunction:timeElapsed.ToSecondsF()]; - [flutterViewController updateViewportMetricsIfNeeded]; - } - }; - [self setUpKeyboardAnimationVsyncClient:keyboardAnimationCallback]; + __weak FlutterViewController* weakSelf = self; + [self setUpKeyboardAnimationVsyncClient:^(fml::TimePoint targetTime) { + [weakSelf handleKeyboardAnimationCallbackWithTargetTime:targetTime]; + }]; VSyncClient* currentVsyncClient = _keyboardAnimationVSyncClient; [UIView animateWithDuration:duration animations:^{ + FlutterViewController* strongSelf = weakSelf; + if (!strongSelf) { + return; + } + // Set end value. - [self keyboardAnimationView].frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0); + strongSelf.keyboardAnimationView.frame = CGRectMake(0, self.targetViewInsetBottom, 0, 0); // Setup keyboard animation interpolation. CAAnimation* keyboardAnimation = - [[self keyboardAnimationView].layer animationForKey:@"position"]; - [self setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation]; + [strongSelf.keyboardAnimationView.layer animationForKey:@"position"]; + [strongSelf setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation]; } completion:^(BOOL finished) { if (_keyboardAnimationVSyncClient == currentVsyncClient) { + FlutterViewController* strongSelf = weakSelf; + if (!strongSelf) { + return; + } + // Indicates the vsync client captured by this block is the original one, which also // indicates the animation has not been interrupted from its beginning. Moreover, // indicates the animation is over and there is no more to execute. - [self invalidateKeyboardAnimationVSyncClient]; - [self removeKeyboardAnimationView]; - [self ensureViewportMetricsIsCorrect]; + [strongSelf invalidateKeyboardAnimationVSyncClient]; + [strongSelf removeKeyboardAnimationView]; + [strongSelf ensureViewportMetricsIsCorrect]; } }]; } @@ -1793,19 +1766,54 @@ - (void)startKeyBoardAnimation:(NSTimeInterval)duration { - (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation { // If keyboard animation is null or not a spring animation, fallback to DisplayLink tracking. if (keyboardAnimation == nil || ![keyboardAnimation isKindOfClass:[CASpringAnimation class]]) { - _keyboardSpringAnimation.reset(); + _keyboardSpringAnimation = nil; return; } // Setup keyboard spring animation details for spring curve animation calculation. CASpringAnimation* keyboardCASpringAnimation = (CASpringAnimation*)keyboardAnimation; - _keyboardSpringAnimation.reset([[SpringAnimation alloc] - initWithStiffness:keyboardCASpringAnimation.stiffness - damping:keyboardCASpringAnimation.damping - mass:keyboardCASpringAnimation.mass - initialVelocity:keyboardCASpringAnimation.initialVelocity - fromValue:self.originalViewInsetBottom - toValue:self.targetViewInsetBottom]); + _keyboardSpringAnimation = + [[SpringAnimation alloc] initWithStiffness:keyboardCASpringAnimation.stiffness + damping:keyboardCASpringAnimation.damping + mass:keyboardCASpringAnimation.mass + initialVelocity:keyboardCASpringAnimation.initialVelocity + fromValue:self.originalViewInsetBottom + toValue:self.targetViewInsetBottom]; +} + +- (void)handleKeyboardAnimationCallbackWithTargetTime:(fml::TimePoint)targetTime { + // If the view controller's view is not loaded, bail out. + if (!self.isViewLoaded) { + return; + } + // If the view for tracking keyboard animation is nil, means it is not + // created, bail out. + if (!self.keyboardAnimationView) { + return; + } + // If keyboardAnimationVSyncClient is nil, means the animation ends. + // And should bail out. + if (!self.keyboardAnimationVSyncClient) { + return; + } + + if (!self.keyboardAnimationView.superview) { + // Ensure the keyboardAnimationView is in view hierarchy when animation running. + [self.view addSubview:self.keyboardAnimationView]; + } + + if (!self.keyboardSpringAnimation) { + if (self.keyboardAnimationView.layer.presentationLayer) { + self->_viewportMetrics.physical_view_inset_bottom = + self.keyboardAnimationView.layer.presentationLayer.frame.origin.y; + [self updateViewportMetricsIfNeeded]; + } + } else { + fml::TimeDelta timeElapsed = targetTime - self.keyboardAnimationStartTime; + self->_viewportMetrics.physical_view_inset_bottom = + [self.keyboardSpringAnimation curveFunction:timeElapsed.ToSecondsF()]; + [self updateViewportMetricsIfNeeded]; + } } - (void)setUpKeyboardAnimationVsyncClient: @@ -1817,17 +1825,16 @@ - (void)setUpKeyboardAnimationVsyncClient: @"_keyboardAnimationVSyncClient must be nil when setting up."); // Make sure the new viewport metrics get sent after the begin frame event has processed. - fml::scoped_nsprotocol animationCallback( - [keyboardAnimationCallback copy]); + FlutterKeyboardAnimationCallback animationCallback = [keyboardAnimationCallback copy]; auto uiCallback = [animationCallback](std::unique_ptr recorder) { fml::TimeDelta frameInterval = recorder->GetVsyncTargetTime() - recorder->GetVsyncStartTime(); - fml::TimePoint keyboardAnimationTargetTime = recorder->GetVsyncTargetTime() + frameInterval; + fml::TimePoint targetTime = recorder->GetVsyncTargetTime() + frameInterval; dispatch_async(dispatch_get_main_queue(), ^(void) { - animationCallback.get()(keyboardAnimationTargetTime); + animationCallback(targetTime); }); }; - _keyboardAnimationVSyncClient = [[VSyncClient alloc] initWithTaskRunner:[_engine uiTaskRunner] + _keyboardAnimationVSyncClient = [[VSyncClient alloc] initWithTaskRunner:[self.engine uiTaskRunner] callback:uiCallback]; _keyboardAnimationVSyncClient.allowPauseAfterVsync = NO; [_keyboardAnimationVSyncClient await]; @@ -1835,13 +1842,12 @@ - (void)setUpKeyboardAnimationVsyncClient: - (void)invalidateKeyboardAnimationVSyncClient { [_keyboardAnimationVSyncClient invalidate]; - [_keyboardAnimationVSyncClient release]; _keyboardAnimationVSyncClient = nil; } - (void)removeKeyboardAnimationView { - if ([self keyboardAnimationView].superview != nil) { - [[self keyboardAnimationView] removeFromSuperview]; + if (self.keyboardAnimationView.superview != nil) { + [self.keyboardAnimationView removeFromSuperview]; } } @@ -1864,7 +1870,8 @@ - (void)handlePressEvent:(FlutterUIPressProxy*)press } - (void)sendDeepLinkToFramework:(NSURL*)url completionHandler:(void (^)(BOOL success))completion { - [_engine.get() + __weak FlutterViewController* weakSelf = self; + [self.engine waitForFirstFrame:3.0 callback:^(BOOL didTimeout) { if (didTimeout) { @@ -1872,7 +1879,7 @@ - (void)sendDeepLinkToFramework:(NSURL*)url completionHandler:(void (^)(BOOL suc completion(NO); } else { // invove the method and get the result - [[_engine.get() navigationChannel] + [weakSelf.engine.navigationChannel invokeMethod:@"pushRouteInformation" arguments:@{ @"location" : url.absoluteString ?: [NSNull null], @@ -1899,20 +1906,39 @@ - (void)sendDeepLinkToFramework:(NSURL*)url completionHandler:(void (^)(BOOL suc // the wild, however, so I suspect that the API is built for a tvOS remote or // something, and perhaps only one ever appears in the set on iOS from a // keyboard. +// +// We define separate superPresses* overrides to avoid implicitly capturing self in the blocks +// passed to the presses* methods below. + +- (void)superPressesBegan:(NSSet*)presses withEvent:(UIPressesEvent*)event { + [super pressesBegan:presses withEvent:event]; +} + +- (void)superPressesChanged:(NSSet*)presses withEvent:(UIPressesEvent*)event { + [super pressesChanged:presses withEvent:event]; +} + +- (void)superPressesEnded:(NSSet*)presses withEvent:(UIPressesEvent*)event { + [super pressesEnded:presses withEvent:event]; +} + +- (void)superPressesCancelled:(NSSet*)presses withEvent:(UIPressesEvent*)event { + [super pressesCancelled:presses withEvent:event]; +} // If you substantially change these presses overrides, consider also changing // the similar ones in FlutterTextInputPlugin. They need to be overridden in // both places to capture keys both inside and outside of a text field, but have -// slightly different implmentations. +// slightly different implementations. - (void)pressesBegan:(NSSet*)presses withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { + __weak FlutterViewController* weakSelf = self; for (UIPress* press in presses) { - [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press - withEvent:event] autorelease] + [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] nextAction:^() { - [super pressesBegan:[NSSet setWithObject:press] withEvent:event]; + [weakSelf superPressesBegan:[NSSet setWithObject:press] withEvent:event]; }]; } } else { @@ -1923,11 +1949,11 @@ - (void)pressesBegan:(NSSet*)presses - (void)pressesChanged:(NSSet*)presses withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { + __weak FlutterViewController* weakSelf = self; for (UIPress* press in presses) { - [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press - withEvent:event] autorelease] + [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] nextAction:^() { - [super pressesChanged:[NSSet setWithObject:press] withEvent:event]; + [weakSelf superPressesChanged:[NSSet setWithObject:press] withEvent:event]; }]; } } else { @@ -1938,11 +1964,11 @@ - (void)pressesChanged:(NSSet*)presses - (void)pressesEnded:(NSSet*)presses withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { + __weak FlutterViewController* weakSelf = self; for (UIPress* press in presses) { - [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press - withEvent:event] autorelease] + [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] nextAction:^() { - [super pressesEnded:[NSSet setWithObject:press] withEvent:event]; + [weakSelf superPressesEnded:[NSSet setWithObject:press] withEvent:event]; }]; } } else { @@ -1953,11 +1979,11 @@ - (void)pressesEnded:(NSSet*)presses - (void)pressesCancelled:(NSSet*)presses withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) { if (@available(iOS 13.4, *)) { + __weak FlutterViewController* weakSelf = self; for (UIPress* press in presses) { - [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press - withEvent:event] autorelease] + [self handlePressEvent:[[FlutterUIPressProxy alloc] initWithPress:press withEvent:event] nextAction:^() { - [super pressesCancelled:[NSSet setWithObject:press] withEvent:event]; + [weakSelf superPressesCancelled:[NSSet setWithObject:press] withEvent:event]; }]; } } else { @@ -1969,15 +1995,14 @@ - (void)pressesCancelled:(NSSet*)presses - (void)onOrientationPreferencesUpdated:(NSNotification*)notification { // Notifications may not be on the iOS UI thread + __weak FlutterViewController* weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ NSDictionary* info = notification.userInfo; - NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)]; - if (update == nil) { return; } - [self performOrientationUpdate:update.unsignedIntegerValue]; + [weakSelf performOrientationUpdate:update.unsignedIntegerValue]; }); } @@ -1985,8 +2010,8 @@ - (void)requestGeometryUpdateForWindowScenes:(NSSet*)windowScenes API_AVAILABLE(ios(16.0)) { for (UIScene* windowScene in windowScenes) { FML_DCHECK([windowScene isKindOfClass:[UIWindowScene class]]); - UIWindowSceneGeometryPreferencesIOS* preference = [[[UIWindowSceneGeometryPreferencesIOS alloc] - initWithInterfaceOrientations:_orientationPreferences] autorelease]; + UIWindowSceneGeometryPreferencesIOS* preference = [[UIWindowSceneGeometryPreferencesIOS alloc] + initWithInterfaceOrientations:self.orientationPreferences]; [(UIWindowScene*)windowScene requestGeometryUpdateWithPreferences:preference errorHandler:^(NSError* error) { @@ -1998,8 +2023,8 @@ - (void)requestGeometryUpdateForWindowScenes:(NSSet*)windowScenes } - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences { - if (new_preferences != _orientationPreferences) { - _orientationPreferences = new_preferences; + if (new_preferences != self.orientationPreferences) { + self.orientationPreferences = new_preferences; if (@available(iOS 16.0, *)) { NSSet* scenes = @@ -2018,7 +2043,7 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences { } else { UIInterfaceOrientationMask currentInterfaceOrientation = 0; if (@available(iOS 13.0, *)) { - UIWindowScene* windowScene = [self flutterWindowSceneIfViewLoaded]; + UIWindowScene* windowScene = self.flutterWindowSceneIfViewLoaded; if (!windowScene) { FML_LOG(WARNING) << "Accessing the interface orientation when the window scene is unavailable."; @@ -2034,22 +2059,22 @@ - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences { currentInterfaceOrientation = 1 << [[UIApplication sharedApplication] statusBarOrientation]; #endif } - if (!(_orientationPreferences & currentInterfaceOrientation)) { + if (!(self.orientationPreferences & currentInterfaceOrientation)) { [UIViewController attemptRotationToDeviceOrientation]; // Force orientation switch if the current orientation is not allowed - if (_orientationPreferences & UIInterfaceOrientationMaskPortrait) { + if (self.orientationPreferences & UIInterfaceOrientationMaskPortrait) { // This is no official API but more like a workaround / hack (using // key-value coding on a read-only property). This might break in // the future, but currently it´s the only way to force an orientation change [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait) forKey:@"orientation"]; - } else if (_orientationPreferences & UIInterfaceOrientationMaskPortraitUpsideDown) { + } else if (self.orientationPreferences & UIInterfaceOrientationMaskPortraitUpsideDown) { [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortraitUpsideDown) forKey:@"orientation"]; - } else if (_orientationPreferences & UIInterfaceOrientationMaskLandscapeLeft) { + } else if (self.orientationPreferences & UIInterfaceOrientationMaskLandscapeLeft) { [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeLeft) forKey:@"orientation"]; - } else if (_orientationPreferences & UIInterfaceOrientationMaskLandscapeRight) { + } else if (self.orientationPreferences & UIInterfaceOrientationMaskLandscapeRight) { [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeRight) forKey:@"orientation"]; } @@ -2082,17 +2107,17 @@ - (BOOL)shouldAutorotate { } - (NSUInteger)supportedInterfaceOrientations { - return _orientationPreferences; + return self.orientationPreferences; } #pragma mark - Accessibility - (void)onAccessibilityStatusChanged:(NSNotification*)notification { - if (!_engine) { + if (!self.engine) { return; } - auto platformView = [_engine.get() platformView]; - int32_t flags = [self accessibilityFlags]; + fml::WeakPtr platformView = self.engine.platformView; + int32_t flags = self.accessibilityFlags; #if TARGET_OS_SIMULATOR // There doesn't appear to be any way to determine whether the accessibility // inspector is enabled on the simulator. We conservatively always turn on the @@ -2132,7 +2157,7 @@ - (int32_t)accessibilityFlags { } - (BOOL)accessibilityPerformEscape { - FlutterMethodChannel* navigationChannel = [_engine.get() navigationChannel]; + FlutterMethodChannel* navigationChannel = self.engine.navigationChannel; if (navigationChannel) { [self popRoute]; return YES; @@ -2156,13 +2181,13 @@ - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { } - (void)onUserSettingsChanged:(NSNotification*)notification { - [[_engine.get() settingsChannel] sendMessage:@{ - @"textScaleFactor" : @([self textScaleFactor]), - @"alwaysUse24HourFormat" : @([FlutterHourFormat isAlwaysUse24HourFormat]), - @"platformBrightness" : [self brightnessMode], - @"platformContrast" : [self contrastMode], - @"nativeSpellCheckServiceDefined" : @true, - @"supportsShowingSystemContextMenu" : @([self supportsShowingSystemContextMenu]) + [self.engine.settingsChannel sendMessage:@{ + @"textScaleFactor" : @(self.textScaleFactor), + @"alwaysUse24HourFormat" : @(FlutterHourFormat.isAlwaysUse24HourFormat), + @"platformBrightness" : self.brightnessMode, + @"platformContrast" : self.contrastMode, + @"nativeSpellCheckServiceDefined" : @YES, + @"supportsShowingSystemContextMenu" : @(self.supportsShowingSystemContextMenu) }]; } @@ -2269,25 +2294,28 @@ - (NSString*)contrastMode { #pragma mark - Status bar style - (UIStatusBarStyle)preferredStatusBarStyle { - return _statusBarStyle; + return self.statusBarStyle; } - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification { // Notifications may not be on the iOS UI thread + __weak FlutterViewController* weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ - NSDictionary* info = notification.userInfo; + FlutterViewController* strongSelf = weakSelf; + if (!strongSelf) { + return; + } + NSDictionary* info = notification.userInfo; NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)]; - if (update == nil) { return; } - NSInteger style = update.integerValue; - - if (style != _statusBarStyle) { - _statusBarStyle = static_cast(style); - [self setNeedsStatusBarAppearanceUpdate]; + UIStatusBarStyle style = static_cast(update.integerValue); + if (style != strongSelf.statusBarStyle) { + strongSelf.statusBarStyle = style; + [strongSelf setNeedsStatusBarAppearanceUpdate]; } }); } @@ -2306,28 +2334,28 @@ - (BOOL)prefersStatusBarHidden { #pragma mark - Platform views - (std::shared_ptr&)platformViewsController { - return [_engine.get() platformViewsController]; + return self.engine.platformViewsController; } - (NSObject*)binaryMessenger { - return _engine.get().binaryMessenger; + return self.engine.binaryMessenger; } #pragma mark - FlutterBinaryMessenger - (void)sendOnChannel:(NSString*)channel message:(NSData*)message { - [_engine.get().binaryMessenger sendOnChannel:channel message:message]; + [self.engine.binaryMessenger sendOnChannel:channel message:message]; } - (void)sendOnChannel:(NSString*)channel message:(NSData*)message binaryReply:(FlutterBinaryReply)callback { NSAssert(channel, @"The channel must not be null"); - [_engine.get().binaryMessenger sendOnChannel:channel message:message binaryReply:callback]; + [self.engine.binaryMessenger sendOnChannel:channel message:message binaryReply:callback]; } - (NSObject*)makeBackgroundTaskQueue { - return [_engine.get().binaryMessenger makeBackgroundTaskQueue]; + return [self.engine.binaryMessenger makeBackgroundTaskQueue]; } - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channel @@ -2341,27 +2369,27 @@ - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channe binaryMessageHandler:(FlutterBinaryMessageHandler _Nullable)handler taskQueue:(NSObject* _Nullable)taskQueue { NSAssert(channel, @"The channel must not be null"); - return [_engine.get().binaryMessenger setMessageHandlerOnChannel:channel - binaryMessageHandler:handler - taskQueue:taskQueue]; + return [self.engine.binaryMessenger setMessageHandlerOnChannel:channel + binaryMessageHandler:handler + taskQueue:taskQueue]; } - (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection { - [_engine.get().binaryMessenger cleanUpConnection:connection]; + [self.engine.binaryMessenger cleanUpConnection:connection]; } #pragma mark - FlutterTextureRegistry - (int64_t)registerTexture:(NSObject*)texture { - return [_engine.get().textureRegistry registerTexture:texture]; + return [self.engine.textureRegistry registerTexture:texture]; } - (void)unregisterTexture:(int64_t)textureId { - [_engine.get().textureRegistry unregisterTexture:textureId]; + [self.engine.textureRegistry unregisterTexture:textureId]; } - (void)textureFrameAvailable:(int64_t)textureId { - [_engine.get().textureRegistry textureFrameAvailable:textureId]; + [self.engine.textureRegistry textureFrameAvailable:textureId]; } - (NSString*)lookupKeyForAsset:(NSString*)asset { @@ -2373,7 +2401,7 @@ - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package { } - (id)pluginRegistry { - return _engine; + return self.engine; } + (BOOL)isUIAccessibilityIsVoiceOverRunning { @@ -2383,25 +2411,26 @@ + (BOOL)isUIAccessibilityIsVoiceOverRunning { #pragma mark - FlutterPluginRegistry - (NSObject*)registrarForPlugin:(NSString*)pluginKey { - return [_engine.get() registrarForPlugin:pluginKey]; + return [self.engine registrarForPlugin:pluginKey]; } - (BOOL)hasPlugin:(NSString*)pluginKey { - return [_engine.get() hasPlugin:pluginKey]; + return [self.engine hasPlugin:pluginKey]; } - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey { - return [_engine.get() valuePublishedByPlugin:pluginKey]; + return [self.engine valuePublishedByPlugin:pluginKey]; } - (void)presentViewController:(UIViewController*)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion { self.isPresentingViewControllerAnimating = YES; + __weak FlutterViewController* weakSelf = self; [super presentViewController:viewControllerToPresent animated:flag completion:^{ - self.isPresentingViewControllerAnimating = NO; + weakSelf.isPresentingViewControllerAnimating = NO; if (completion) { completion(); } @@ -2415,7 +2444,7 @@ - (BOOL)isPresentingViewController { - (flutter::PointerData)updateMousePointerDataFrom:(UIGestureRecognizer*)gestureRecognizer API_AVAILABLE(ios(13.4)) { CGPoint location = [gestureRecognizer locationInView:self.view]; - CGFloat scale = [self flutterScreenIfViewLoaded].scale; + CGFloat scale = self.flutterScreenIfViewLoaded.scale; _mouseState.location = {location.x * scale, location.y * scale}; flutter::PointerData pointer_data; pointer_data.Clear(); @@ -2442,13 +2471,13 @@ - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer pointer_data.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel; pointer_data.view_id = self.viewIdentifier; - if (event.timestamp < _scrollInertiaEventAppKitDeadline) { + if (event.timestamp < self.scrollInertiaEventAppKitDeadline) { // Only send the event if it occured before the expected natural end of gesture momentum. // If received after the deadline, it's not likely the event is from a user-initiated cancel. auto packet = std::make_unique(1); packet->SetPointerData(/*i=*/0, pointer_data); - [_engine.get() dispatchPointerDataPacket:std::move(packet)]; - _scrollInertiaEventAppKitDeadline = 0; + [self.engine dispatchPointerDataPacket:std::move(packet)]; + self.scrollInertiaEventAppKitDeadline = 0; } } // This method is also called for UITouches, should return YES to process all touches. @@ -2490,7 +2519,7 @@ - (void)hoverEvent:(UIHoverGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4) isRunningOnMac = [NSProcessInfo processInfo].iOSAppOnMac; } if (!isRunningOnMac && CGPointEqualToPoint(oldLocation, _mouseState.location) && - time > _scrollInertiaEventStartline) { + time > self.scrollInertiaEventStartline) { // iPadOS reports trackpad movements events with high (sub-pixel) precision. When an event // is received with the same position as the previous one, it can only be from a finger // making or breaking contact with the trackpad surface. @@ -2502,18 +2531,18 @@ - (void)hoverEvent:(UIHoverGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4) inertia_cancel.signal_kind = flutter::PointerData::SignalKind::kScrollInertiaCancel; inertia_cancel.view_id = self.viewIdentifier; packet->SetPointerData(/*i=*/1, inertia_cancel); - [_engine.get() dispatchPointerDataPacket:std::move(packet)]; - _scrollInertiaEventStartline = DBL_MAX; + [self.engine dispatchPointerDataPacket:std::move(packet)]; + self.scrollInertiaEventStartline = DBL_MAX; } else { auto packet = std::make_unique(1); packet->SetPointerData(/*i=*/0, pointer_data); - [_engine.get() dispatchPointerDataPacket:std::move(packet)]; + [self.engine dispatchPointerDataPacket:std::move(packet)]; } } - (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) { CGPoint translation = [recognizer translationInView:self.view]; - const CGFloat scale = [self flutterScreenIfViewLoaded].scale; + const CGFloat scale = self.flutterScreenIfViewLoaded.scale; translation.x *= scale; translation.y *= scale; @@ -2538,12 +2567,12 @@ - (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(io auto packet = std::make_unique(1); packet->SetPointerData(/*i=*/0, pointer_data); - [_engine.get() dispatchPointerDataPacket:std::move(packet)]; + [self.engine dispatchPointerDataPacket:std::move(packet)]; } - (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) { CGPoint translation = [recognizer translationInView:self.view]; - const CGFloat scale = [self flutterScreenIfViewLoaded].scale; + const CGFloat scale = self.flutterScreenIfViewLoaded.scale; flutter::PointerData pointer_data = [self updateMousePointerDataFrom:recognizer]; pointer_data.device = reinterpret_cast(recognizer); @@ -2563,7 +2592,7 @@ - (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE( break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: - _scrollInertiaEventStartline = + self.scrollInertiaEventStartline = [[NSProcessInfo processInfo] systemUptime] + 0.1; // Time to lift fingers off trackpad (experimentally determined) // When running an iOS app on an Apple Silicon Mac, AppKit will send an event @@ -2574,7 +2603,7 @@ - (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE( // The following (curve-fitted) calculation provides a cutoff point after which any // UIEventTypeScroll event will likely be from the system instead of the user. // See https://github.com/flutter/engine/pull/34929. - _scrollInertiaEventAppKitDeadline = + self.scrollInertiaEventAppKitDeadline = [[NSProcessInfo processInfo] systemUptime] + (0.1821 * log(fmax([recognizer velocityInView:self.view].x, [recognizer velocityInView:self.view].y))) - @@ -2583,14 +2612,14 @@ - (void)continuousScrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE( break; default: // continuousScrollEvent: should only ever be triggered with the above phases - NSAssert(false, @"Trackpad pan event occured with unexpected phase 0x%lx", + NSAssert(NO, @"Trackpad pan event occured with unexpected phase 0x%lx", (long)recognizer.state); break; } auto packet = std::make_unique(1); packet->SetPointerData(/*i=*/0, pointer_data); - [_engine.get() dispatchPointerDataPacket:std::move(packet)]; + [self.engine dispatchPointerDataPacket:std::move(packet)]; } - (void)pinchEvent:(UIPinchGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) { @@ -2613,20 +2642,20 @@ - (void)pinchEvent:(UIPinchGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4) break; default: // pinchEvent: should only ever be triggered with the above phases - NSAssert(false, @"Trackpad pinch event occured with unexpected phase 0x%lx", + NSAssert(NO, @"Trackpad pinch event occured with unexpected phase 0x%lx", (long)recognizer.state); break; } auto packet = std::make_unique(1); packet->SetPointerData(/*i=*/0, pointer_data); - [_engine.get() dispatchPointerDataPacket:std::move(packet)]; + [self.engine dispatchPointerDataPacket:std::move(packet)]; } #pragma mark - State Restoration - (void)encodeRestorableStateWithCoder:(NSCoder*)coder { - NSData* restorationData = [[_engine.get() restorationPlugin] restorationData]; + NSData* restorationData = [self.engine.restorationPlugin restorationData]; [coder encodeBytes:(const unsigned char*)restorationData.bytes length:restorationData.length forKey:kFlutterRestorationStateAppData]; @@ -2638,11 +2667,11 @@ - (void)decodeRestorableStateWithCoder:(NSCoder*)coder { const unsigned char* restorationBytes = [coder decodeBytesForKey:kFlutterRestorationStateAppData returnedLength:&restorationDataLength]; NSData* restorationData = [NSData dataWithBytes:restorationBytes length:restorationDataLength]; - [[_engine.get() restorationPlugin] setRestorationData:restorationData]; + [self.engine.restorationPlugin setRestorationData:restorationData]; } - (FlutterRestorationPlugin*)restorationPlugin { - return [_engine.get() restorationPlugin]; + return self.engine.restorationPlugin; } @end diff --git a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h index c41533ff9c041..db9fc37bfd75f 100644 --- a/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h +++ b/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h @@ -46,7 +46,7 @@ typedef void (^FlutterKeyboardAnimationCallback)(fml::TimePoint); @property(class, nonatomic, readonly) BOOL accessibilityIsOnOffSwitchLabelsEnabled; @property(nonatomic, readonly) BOOL isPresentingViewController; @property(nonatomic, readonly) BOOL isVoiceOverRunning; -@property(nonatomic, retain) FlutterKeyboardManager* keyboardManager; +@property(nonatomic, strong) FlutterKeyboardManager* keyboardManager; /** * @brief Whether the status bar is preferred hidden.