Flutter Engine
FlutterViewController.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #define FML_USED_ON_EMBEDDER
6 
7 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
8 
9 #include <memory>
10 
11 #include "flutter/fml/memory/weak_ptr.h"
12 #include "flutter/fml/message_loop.h"
13 #include "flutter/fml/platform/darwin/platform_version.h"
14 #include "flutter/fml/platform/darwin/scoped_nsobject.h"
15 #include "flutter/runtime/ptrace_check.h"
16 #include "flutter/shell/common/thread_host.h"
17 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h"
18 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterChannelKeyResponder.h"
19 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.h"
20 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
21 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyPrimaryResponder.h"
22 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterKeyboardManager.h"
23 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
24 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformViews_Internal.h"
25 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"
26 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
27 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h"
28 #import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h"
29 #import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
30 #import "flutter/shell/platform/embedder/embedder.h"
31 
32 static constexpr int kMicrosecondsPerSecond = 1000 * 1000;
33 static constexpr CGFloat kScrollViewContentSize = 2.0;
34 
35 NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate";
36 NSNotificationName const FlutterViewControllerWillDealloc = @"FlutterViewControllerWillDealloc";
37 NSNotificationName const FlutterViewControllerHideHomeIndicator =
38  @"FlutterViewControllerHideHomeIndicator";
39 NSNotificationName const FlutterViewControllerShowHomeIndicator =
40  @"FlutterViewControllerShowHomeIndicator";
41 
42 // Struct holding the mouse state. The engine doesn't keep track of which
43 // mouse buttons have been pressed, so it's the embedding's responsibility.
44 typedef struct MouseState {
45  // True if the last event sent to Flutter had at least one mouse button.
46  // pressed.
47  bool flutter_state_is_down = false;
48 
49  // True if kAdd has been sent to Flutter. Used to determine whether
50  // to send a kAdd event before sending an incoming mouse event, since
51  // Flutter expects pointers to be added before events are sent for them.
52  bool flutter_state_is_added = false;
53 
54  // Current coordinate of the mouse cursor in physical device pixels.
55  CGPoint location = CGPointZero;
56 
57  // Last reported translation for an in-flight pan gesture in physical device pixels.
58  CGPoint last_translation = CGPointZero;
59 
60  // The currently pressed buttons, as represented in FlutterPointerEvent.
61  uint64_t buttons = 0;
62 } MouseState;
63 
64 // This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the
65 // change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are
66 // just a warning.
67 @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegate>
68 @property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
69 @property(nonatomic, assign) BOOL isHomeIndicatorHidden;
70 @property(nonatomic, assign) BOOL isPresentingViewControllerAnimating;
71 
72 /**
73  * Creates and registers plugins used by this view controller.
74  */
75 - (void)addInternalPlugins;
76 - (void)deregisterNotifications;
77 @end
78 
79 // The following conditional compilation defines an API 13 concept on earlier API targets so that
80 // a compiler compiling against API 12 or below does not blow up due to non-existent members.
81 #if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000
82 typedef enum UIAccessibilityContrast : NSInteger {
87 
90 @end
91 #endif
92 
93 @implementation FlutterViewController {
94  std::unique_ptr<fml::WeakPtrFactory<FlutterViewController>> _weakFactory;
96 
97  // We keep a separate reference to this and create it ahead of time because we want to be able to
98  // set up a shell along with its platform view before the view has to appear.
102  UIInterfaceOrientationMask _orientationPreferences;
103  UIStatusBarStyle _statusBarStyle;
109  // This scroll view is a workaround to accommodate iOS 13 and higher. There isn't a way to get
110  // touches on the status bar to trigger scrolling to the top of a scroll view. We place a
111  // UIScrollView with height zero and a content offset so we can get those events. See also:
112  // https://github.com/flutter/flutter/issues/35050
114  fml::scoped_nsobject<UIPointerInteraction> _pointerInteraction API_AVAILABLE(ios(13.4));
115  fml::scoped_nsobject<UIPanGestureRecognizer> _panGestureRecognizer API_AVAILABLE(ios(13.4));
117 }
118 
119 @synthesize displayingFlutterUI = _displayingFlutterUI;
120 
121 #pragma mark - Manage and override all designated initializers
122 
123 - (instancetype)initWithEngine:(FlutterEngine*)engine
124  nibName:(nullable NSString*)nibName
125  bundle:(nullable NSBundle*)nibBundle {
126  NSAssert(engine != nil, @"Engine is required");
127  self = [super initWithNibName:nibName bundle:nibBundle];
128  if (self) {
129  _viewOpaque = YES;
130  if (engine.viewController) {
131  FML_LOG(ERROR) << "The supplied FlutterEngine " << [[engine description] UTF8String]
132  << " is already used with FlutterViewController instance "
133  << [[engine.viewController description] UTF8String]
134  << ". One instance of the FlutterEngine can only be attached to one "
135  "FlutterViewController at a time. Set FlutterEngine.viewController "
136  "to nil before attaching it to another FlutterViewController.";
137  }
138  _engine.reset([engine retain]);
139  _engineNeedsLaunch = NO;
140  _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
141  _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
142  _ongoingTouches.reset([[NSMutableSet alloc] init]);
143 
144  [self performCommonViewControllerInitialization];
145  [engine setViewController:self];
146  }
147 
148  return self;
149 }
150 
151 - (instancetype)initWithProject:(FlutterDartProject*)project
152  nibName:(NSString*)nibName
153  bundle:(NSBundle*)nibBundle {
154  self = [super initWithNibName:nibName bundle:nibBundle];
155  if (self) {
156  [self sharedSetupWithProject:project initialRoute:nil];
157  }
158 
159  return self;
160 }
161 
162 - (instancetype)initWithProject:(FlutterDartProject*)project
163  initialRoute:(NSString*)initialRoute
164  nibName:(NSString*)nibName
165  bundle:(NSBundle*)nibBundle {
166  self = [super initWithNibName:nibName bundle:nibBundle];
167  if (self) {
168  [self sharedSetupWithProject:project initialRoute:initialRoute];
169  }
170 
171  return self;
172 }
173 
174 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
175  return [self initWithProject:nil nibName:nil bundle:nil];
176 }
177 
178 - (instancetype)initWithCoder:(NSCoder*)aDecoder {
179  self = [super initWithCoder:aDecoder];
180  return self;
181 }
182 
183 - (void)awakeFromNib {
184  [super awakeFromNib];
185  if (!_engine) {
186  [self sharedSetupWithProject:nil initialRoute:nil];
187  }
188 }
189 
190 - (instancetype)init {
191  return [self initWithProject:nil nibName:nil bundle:nil];
192 }
193 
194 - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
195  initialRoute:(nullable NSString*)initialRoute {
196  // Need the project to get settings for the view. Initializing it here means
197  // the Engine class won't initialize it later.
198  if (!project) {
199  project = [[[FlutterDartProject alloc] init] autorelease];
200  }
201  FlutterView.forceSoftwareRendering = project.settings.enable_software_rendering;
203  initWithName:@"io.flutter"
204  project:project
205  allowHeadlessExecution:self.engineAllowHeadlessExecution
206  restorationEnabled:[self restorationIdentifier] != nil]};
207 
208  if (!engine) {
209  return;
210  }
211 
212  _viewOpaque = YES;
213  _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
214  _engine = std::move(engine);
215  _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
216  [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute];
217  _engineNeedsLaunch = YES;
218  _ongoingTouches.reset([[NSMutableSet alloc] init]);
220  [self performCommonViewControllerInitialization];
221 }
222 
223 - (BOOL)isViewOpaque {
224  return _viewOpaque;
225 }
226 
227 - (void)setViewOpaque:(BOOL)value {
228  _viewOpaque = value;
229  if (_flutterView.get().layer.opaque != value) {
230  _flutterView.get().layer.opaque = value;
231  [_flutterView.get().layer setNeedsLayout];
232  }
233 }
234 
235 #pragma mark - Common view controller initialization tasks
236 
237 - (void)performCommonViewControllerInitialization {
238  if (_initialized)
239  return;
240 
241  _initialized = YES;
242 
243  _orientationPreferences = UIInterfaceOrientationMaskAll;
244  _statusBarStyle = UIStatusBarStyleDefault;
245 
246  [self setupNotificationCenterObservers];
247 }
248 
249 - (FlutterEngine*)engine {
250  return _engine.get();
251 }
252 
254  return _weakFactory->GetWeakPtr();
255 }
256 
257 - (void)setupNotificationCenterObservers {
258  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
259  [center addObserver:self
260  selector:@selector(onOrientationPreferencesUpdated:)
261  name:@(flutter::kOrientationUpdateNotificationName)
262  object:nil];
263 
264  [center addObserver:self
265  selector:@selector(onPreferredStatusBarStyleUpdated:)
266  name:@(flutter::kOverlayStyleUpdateNotificationName)
267  object:nil];
268 
269  [center addObserver:self
270  selector:@selector(applicationBecameActive:)
271  name:UIApplicationDidBecomeActiveNotification
272  object:nil];
273 
274  [center addObserver:self
275  selector:@selector(applicationWillResignActive:)
276  name:UIApplicationWillResignActiveNotification
277  object:nil];
278 
279  [center addObserver:self
280  selector:@selector(applicationDidEnterBackground:)
281  name:UIApplicationDidEnterBackgroundNotification
282  object:nil];
283 
284  [center addObserver:self
285  selector:@selector(applicationWillEnterForeground:)
286  name:UIApplicationWillEnterForegroundNotification
287  object:nil];
288 
289  [center addObserver:self
290  selector:@selector(keyboardWillChangeFrame:)
291  name:UIKeyboardWillChangeFrameNotification
292  object:nil];
293 
294  [center addObserver:self
295  selector:@selector(keyboardWillBeHidden:)
296  name:UIKeyboardWillHideNotification
297  object:nil];
298 
299  [center addObserver:self
300  selector:@selector(onAccessibilityStatusChanged:)
301  name:UIAccessibilityVoiceOverStatusChanged
302  object:nil];
303 
304  [center addObserver:self
305  selector:@selector(onAccessibilityStatusChanged:)
306  name:UIAccessibilitySwitchControlStatusDidChangeNotification
307  object:nil];
308 
309  [center addObserver:self
310  selector:@selector(onAccessibilityStatusChanged:)
311  name:UIAccessibilitySpeakScreenStatusDidChangeNotification
312  object:nil];
313 
314  [center addObserver:self
315  selector:@selector(onAccessibilityStatusChanged:)
316  name:UIAccessibilityInvertColorsStatusDidChangeNotification
317  object:nil];
318 
319  [center addObserver:self
320  selector:@selector(onAccessibilityStatusChanged:)
321  name:UIAccessibilityReduceMotionStatusDidChangeNotification
322  object:nil];
323 
324  [center addObserver:self
325  selector:@selector(onAccessibilityStatusChanged:)
326  name:UIAccessibilityBoldTextStatusDidChangeNotification
327  object:nil];
328 
329  [center addObserver:self
330  selector:@selector(onAccessibilityStatusChanged:)
331  name:UIAccessibilityDarkerSystemColorsStatusDidChangeNotification
332  object:nil];
333 
334  [center addObserver:self
335  selector:@selector(onUserSettingsChanged:)
336  name:UIContentSizeCategoryDidChangeNotification
337  object:nil];
338 
339  [center addObserver:self
340  selector:@selector(onHideHomeIndicatorNotification:)
341  name:FlutterViewControllerHideHomeIndicator
342  object:nil];
343 
344  [center addObserver:self
345  selector:@selector(onShowHomeIndicatorNotification:)
346  name:FlutterViewControllerShowHomeIndicator
347  object:nil];
348 }
349 
350 - (void)setInitialRoute:(NSString*)route {
351  [[_engine.get() navigationChannel] invokeMethod:@"setInitialRoute" arguments:route];
352 }
353 
354 - (void)popRoute {
355  [[_engine.get() navigationChannel] invokeMethod:@"popRoute" arguments:nil];
356 }
357 
358 - (void)pushRoute:(NSString*)route {
359  [[_engine.get() navigationChannel] invokeMethod:@"pushRoute" arguments:route];
360 }
361 
362 #pragma mark - Loading the view
363 
364 static UIView* GetViewOrPlaceholder(UIView* existing_view) {
365  if (existing_view) {
366  return existing_view;
367  }
368 
369  auto placeholder = [[[UIView alloc] init] autorelease];
370 
371  placeholder.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
372  if (@available(iOS 13.0, *)) {
373  placeholder.backgroundColor = UIColor.systemBackgroundColor;
374  } else {
375  placeholder.backgroundColor = UIColor.whiteColor;
376  }
377  placeholder.autoresizesSubviews = YES;
378 
379  // Only add the label when we know we have failed to enable tracing (and it was necessary).
380  // Otherwise, a spurious warning will be shown in cases where an engine cannot be initialized for
381  // other reasons.
383  auto messageLabel = [[[UILabel alloc] init] autorelease];
384  messageLabel.numberOfLines = 0u;
385  messageLabel.textAlignment = NSTextAlignmentCenter;
386  messageLabel.autoresizingMask =
387  UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
388  messageLabel.text =
389  @"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling, "
390  @"IDEs with Flutter plugins or from Xcode.\n\nAlternatively, build in profile or release "
391  @"modes to enable launching from the home screen.";
392  [placeholder addSubview:messageLabel];
393  }
394 
395  return placeholder;
396 }
397 
398 - (void)loadView {
399  self.view = GetViewOrPlaceholder(_flutterView.get());
400  self.view.multipleTouchEnabled = YES;
401  self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
402 
403  [self installSplashScreenViewIfNecessary];
404  UIScrollView* scrollView = [[UIScrollView alloc] init];
405  scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
406  // The color shouldn't matter since it is offscreen.
407  scrollView.backgroundColor = UIColor.whiteColor;
408  scrollView.delegate = self;
409  // This is an arbitrary small size.
410  scrollView.contentSize = CGSizeMake(kScrollViewContentSize, kScrollViewContentSize);
411  // This is an arbitrary offset that is not CGPointZero.
412  scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
413  [self.view addSubview:scrollView];
414  _scrollView.reset(scrollView);
415 }
416 
417 static void sendFakeTouchEvent(FlutterEngine* engine,
418  CGPoint location,
420  const CGFloat scale = [UIScreen mainScreen].scale;
421  flutter::PointerData pointer_data;
422  pointer_data.Clear();
423  pointer_data.physical_x = location.x * scale;
424  pointer_data.physical_y = location.y * scale;
426  pointer_data.time_stamp = [[NSDate date] timeIntervalSince1970] * kMicrosecondsPerSecond;
427  auto packet = std::make_unique<flutter::PointerDataPacket>(/*count=*/1);
428  pointer_data.change = change;
429  packet->SetPointerData(0, pointer_data);
430  [engine dispatchPointerDataPacket:std::move(packet)];
431 }
432 
433 - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
434  if (!_engine) {
435  return NO;
436  }
437  CGPoint statusBarPoint = CGPointZero;
438  sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown);
439  sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kUp);
440  return NO;
441 }
442 
443 #pragma mark - Managing launch views
444 
445 - (void)installSplashScreenViewIfNecessary {
446  // Show the launch screen view again on top of the FlutterView if available.
447  // This launch screen view will be removed once the first Flutter frame is rendered.
448  if (_splashScreenView && (self.isBeingPresented || self.isMovingToParentViewController)) {
449  [_splashScreenView.get() removeFromSuperview];
451  return;
452  }
453 
454  // Use the property getter to initialize the default value.
455  UIView* splashScreenView = self.splashScreenView;
456  if (splashScreenView == nil) {
457  return;
458  }
459  splashScreenView.frame = self.view.bounds;
460  [self.view addSubview:splashScreenView];
461 }
462 
463 + (BOOL)automaticallyNotifiesObserversOfDisplayingFlutterUI {
464  return NO;
465 }
466 
467 - (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI {
468  if (_displayingFlutterUI != displayingFlutterUI) {
469  if (displayingFlutterUI == YES) {
470  if (!self.isViewLoaded || !self.view.window) {
471  return;
472  }
473  }
474  [self willChangeValueForKey:@"displayingFlutterUI"];
475  _displayingFlutterUI = displayingFlutterUI;
476  [self didChangeValueForKey:@"displayingFlutterUI"];
477  }
478 }
479 
480 - (void)callViewRenderedCallback {
481  self.displayingFlutterUI = YES;
482  if (_flutterViewRenderedCallback != nil) {
485  }
486 }
487 
488 - (void)removeSplashScreenView:(dispatch_block_t _Nullable)onComplete {
489  NSAssert(_splashScreenView, @"The splash screen view must not be null");
490  UIView* splashScreen = _splashScreenView.get();
492  [UIView animateWithDuration:0.2
493  animations:^{
494  splashScreen.alpha = 0;
495  }
496  completion:^(BOOL finished) {
497  [splashScreen removeFromSuperview];
498  if (onComplete) {
499  onComplete();
500  }
501  }];
502 }
503 
504 - (void)installFirstFrameCallback {
505  if (!_engine) {
506  return;
507  }
508 
509  fml::WeakPtr<flutter::PlatformViewIOS> weakPlatformView = [_engine.get() platformView];
510  if (!weakPlatformView) {
511  return;
512  }
513 
514  // Start on the platform thread.
515  weakPlatformView->SetNextFrameCallback([weakSelf = [self getWeakPtr],
516  platformTaskRunner = [_engine.get() platformTaskRunner],
517  RasterTaskRunner = [_engine.get() RasterTaskRunner]]() {
518  FML_DCHECK(RasterTaskRunner->RunsTasksOnCurrentThread());
519  // Get callback on raster thread and jump back to platform thread.
520  platformTaskRunner->PostTask([weakSelf]() {
521  fml::scoped_nsobject<FlutterViewController> flutterViewController(
522  [(FlutterViewController*)weakSelf.get() retain]);
523  if (flutterViewController) {
524  if (flutterViewController.get()->_splashScreenView) {
525  [flutterViewController removeSplashScreenView:^{
526  [flutterViewController callViewRenderedCallback];
527  }];
528  } else {
529  [flutterViewController callViewRenderedCallback];
530  }
531  }
532  });
533  });
534 }
535 
536 #pragma mark - Properties
537 
538 - (UIView*)splashScreenView {
539  if (!_splashScreenView) {
540  return nil;
541  }
542  return _splashScreenView.get();
543 }
544 
545 - (BOOL)loadDefaultSplashScreenView {
546  NSString* launchscreenName =
547  [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"];
548  if (launchscreenName == nil) {
549  return NO;
550  }
551  UIView* splashView = [self splashScreenFromStoryboard:launchscreenName];
552  if (!splashView) {
553  splashView = [self splashScreenFromXib:launchscreenName];
554  }
555  if (!splashView) {
556  return NO;
557  }
558  self.splashScreenView = splashView;
559  return YES;
560 }
561 
562 - (UIView*)splashScreenFromStoryboard:(NSString*)name {
563  UIStoryboard* storyboard = nil;
564  @try {
565  storyboard = [UIStoryboard storyboardWithName:name bundle:nil];
566  } @catch (NSException* exception) {
567  return nil;
568  }
569  if (storyboard) {
570  UIViewController* splashScreenViewController = [storyboard instantiateInitialViewController];
571  return splashScreenViewController.view;
572  }
573  return nil;
574 }
575 
576 - (UIView*)splashScreenFromXib:(NSString*)name {
577  NSArray* objects = nil;
578  @try {
579  objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil];
580  } @catch (NSException* exception) {
581  return nil;
582  }
583  if ([objects count] != 0) {
584  UIView* view = [objects objectAtIndex:0];
585  return view;
586  }
587  return nil;
588 }
589 
590 - (void)setSplashScreenView:(UIView*)view {
591  if (!view) {
592  // Special case: user wants to remove the splash screen view.
593  if (_splashScreenView) {
594  [self removeSplashScreenView:nil];
595  }
596  return;
597  }
598 
599  _splashScreenView.reset([view retain]);
600  _splashScreenView.get().autoresizingMask =
601  UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
602 }
603 
604 - (void)setFlutterViewDidRenderCallback:(void (^)(void))callback {
606 }
607 
608 #pragma mark - Surface creation and teardown updates
609 
610 - (void)surfaceUpdated:(BOOL)appeared {
611  if (!_engine) {
612  return;
613  }
614 
615  // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and raster
616  // thread.
617  if (appeared) {
618  [self installFirstFrameCallback];
619  [_engine.get() platformViewsController]->SetFlutterView(_flutterView.get());
620  [_engine.get() platformViewsController]->SetFlutterViewController(self);
621  [_engine.get() iosPlatformView]->NotifyCreated();
622  } else {
623  self.displayingFlutterUI = NO;
624  [_engine.get() iosPlatformView]->NotifyDestroyed();
625  [_engine.get() platformViewsController]->SetFlutterView(nullptr);
626  [_engine.get() platformViewsController]->SetFlutterViewController(nullptr);
627  }
628 }
629 
630 #pragma mark - UIViewController lifecycle notifications
631 
632 - (void)viewDidLoad {
633  TRACE_EVENT0("flutter", "viewDidLoad");
634 
635  if (_engine && _engineNeedsLaunch) {
636  // Register internal plugins before starting the engine.
637  [self addInternalPlugins];
638 
639  [_engine.get() launchEngine:nil libraryURI:nil];
640  [_engine.get() setViewController:self];
641  _engineNeedsLaunch = NO;
642  }
643 
644  if ([_engine.get() viewController] == self) {
645  [_engine.get() attachView];
646  }
647 
648  if (@available(iOS 13.4, *)) {
649  _pointerInteraction.reset([[UIPointerInteraction alloc] initWithDelegate:self]);
650  [self.view addInteraction:_pointerInteraction];
651 
652  _panGestureRecognizer.reset(
653  [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(scrollEvent:)]);
654  _panGestureRecognizer.get().allowedScrollTypesMask = UIScrollTypeMaskAll;
655  _panGestureRecognizer.get().allowedTouchTypes = @[ @(UITouchTypeIndirectPointer) ];
656  [_flutterView.get() addGestureRecognizer:_panGestureRecognizer.get()];
657  }
658 
659  [super viewDidLoad];
660 }
661 
662 - (void)addInternalPlugins {
663  [self.keyboardManager release];
664  self.keyboardManager = [[FlutterKeyboardManager alloc] init];
665  FlutterSendKeyEvent sendEvent =
666  ^(const FlutterKeyEvent& event, FlutterKeyEventCallback callback, void* userData) {
667  [_engine.get() sendKeyEvent:event callback:callback userData:userData];
668  };
669  [self.keyboardManager
670  addPrimaryResponder:[[FlutterEmbedderKeyResponder alloc] initWithSendEvent:sendEvent]];
671  [self.keyboardManager addPrimaryResponder:[[FlutterChannelKeyResponder alloc]
672  initWithChannel:self.engine.keyEventChannel]];
673  [self.keyboardManager addSecondaryResponder:self.engine.textInputPlugin];
674 }
675 
676 - (void)removeInternalPlugins {
677  [self.keyboardManager release];
678  self.keyboardManager = nil;
679 }
680 
681 - (void)viewWillAppear:(BOOL)animated {
682  TRACE_EVENT0("flutter", "viewWillAppear");
683  if ([_engine.get() viewController] == self) {
684  // Send platform settings to Flutter, e.g., platform brightness.
685  [self onUserSettingsChanged:nil];
686 
687  // Only recreate surface on subsequent appearances when viewport metrics are known.
688  // First time surface creation is done on viewDidLayoutSubviews.
690  [self surfaceUpdated:YES];
691  }
692  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
693  [[_engine.get() restorationPlugin] markRestorationComplete];
694  }
695 
696  [super viewWillAppear:animated];
697 }
698 
699 - (void)viewDidAppear:(BOOL)animated {
700  TRACE_EVENT0("flutter", "viewDidAppear");
701  if ([_engine.get() viewController] == self) {
702  [self onUserSettingsChanged:nil];
703  [self onAccessibilityStatusChanged:nil];
704  if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) {
705  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"];
706  }
707  }
708  [super viewDidAppear:animated];
709 }
710 
711 - (void)viewWillDisappear:(BOOL)animated {
712  TRACE_EVENT0("flutter", "viewWillDisappear");
713  if ([_engine.get() viewController] == self) {
714  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
715  }
716  [super viewWillDisappear:animated];
717 }
718 
719 - (void)viewDidDisappear:(BOOL)animated {
720  TRACE_EVENT0("flutter", "viewDidDisappear");
721  if ([_engine.get() viewController] == self) {
722  [self surfaceUpdated:NO];
723  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"];
724  [self flushOngoingTouches];
725  [_engine.get() notifyLowMemory];
726  }
727 
728  [super viewDidDisappear:animated];
729 }
730 
731 - (void)flushOngoingTouches {
732  if (_engine && _ongoingTouches.get().count > 0) {
733  auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.get().count);
734  size_t pointer_index = 0;
735  // If the view controller is going away, we want to flush cancel all the ongoing
736  // touches to the framework so nothing gets orphaned.
737  for (NSNumber* device in _ongoingTouches.get()) {
738  // Create fake PointerData to balance out each previously started one for the framework.
739  flutter::PointerData pointer_data;
740  pointer_data.Clear();
741 
742  // Use current time.
743  pointer_data.time_stamp = [[NSDate date] timeIntervalSince1970] * kMicrosecondsPerSecond;
744 
747  pointer_data.device = device.longLongValue;
748  pointer_data.pointer_identifier = 0;
749 
750  // Anything we put here will be arbitrary since there are no touches.
751  pointer_data.physical_x = 0;
752  pointer_data.physical_y = 0;
753  pointer_data.physical_delta_x = 0.0;
754  pointer_data.physical_delta_y = 0.0;
755  pointer_data.pressure = 1.0;
756  pointer_data.pressure_max = 1.0;
757 
758  packet->SetPointerData(pointer_index++, pointer_data);
759  }
760 
761  [_ongoingTouches removeAllObjects];
762  [_engine.get() dispatchPointerDataPacket:std::move(packet)];
763  }
764 }
765 
766 - (void)deregisterNotifications {
767  [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerWillDealloc
768  object:self
769  userInfo:nil];
770  [[NSNotificationCenter defaultCenter] removeObserver:self];
771 }
772 
773 - (void)dealloc {
774  [self removeInternalPlugins];
775  [self deregisterNotifications];
776  [super dealloc];
777 }
778 
779 #pragma mark - Application lifecycle notifications
780 
781 - (void)applicationBecameActive:(NSNotification*)notification {
782  TRACE_EVENT0("flutter", "applicationBecameActive");
784  [self surfaceUpdated:YES];
785  [self goToApplicationLifecycle:@"AppLifecycleState.resumed"];
786 }
787 
788 - (void)applicationWillResignActive:(NSNotification*)notification {
789  TRACE_EVENT0("flutter", "applicationWillResignActive");
790  [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
791 }
792 
793 - (void)applicationDidEnterBackground:(NSNotification*)notification {
794  TRACE_EVENT0("flutter", "applicationDidEnterBackground");
795  [self surfaceUpdated:NO];
796  [self goToApplicationLifecycle:@"AppLifecycleState.paused"];
797 }
798 
799 - (void)applicationWillEnterForeground:(NSNotification*)notification {
800  TRACE_EVENT0("flutter", "applicationWillEnterForeground");
801  [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
802 }
803 
804 // Make this transition only while this current view controller is visible.
805 - (void)goToApplicationLifecycle:(nonnull NSString*)state {
806  // Accessing self.view will create the view. Check whether the view is organically loaded
807  // first before checking whether the view is attached to window.
808  if (self.isViewLoaded && self.view.window) {
809  [[_engine.get() lifecycleChannel] sendMessage:state];
810  }
811 }
812 
813 #pragma mark - Touch event handling
814 
815 static flutter::PointerData::Change PointerDataChangeFromUITouchPhase(UITouchPhase phase) {
816  switch (phase) {
817  case UITouchPhaseBegan:
819  case UITouchPhaseMoved:
820  case UITouchPhaseStationary:
821  // There is no EVENT_TYPE_POINTER_STATIONARY. So we just pass a move type
822  // with the same coordinates
824  case UITouchPhaseEnded:
826  case UITouchPhaseCancelled:
828  default:
829  // TODO(53695): Handle the `UITouchPhaseRegion`... enum values.
830  FML_DLOG(INFO) << "Unhandled touch phase: " << phase;
831  break;
832  }
833 
835 }
836 
837 static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) {
838  if (@available(iOS 9, *)) {
839  switch (touch.type) {
840  case UITouchTypeDirect:
841  case UITouchTypeIndirect:
843  case UITouchTypeStylus:
845  case UITouchTypeIndirectPointer:
847  default:
848  FML_DLOG(INFO) << "Unhandled touch type: " << touch.type;
849  break;
850  }
851  }
852 
854 }
855 
856 // Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined
857 // from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events
858 // in the status bar area are available to framework code. The change type (optional) of the faked
859 // touch is specified in the second argument.
860 - (void)dispatchTouches:(NSSet*)touches
861  pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change
862  event:(UIEvent*)event {
863  if (!_engine) {
864  return;
865  }
866 
867  const CGFloat scale = [UIScreen mainScreen].scale;
868  auto packet = std::make_unique<flutter::PointerDataPacket>(touches.count);
869 
870  size_t pointer_index = 0;
871 
872  for (UITouch* touch in touches) {
873  CGPoint windowCoordinates = [touch locationInView:self.view];
874 
875  flutter::PointerData pointer_data;
876  pointer_data.Clear();
877 
878  constexpr int kMicrosecondsPerSecond = 1000 * 1000;
879  pointer_data.time_stamp = touch.timestamp * kMicrosecondsPerSecond;
880 
881  pointer_data.change = overridden_change != nullptr
882  ? *overridden_change
883  : PointerDataChangeFromUITouchPhase(touch.phase);
884 
885  pointer_data.kind = DeviceKindFromTouchType(touch);
886 
887  pointer_data.device = reinterpret_cast<int64_t>(touch);
888 
889  // Pointer will be generated in pointer_data_packet_converter.cc.
890  pointer_data.pointer_identifier = 0;
891 
892  pointer_data.physical_x = windowCoordinates.x * scale;
893  pointer_data.physical_y = windowCoordinates.y * scale;
894 
895  // Delta will be generated in pointer_data_packet_converter.cc.
896  pointer_data.physical_delta_x = 0.0;
897  pointer_data.physical_delta_y = 0.0;
898 
899  NSNumber* deviceKey = [NSNumber numberWithLongLong:pointer_data.device];
900  // Track touches that began and not yet stopped so we can flush them
901  // if the view controller goes away.
902  switch (pointer_data.change) {
904  [_ongoingTouches addObject:deviceKey];
905  break;
908  [_ongoingTouches removeObject:deviceKey];
909  break;
912  // We're only tracking starts and stops.
913  break;
916  // We don't use kAdd/kRemove.
917  break;
918  }
919 
920  // pressure_min is always 0.0
921  if (@available(iOS 9, *)) {
922  // These properties were introduced in iOS 9.0.
923  pointer_data.pressure = touch.force;
924  pointer_data.pressure_max = touch.maximumPossibleForce;
925  } else {
926  pointer_data.pressure = 1.0;
927  pointer_data.pressure_max = 1.0;
928  }
929 
930  // These properties were introduced in iOS 8.0
931  pointer_data.radius_major = touch.majorRadius;
932  pointer_data.radius_min = touch.majorRadius - touch.majorRadiusTolerance;
933  pointer_data.radius_max = touch.majorRadius + touch.majorRadiusTolerance;
934 
935  // These properties were introduced in iOS 9.1
936  if (@available(iOS 9.1, *)) {
937  // iOS Documentation: altitudeAngle
938  // A value of 0 radians indicates that the stylus is parallel to the surface. The value of
939  // this property is Pi/2 when the stylus is perpendicular to the surface.
940  //
941  // PointerData Documentation: tilt
942  // The angle of the stylus, in radians in the range:
943  // 0 <= tilt <= pi/2
944  // giving the angle of the axis of the stylus, relative to the axis perpendicular to the input
945  // surface (thus 0.0 indicates the stylus is orthogonal to the plane of the input surface,
946  // while pi/2 indicates that the stylus is flat on that surface).
947  //
948  // Discussion:
949  // The ranges are the same. Origins are swapped.
950  pointer_data.tilt = M_PI_2 - touch.altitudeAngle;
951 
952  // iOS Documentation: azimuthAngleInView:
953  // With the tip of the stylus touching the screen, the value of this property is 0 radians
954  // when the cap end of the stylus (that is, the end opposite of the tip) points along the
955  // positive x axis of the device's screen. The azimuth angle increases as the user swings the
956  // cap end of the stylus in a clockwise direction around the tip.
957  //
958  // PointerData Documentation: orientation
959  // The angle of the stylus, in radians in the range:
960  // -pi < orientation <= pi
961  // giving the angle of the axis of the stylus projected onto the input surface, relative to
962  // the positive y-axis of that surface (thus 0.0 indicates the stylus, if projected onto that
963  // surface, would go from the contact point vertically up in the positive y-axis direction, pi
964  // would indicate that the stylus would go down in the negative y-axis direction; pi/4 would
965  // indicate that the stylus goes up and to the right, -pi/2 would indicate that the stylus
966  // goes to the left, etc).
967  //
968  // Discussion:
969  // Sweep direction is the same. Phase of M_PI_2.
970  pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2;
971  }
972 
973  if (@available(iOS 13.4, *)) {
974  if (event != nullptr) {
975  pointer_data.buttons = (((event.buttonMask & UIEventButtonMaskPrimary) > 0)
977  : 0) |
978  (((event.buttonMask & UIEventButtonMaskSecondary) > 0)
980  : 0);
981  }
982  }
983 
984  packet->SetPointerData(pointer_index++, pointer_data);
985  }
986 
987  [_engine.get() dispatchPointerDataPacket:std::move(packet)];
988 }
989 
990 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
991  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
992 }
993 
994 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
995  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
996 }
997 
998 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
999  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1000 }
1001 
1002 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
1003  [self dispatchTouches:touches pointerDataChangeOverride:nullptr event:event];
1004 }
1005 
1006 - (void)forceTouchesCancelled:(NSSet*)touches {
1008  [self dispatchTouches:touches pointerDataChangeOverride:&cancel event:nullptr];
1009 }
1010 
1011 #pragma mark - Handle view resizing
1012 
1013 - (void)updateViewportMetrics {
1014  if ([_engine.get() viewController] == self) {
1015  [_engine.get() updateViewportMetrics:_viewportMetrics];
1016  }
1017 }
1018 
1019 - (CGFloat)statusBarPadding {
1020  UIScreen* screen = self.view.window.screen;
1021  CGRect statusFrame = [UIApplication sharedApplication].statusBarFrame;
1022  CGRect viewFrame = [self.view convertRect:self.view.bounds
1023  toCoordinateSpace:screen.coordinateSpace];
1024  CGRect intersection = CGRectIntersection(statusFrame, viewFrame);
1025  return CGRectIsNull(intersection) ? 0.0 : intersection.size.height;
1026 }
1027 
1028 - (void)viewDidLayoutSubviews {
1029  CGRect viewBounds = self.view.bounds;
1030  CGFloat scale = [UIScreen mainScreen].scale;
1031 
1032  // Purposefully place this not visible.
1033  _scrollView.get().frame = CGRectMake(0.0, 0.0, viewBounds.size.width, 0.0);
1034  _scrollView.get().contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
1035 
1036  // First time since creation that the dimensions of its view is known.
1037  bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
1039  _viewportMetrics.physical_width = viewBounds.size.width * scale;
1040  _viewportMetrics.physical_height = viewBounds.size.height * scale;
1041 
1042  [self updateViewportPadding];
1043  [self updateViewportMetrics];
1044 
1045  // There is no guarantee that UIKit will layout subviews when the application is active. Creating
1046  // the surface when inactive will cause GPU accesses from the background. Only wait for the first
1047  // frame to render when the application is actually active.
1048  bool applicationIsActive =
1049  [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
1050 
1051  // This must run after updateViewportMetrics so that the surface creation tasks are queued after
1052  // the viewport metrics update tasks.
1053  if (firstViewBoundsUpdate && applicationIsActive && _engine) {
1054  [self surfaceUpdated:YES];
1055 
1056  flutter::Shell& shell = [_engine.get() shell];
1057  fml::TimeDelta waitTime =
1058 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
1060 #else
1062 #endif
1063  if (shell.WaitForFirstFrame(waitTime).code() == fml::StatusCode::kDeadlineExceeded) {
1064  FML_LOG(INFO) << "Timeout waiting for the first frame to render. This may happen in "
1065  << "unoptimized builds. If this is a release build, you should load a less "
1066  << "complex frame to avoid the timeout.";
1067  }
1068  }
1069 }
1070 
1071 - (void)viewSafeAreaInsetsDidChange {
1072  [self updateViewportPadding];
1073  [self updateViewportMetrics];
1074  [super viewSafeAreaInsetsDidChange];
1075 }
1076 
1077 // Updates _viewportMetrics physical padding.
1078 //
1079 // Viewport padding represents the iOS safe area insets.
1080 - (void)updateViewportPadding {
1081  CGFloat scale = [UIScreen mainScreen].scale;
1082  if (@available(iOS 11, *)) {
1083  _viewportMetrics.physical_padding_top = self.view.safeAreaInsets.top * scale;
1084  _viewportMetrics.physical_padding_left = self.view.safeAreaInsets.left * scale;
1085  _viewportMetrics.physical_padding_right = self.view.safeAreaInsets.right * scale;
1086  _viewportMetrics.physical_padding_bottom = self.view.safeAreaInsets.bottom * scale;
1087  } else {
1088  _viewportMetrics.physical_padding_top = [self statusBarPadding] * scale;
1089  }
1090 }
1091 
1092 #pragma mark - Keyboard events
1093 
1094 - (void)keyboardWillChangeFrame:(NSNotification*)notification {
1095  NSDictionary* info = [notification userInfo];
1096 
1097  if (@available(iOS 9, *)) {
1098  // Ignore keyboard notifications related to other apps.
1099  id isLocal = info[UIKeyboardIsLocalUserInfoKey];
1100  if (isLocal && ![isLocal boolValue]) {
1101  return;
1102  }
1103  }
1104 
1105  CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
1106  CGRect screenRect = [[UIScreen mainScreen] bounds];
1107 
1108  // Considering the iPad's split keyboard, Flutter needs to check if the keyboard frame is present
1109  // in the screen to see if the keyboard is visible.
1110  if (CGRectIntersectsRect(keyboardFrame, screenRect)) {
1111  CGFloat bottom = CGRectGetHeight(keyboardFrame);
1112  CGFloat scale = [UIScreen mainScreen].scale;
1113 
1114  // The keyboard is treated as an inset since we want to effectively reduce the window size by
1115  // the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
1116  // bottom padding.
1118  } else {
1120  }
1121 
1122  [self updateViewportMetrics];
1123 }
1124 
1125 - (void)keyboardWillBeHidden:(NSNotification*)notification {
1127  [self updateViewportMetrics];
1128 }
1129 
1130 - (void)handlePressEvent:(FlutterUIPressProxy*)press
1131  nextAction:(void (^)())next API_AVAILABLE(ios(13.4)) {
1132  if (@available(iOS 13.4, *)) {
1133  } else {
1134  next();
1135  return;
1136  }
1137  [self.keyboardManager handlePress:press nextAction:next];
1138 }
1139 
1140 // The documentation for presses* handlers (implemented below) is entirely
1141 // unclear about how to handle the case where some, but not all, of the presses
1142 // are handled here. I've elected to call super separately for each of the
1143 // presses that aren't handled, but it's not clear if this is correct. It may be
1144 // that iOS intends for us to either handle all or none of the presses, and pass
1145 // the original set to super. I have not yet seen multiple presses in the set in
1146 // the wild, however, so I suspect that the API is built for a tvOS remote or
1147 // something, and perhaps only one ever appears in the set on iOS from a
1148 // keyboard.
1149 
1150 // If you substantially change these presses overrides, consider also changing
1151 // the similar ones in FlutterTextInputPlugin. They need to be overridden in
1152 // both places to capture keys both inside and outside of a text field, but have
1153 // slightly different implmentations.
1154 
1155 - (void)pressesBegan:(NSSet<UIPress*>*)presses
1156  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1157  if (@available(iOS 13.4, *)) {
1158  for (UIPress* press in presses) {
1159  [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press
1160  withEvent:event] autorelease]
1161  nextAction:^() {
1162  [super pressesBegan:[NSSet setWithObject:press] withEvent:event];
1163  }];
1164  }
1165  } else {
1166  [super pressesBegan:presses withEvent:event];
1167  }
1168 }
1169 
1170 - (void)pressesChanged:(NSSet<UIPress*>*)presses
1171  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1172  if (@available(iOS 13.4, *)) {
1173  for (UIPress* press in presses) {
1174  [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press
1175  withEvent:event] autorelease]
1176  nextAction:^() {
1177  [super pressesChanged:[NSSet setWithObject:press] withEvent:event];
1178  }];
1179  }
1180  } else {
1181  [super pressesChanged:presses withEvent:event];
1182  }
1183 }
1184 
1185 - (void)pressesEnded:(NSSet<UIPress*>*)presses
1186  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1187  if (@available(iOS 13.4, *)) {
1188  for (UIPress* press in presses) {
1189  [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press
1190  withEvent:event] autorelease]
1191  nextAction:^() {
1192  [super pressesEnded:[NSSet setWithObject:press] withEvent:event];
1193  }];
1194  }
1195  } else {
1196  [super pressesEnded:presses withEvent:event];
1197  }
1198 }
1199 
1200 - (void)pressesCancelled:(NSSet<UIPress*>*)presses
1201  withEvent:(UIPressesEvent*)event API_AVAILABLE(ios(9.0)) {
1202  if (@available(iOS 13.4, *)) {
1203  for (UIPress* press in presses) {
1204  [self handlePressEvent:[[[FlutterUIPressProxy alloc] initWithPress:press
1205  withEvent:event] autorelease]
1206  nextAction:^() {
1207  [super pressesCancelled:[NSSet setWithObject:press] withEvent:event];
1208  }];
1209  }
1210  } else {
1211  [super pressesCancelled:presses withEvent:event];
1212  }
1213 }
1214 
1215 #pragma mark - Orientation updates
1216 
1217 - (void)onOrientationPreferencesUpdated:(NSNotification*)notification {
1218  // Notifications may not be on the iOS UI thread
1219  dispatch_async(dispatch_get_main_queue(), ^{
1220  NSDictionary* info = notification.userInfo;
1221 
1222  NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)];
1223 
1224  if (update == nil) {
1225  return;
1226  }
1227  [self performOrientationUpdate:update.unsignedIntegerValue];
1228  });
1229 }
1230 
1231 - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
1232  if (new_preferences != _orientationPreferences) {
1233  _orientationPreferences = new_preferences;
1234  [UIViewController attemptRotationToDeviceOrientation];
1235 
1236  UIInterfaceOrientationMask currentInterfaceOrientation =
1237  1 << [[UIApplication sharedApplication] statusBarOrientation];
1238  if (!(_orientationPreferences & currentInterfaceOrientation)) {
1239  // Force orientation switch if the current orientation is not allowed
1240  if (_orientationPreferences & UIInterfaceOrientationMaskPortrait) {
1241  // This is no official API but more like a workaround / hack (using
1242  // key-value coding on a read-only property). This might break in
1243  // the future, but currently it´s the only way to force an orientation change
1244  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait) forKey:@"orientation"];
1245  } else if (_orientationPreferences & UIInterfaceOrientationMaskPortraitUpsideDown) {
1246  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortraitUpsideDown)
1247  forKey:@"orientation"];
1248  } else if (_orientationPreferences & UIInterfaceOrientationMaskLandscapeLeft) {
1249  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeLeft)
1250  forKey:@"orientation"];
1251  } else if (_orientationPreferences & UIInterfaceOrientationMaskLandscapeRight) {
1252  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeRight)
1253  forKey:@"orientation"];
1254  }
1255  }
1256  }
1257 }
1258 
1259 - (void)onHideHomeIndicatorNotification:(NSNotification*)notification {
1260  self.isHomeIndicatorHidden = YES;
1261 }
1262 
1263 - (void)onShowHomeIndicatorNotification:(NSNotification*)notification {
1264  self.isHomeIndicatorHidden = NO;
1265 }
1266 
1267 - (void)setIsHomeIndicatorHidden:(BOOL)hideHomeIndicator {
1268  if (hideHomeIndicator != _isHomeIndicatorHidden) {
1269  _isHomeIndicatorHidden = hideHomeIndicator;
1270  if (@available(iOS 11, *)) {
1271  [self setNeedsUpdateOfHomeIndicatorAutoHidden];
1272  }
1273  }
1274 }
1275 
1276 - (BOOL)prefersHomeIndicatorAutoHidden {
1277  return self.isHomeIndicatorHidden;
1278 }
1279 
1280 - (BOOL)shouldAutorotate {
1281  return YES;
1282 }
1283 
1284 - (NSUInteger)supportedInterfaceOrientations {
1285  return _orientationPreferences;
1286 }
1287 
1288 #pragma mark - Accessibility
1289 
1290 - (void)onAccessibilityStatusChanged:(NSNotification*)notification {
1291  if (!_engine) {
1292  return;
1293  }
1294  auto platformView = [_engine.get() platformView];
1295  int32_t flags = 0;
1296  if (UIAccessibilityIsInvertColorsEnabled()) {
1297  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kInvertColors);
1298  }
1299  if (UIAccessibilityIsReduceMotionEnabled()) {
1300  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kReduceMotion);
1301  }
1302  if (UIAccessibilityIsBoldTextEnabled()) {
1303  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kBoldText);
1304  }
1305  if (UIAccessibilityDarkerSystemColorsEnabled()) {
1306  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kHighContrast);
1307  }
1308 #if TARGET_OS_SIMULATOR
1309  // There doesn't appear to be any way to determine whether the accessibility
1310  // inspector is enabled on the simulator. We conservatively always turn on the
1311  // accessibility bridge in the simulator, but never assistive technology.
1312  platformView->SetSemanticsEnabled(true);
1313  platformView->SetAccessibilityFeatures(flags);
1314 #else
1315  _isVoiceOverRunning = UIAccessibilityIsVoiceOverRunning();
1316  bool enabled = _isVoiceOverRunning || UIAccessibilityIsSwitchControlRunning();
1317  if (enabled)
1318  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kAccessibleNavigation);
1319  platformView->SetSemanticsEnabled(enabled || UIAccessibilityIsSpeakScreenEnabled());
1320  platformView->SetAccessibilityFeatures(flags);
1321 #endif
1322 }
1323 
1324 #pragma mark - Set user settings
1325 
1326 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
1327  [super traitCollectionDidChange:previousTraitCollection];
1328  [self onUserSettingsChanged:nil];
1329 }
1330 
1331 - (void)onUserSettingsChanged:(NSNotification*)notification {
1332  [[_engine.get() settingsChannel] sendMessage:@{
1333  @"textScaleFactor" : @([self textScaleFactor]),
1334  @"alwaysUse24HourFormat" : @([self isAlwaysUse24HourFormat]),
1335  @"platformBrightness" : [self brightnessMode],
1336  @"platformContrast" : [self contrastMode]
1337  }];
1338 }
1339 
1340 - (CGFloat)textScaleFactor {
1341  UIContentSizeCategory category = [UIApplication sharedApplication].preferredContentSizeCategory;
1342  // The delta is computed by approximating Apple's typography guidelines:
1343  // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/
1344  //
1345  // Specifically:
1346  // Non-accessibility sizes for "body" text are:
1347  const CGFloat xs = 14;
1348  const CGFloat s = 15;
1349  const CGFloat m = 16;
1350  const CGFloat l = 17;
1351  const CGFloat xl = 19;
1352  const CGFloat xxl = 21;
1353  const CGFloat xxxl = 23;
1354 
1355  // Accessibility sizes for "body" text are:
1356  const CGFloat ax1 = 28;
1357  const CGFloat ax2 = 33;
1358  const CGFloat ax3 = 40;
1359  const CGFloat ax4 = 47;
1360  const CGFloat ax5 = 53;
1361 
1362  // We compute the scale as relative difference from size L (large, the default size), where
1363  // L is assumed to have scale 1.0.
1364  if ([category isEqualToString:UIContentSizeCategoryExtraSmall])
1365  return xs / l;
1366  else if ([category isEqualToString:UIContentSizeCategorySmall])
1367  return s / l;
1368  else if ([category isEqualToString:UIContentSizeCategoryMedium])
1369  return m / l;
1370  else if ([category isEqualToString:UIContentSizeCategoryLarge])
1371  return 1.0;
1372  else if ([category isEqualToString:UIContentSizeCategoryExtraLarge])
1373  return xl / l;
1374  else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge])
1375  return xxl / l;
1376  else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge])
1377  return xxxl / l;
1378  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium])
1379  return ax1 / l;
1380  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge])
1381  return ax2 / l;
1382  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge])
1383  return ax3 / l;
1384  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge])
1385  return ax4 / l;
1386  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge])
1387  return ax5 / l;
1388  else
1389  return 1.0;
1390 }
1391 
1392 - (BOOL)isAlwaysUse24HourFormat {
1393  // iOS does not report its "24-Hour Time" user setting in the API. Instead, it applies
1394  // it automatically to NSDateFormatter when used with [NSLocale currentLocale]. It is
1395  // essential that [NSLocale currentLocale] is used. Any custom locale, even the one
1396  // that's the same as [NSLocale currentLocale] will ignore the 24-hour option (there
1397  // must be some internal field that's not exposed to developers).
1398  //
1399  // Therefore this option behaves differently across Android and iOS. On Android this
1400  // setting is exposed standalone, and can therefore be applied to all locales, whether
1401  // the "current system locale" or a custom one. On iOS it only applies to the current
1402  // system locale. Widget implementors must take this into account in order to provide
1403  // platform-idiomatic behavior in their widgets.
1404  NSString* dateFormat = [NSDateFormatter dateFormatFromTemplate:@"j"
1405  options:0
1406  locale:[NSLocale currentLocale]];
1407  return [dateFormat rangeOfString:@"a"].location == NSNotFound;
1408 }
1409 
1410 // The brightness mode of the platform, e.g., light or dark, expressed as a string that
1411 // is understood by the Flutter framework. See the settings system channel for more
1412 // information.
1413 - (NSString*)brightnessMode {
1414  if (@available(iOS 13, *)) {
1415  UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;
1416 
1417  if (style == UIUserInterfaceStyleDark) {
1418  return @"dark";
1419  } else {
1420  return @"light";
1421  }
1422  } else {
1423  return @"light";
1424  }
1425 }
1426 
1427 // The contrast mode of the platform, e.g., normal or high, expressed as a string that is
1428 // understood by the Flutter framework. See the settings system channel for more
1429 // information.
1430 - (NSString*)contrastMode {
1431  if (@available(iOS 13, *)) {
1432  UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast;
1433 
1434  if (contrast == UIAccessibilityContrastHigh) {
1435  return @"high";
1436  } else {
1437  return @"normal";
1438  }
1439  } else {
1440  return @"normal";
1441  }
1442 }
1443 
1444 #pragma mark - Status bar style
1445 
1446 - (UIStatusBarStyle)preferredStatusBarStyle {
1447  return _statusBarStyle;
1448 }
1449 
1450 - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
1451  // Notifications may not be on the iOS UI thread
1452  dispatch_async(dispatch_get_main_queue(), ^{
1453  NSDictionary* info = notification.userInfo;
1454 
1455  NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)];
1456 
1457  if (update == nil) {
1458  return;
1459  }
1460 
1461  NSInteger style = update.integerValue;
1462 
1463  if (style != _statusBarStyle) {
1464  _statusBarStyle = static_cast<UIStatusBarStyle>(style);
1465  [self setNeedsStatusBarAppearanceUpdate];
1466  }
1467  });
1468 }
1469 
1470 #pragma mark - Platform views
1471 
1472 - (std::shared_ptr<flutter::FlutterPlatformViewsController>&)platformViewsController {
1473  return [_engine.get() platformViewsController];
1474 }
1475 
1476 - (NSObject<FlutterBinaryMessenger>*)binaryMessenger {
1477  return _engine.get().binaryMessenger;
1478 }
1479 
1480 #pragma mark - FlutterBinaryMessenger
1481 
1482 - (void)sendOnChannel:(NSString*)channel message:(NSData*)message {
1483  [_engine.get().binaryMessenger sendOnChannel:channel message:message];
1484 }
1485 
1486 - (void)sendOnChannel:(NSString*)channel
1487  message:(NSData*)message
1488  binaryReply:(FlutterBinaryReply)callback {
1489  NSAssert(channel, @"The channel must not be null");
1490  [_engine.get().binaryMessenger sendOnChannel:channel message:message binaryReply:callback];
1491 }
1492 
1493 - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channel
1494  binaryMessageHandler:
1495  (FlutterBinaryMessageHandler)handler {
1496  NSAssert(channel, @"The channel must not be null");
1497  return [_engine.get().binaryMessenger setMessageHandlerOnChannel:channel
1498  binaryMessageHandler:handler];
1499 }
1500 
1501 - (void)cleanUpConnection:(FlutterBinaryMessengerConnection)connection {
1502  [_engine.get().binaryMessenger cleanUpConnection:connection];
1503 }
1504 
1505 #pragma mark - FlutterTextureRegistry
1506 
1507 - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture {
1508  return [_engine.get() registerTexture:texture];
1509 }
1510 
1511 - (void)unregisterTexture:(int64_t)textureId {
1512  [_engine.get() unregisterTexture:textureId];
1513 }
1514 
1515 - (void)textureFrameAvailable:(int64_t)textureId {
1516  [_engine.get() textureFrameAvailable:textureId];
1517 }
1518 
1519 - (NSString*)lookupKeyForAsset:(NSString*)asset {
1520  return [FlutterDartProject lookupKeyForAsset:asset];
1521 }
1522 
1523 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
1524  return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
1525 }
1526 
1527 - (id<FlutterPluginRegistry>)pluginRegistry {
1528  return _engine;
1529 }
1530 
1531 #pragma mark - FlutterPluginRegistry
1532 
1533 - (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
1534  return [_engine.get() registrarForPlugin:pluginKey];
1535 }
1536 
1537 - (BOOL)hasPlugin:(NSString*)pluginKey {
1538  return [_engine.get() hasPlugin:pluginKey];
1539 }
1540 
1541 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
1542  return [_engine.get() valuePublishedByPlugin:pluginKey];
1543 }
1544 
1545 - (void)presentViewController:(UIViewController*)viewControllerToPresent
1546  animated:(BOOL)flag
1547  completion:(void (^)(void))completion {
1548  self.isPresentingViewControllerAnimating = YES;
1549  [super presentViewController:viewControllerToPresent
1550  animated:flag
1551  completion:^{
1552  self.isPresentingViewControllerAnimating = NO;
1553  if (completion) {
1554  completion();
1555  }
1556  }];
1557 }
1558 
1559 - (BOOL)isPresentingViewController {
1560  return self.presentedViewController != nil || self.isPresentingViewControllerAnimating;
1561 }
1562 
1563 - (flutter::PointerData)generatePointerDataForMouse API_AVAILABLE(ios(13.4)) {
1564  flutter::PointerData pointer_data;
1565 
1566  pointer_data.Clear();
1567 
1571  pointer_data.pointer_identifier = reinterpret_cast<int64_t>(_pointerInteraction.get());
1572 
1573  pointer_data.physical_x = _mouseState.location.x;
1574  pointer_data.physical_y = _mouseState.location.y;
1575 
1576  return pointer_data;
1577 }
1578 
1579 - (UIPointerRegion*)pointerInteraction:(UIPointerInteraction*)interaction
1580  regionForRequest:(UIPointerRegionRequest*)request
1581  defaultRegion:(UIPointerRegion*)defaultRegion API_AVAILABLE(ios(13.4)) {
1582  if (request != nil) {
1583  auto packet = std::make_unique<flutter::PointerDataPacket>(1);
1584  const CGFloat scale = [UIScreen mainScreen].scale;
1585  _mouseState.location = {request.location.x * scale, request.location.y * scale};
1586 
1587  flutter::PointerData pointer_data = [self generatePointerDataForMouse];
1588 
1590  packet->SetPointerData(/*index=*/0, pointer_data);
1591 
1592  [_engine.get() dispatchPointerDataPacket:std::move(packet)];
1593  }
1594  return nil;
1595 }
1596 
1597 - (void)scrollEvent:(UIPanGestureRecognizer*)recognizer API_AVAILABLE(ios(13.4)) {
1598  CGPoint translation = [recognizer translationInView:self.view];
1599  const CGFloat scale = [UIScreen mainScreen].scale;
1600 
1601  translation.x *= scale;
1602  translation.y *= scale;
1603 
1604  auto packet = std::make_unique<flutter::PointerDataPacket>(1);
1605 
1606  flutter::PointerData pointer_data = [self generatePointerDataForMouse];
1608  pointer_data.scroll_delta_x = (translation.x - _mouseState.last_translation.x);
1609  pointer_data.scroll_delta_y = -(translation.y - _mouseState.last_translation.y);
1610 
1611  // The translation reported by UIPanGestureRecognizer is the total translation
1612  // generated by the pan gesture since the gesture began. We need to be able
1613  // to keep track of the last translation value in order to generate the deltaX
1614  // and deltaY coordinates for each subsequent scroll event.
1615  if (recognizer.state != UIGestureRecognizerStateEnded) {
1616  _mouseState.last_translation = translation;
1617  } else {
1618  _mouseState.last_translation = CGPointZero;
1619  }
1620 
1621  packet->SetPointerData(/*index=*/0, pointer_data);
1622 
1623  [_engine.get() dispatchPointerDataPacket:std::move(packet)];
1624 }
1625 
1626 #pragma mark - State Restoration
1627 
1628 - (void)encodeRestorableStateWithCoder:(NSCoder*)coder {
1629  NSData* restorationData = [[_engine.get() restorationPlugin] restorationData];
1630  [coder encodeDataObject:restorationData];
1631 }
1632 
1633 - (void)decodeRestorableStateWithCoder:(NSCoder*)coder {
1634  NSData* restorationData = [coder decodeDataObject];
1635  [[_engine.get() restorationPlugin] setRestorationData:restorationData];
1636 }
1637 
1638 - (FlutterRestorationPlugin*)restorationPlugin {
1639  return [_engine.get() restorationPlugin];
1640 }
1641 
1642 @end
BOOL forceSoftwareRendering
Definition: FlutterView.h:47
static constexpr CGFloat kScrollViewContentSize
G_BEGIN_DECLS FlTexture * texture
#define TRACE_EVENT0(category_group, name)
Definition: trace_event.h:90
std::unique_ptr< fml::WeakPtrFactory< FlutterEngine > > _weakFactory
#define FML_DCHECK(condition)
Definition: logging.h:86
FlutterViewController * viewController
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
fml::scoped_nsobject< FlutterView > _flutterView
SignalKind signal_kind
Definition: pointer_data.h:65
int64_t pointer_identifier
Definition: pointer_data.h:67
void reset(NST * object=nil)
MouseState _mouseState
flutter::ViewportMetrics _viewportMetrics
DEF_SWITCHES_START aot vmservice shared library name
Definition: switches.h:32
#define FML_LOG(severity)
Definition: logging.h:65
NSNotificationName const FlutterViewControllerShowHomeIndicator
fml::scoped_nsobject< UIPointerInteraction > _pointerInteraction API_AVAILABLE(ios(13.4))
FlKeyEvent FlKeyResponderAsyncCallback callback
FlutterSemanticsFlag flags
void SetNextFrameCallback(const fml::closure &closure)
Sets a callback that gets executed when the rasterizer renders the next frame. Due to the asynchronou...
FlKeyEvent * event
BOOL _engineNeedsLaunch
void(^ FlutterBinaryMessageHandler)(NSData *_Nullable message, FlutterBinaryReply reply)
fml::scoped_nsobject< FlutterEngine > _engine
struct MouseState MouseState
UIStatusBarStyle _statusBarStyle
static constexpr int kMicrosecondsPerSecond
BOOL _initialized
uint8_t value
fml::scoped_nsobject< UIScrollView > _scrollView
fml::scoped_nsobject< UIView > _splashScreenView
SemanticsAction action
void onAccessibilityStatusChanged:(nonnull NSNotification *notification)
fml::scoped_nsobject< NSMutableSet< NSNumber * > > _ongoingTouches
fml::ScopedBlock< void(^)(void)> _flutterViewRenderedCallback
FlutterSemanticsFlag flag
NSString * lookupKeyForAsset:fromPackage:(NSString *asset, [fromPackage] NSString *package)
fml::Status WaitForFirstFrame(fml::TimeDelta timeout)
Pauses the calling thread until the first frame is presented.
Definition: shell.cc:1778
UIAccessibilityContrast accessibilityContrast()
NSString * lookupKeyForAsset:(NSString *asset)
int BOOL
Definition: windows_types.h:37
BOOL _viewOpaque
fml::StatusCode code() const
Definition: status.h:63
TracingResult GetTracingResult()
Returns if a tracing check has been performed and its result. To enable tracing, the Settings object ...
Definition: ptrace_check.h:62
FlView * view
static constexpr TimeDelta FromMilliseconds(int64_t millis)
Definition: time_delta.h:46
NSNotificationName const FlutterViewControllerWillDealloc
int32_t id
UIAccessibilityContrast
#define FML_DLOG(severity)
Definition: logging.h:85
void(* FlutterKeyEventCallback)(bool, void *)
Definition: embedder.h:748
NSNotificationName const FlutterViewControllerHideHomeIndicator
int64_t FlutterBinaryMessengerConnection
AtkStateType state
instancetype initWithProject:nibName:bundle:(nullable FlutterDartProject *project, [nibName] nullable NSString *nibName, [bundle] nullable NSBundle *NS_DESIGNATED_INITIALIZER)
UIInterfaceOrientationMask _orientationPreferences
FlutterViewController * viewController
void(^ FlutterSendKeyEvent)(const FlutterKeyEvent &, _Nullable FlutterKeyEventCallback, _Nullable _VoidPtr)
NSNotificationName const FlutterSemanticsUpdateNotification