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/FlutterPlatformViews_Internal.h"
8 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
9 
10 #include <memory>
11 
12 #include "flutter/fml/memory/weak_ptr.h"
13 #include "flutter/fml/message_loop.h"
14 #include "flutter/fml/platform/darwin/platform_version.h"
15 #include "flutter/fml/platform/darwin/scoped_nsobject.h"
16 #include "flutter/runtime/ptrace_check.h"
17 #include "flutter/shell/common/thread_host.h"
18 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterBinaryMessengerRelay.h"
19 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEngine_Internal.h"
20 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h"
21 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h"
22 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h"
23 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h"
24 #import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h"
25 #import "flutter/shell/platform/darwin/ios/platform_view_ios.h"
26 
27 static constexpr int kMicrosecondsPerSecond = 1000 * 1000;
28 static constexpr CGFloat kScrollViewContentSize = 2.0;
29 
30 NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate";
31 NSNotificationName const FlutterViewControllerWillDealloc = @"FlutterViewControllerWillDealloc";
32 NSNotificationName const FlutterViewControllerHideHomeIndicator =
33  @"FlutterViewControllerHideHomeIndicator";
34 NSNotificationName const FlutterViewControllerShowHomeIndicator =
35  @"FlutterViewControllerShowHomeIndicator";
36 
37 // This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the
38 // change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are
39 // just a warning.
40 @interface FlutterViewController () <FlutterBinaryMessenger, UIScrollViewDelegate>
41 @property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI;
42 @property(nonatomic, assign) BOOL isHomeIndicatorHidden;
43 @property(nonatomic, assign) BOOL isPresentingViewControllerAnimating;
44 @end
45 
46 // The following conditional compilation defines an API 13 concept on earlier API targets so that
47 // a compiler compiling against API 12 or below does not blow up due to non-existent members.
48 #if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000
49 typedef enum UIAccessibilityContrast : NSInteger {
54 
57 @end
58 #endif
59 
60 @implementation FlutterViewController {
61  std::unique_ptr<fml::WeakPtrFactory<FlutterViewController>> _weakFactory;
63 
64  // We keep a separate reference to this and create it ahead of time because we want to be able to
65  // setup a shell along with its platform view before the view has to appear.
69  UIInterfaceOrientationMask _orientationPreferences;
70  UIStatusBarStyle _statusBarStyle;
76  // This scroll view is a workaround to accomodate iOS 13 and higher. There isn't a way to get
77  // touches on the status bar to trigger scrolling to the top of a scroll view. We place a
78  // UIScrollView with height zero and a content offset so we can get those events. See also:
79  // https://github.com/flutter/flutter/issues/35050
81 }
82 
83 @synthesize displayingFlutterUI = _displayingFlutterUI;
84 
85 #pragma mark - Manage and override all designated initializers
86 
87 - (instancetype)initWithEngine:(FlutterEngine*)engine
88  nibName:(nullable NSString*)nibName
89  bundle:(nullable NSBundle*)nibBundle {
90  NSAssert(engine != nil, @"Engine is required");
91  self = [super initWithNibName:nibName bundle:nibBundle];
92  if (self) {
93  _viewOpaque = YES;
94  if (engine.viewController) {
95  FML_LOG(ERROR) << "The supplied FlutterEngine " << [[engine description] UTF8String]
96  << " is already used with FlutterViewController instance "
97  << [[engine.viewController description] UTF8String]
98  << ". One instance of the FlutterEngine can only be attached to one "
99  "FlutterViewController at a time. Set FlutterEngine.viewController "
100  "to nil before attaching it to another FlutterViewController.";
101  }
102  _engine.reset([engine retain]);
103  _engineNeedsLaunch = NO;
104  _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
105  _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
106  _ongoingTouches.reset([[NSMutableSet alloc] init]);
107 
108  [self performCommonViewControllerInitialization];
109  [engine setViewController:self];
110  }
111 
112  return self;
113 }
114 
115 - (instancetype)initWithProject:(FlutterDartProject*)project
116  nibName:(NSString*)nibName
117  bundle:(NSBundle*)nibBundle {
118  self = [super initWithNibName:nibName bundle:nibBundle];
119  if (self) {
120  [self sharedSetupWithProject:project initialRoute:nil];
121  }
122 
123  return self;
124 }
125 
126 - (instancetype)initWithProject:(FlutterDartProject*)project
127  initialRoute:(NSString*)initialRoute
128  nibName:(NSString*)nibName
129  bundle:(NSBundle*)nibBundle {
130  self = [super initWithNibName:nibName bundle:nibBundle];
131  if (self) {
132  [self sharedSetupWithProject:project initialRoute:initialRoute];
133  }
134 
135  return self;
136 }
137 
138 - (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil {
139  return [self initWithProject:nil nibName:nil bundle:nil];
140 }
141 
142 - (instancetype)initWithCoder:(NSCoder*)aDecoder {
143  self = [super initWithCoder:aDecoder];
144  return self;
145 }
146 
147 - (void)awakeFromNib {
148  [super awakeFromNib];
149  if (!_engine) {
150  [self sharedSetupWithProject:nil initialRoute:nil];
151  }
152 }
153 
154 - (instancetype)init {
155  return [self initWithProject:nil nibName:nil bundle:nil];
156 }
157 
158 - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
159  initialRoute:(nullable NSString*)initialRoute {
160  // Need the project to get settings for the view. Initializing it here means
161  // the Engine class won't initialize it later.
162  if (!project) {
163  project = [[[FlutterDartProject alloc] init] autorelease];
164  }
165  FlutterView.forceSoftwareRendering = project.settings.enable_software_rendering;
167  initWithName:@"io.flutter"
168  project:project
169  allowHeadlessExecution:self.engineAllowHeadlessExecution]};
170 
171  if (!engine) {
172  return;
173  }
174 
175  _viewOpaque = YES;
176  _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self);
177  _engine = std::move(engine);
178  _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]);
179  [_engine.get() createShell:nil libraryURI:nil initialRoute:initialRoute];
180  _engineNeedsLaunch = YES;
181  _ongoingTouches.reset([[NSMutableSet alloc] init]);
183  [self performCommonViewControllerInitialization];
184 }
185 
186 - (BOOL)isViewOpaque {
187  return _viewOpaque;
188 }
189 
190 - (void)setViewOpaque:(BOOL)value {
191  _viewOpaque = value;
192  if (_flutterView.get().layer.opaque != value) {
193  _flutterView.get().layer.opaque = value;
194  [_flutterView.get().layer setNeedsLayout];
195  }
196 }
197 
198 #pragma mark - Common view controller initialization tasks
199 
200 - (void)performCommonViewControllerInitialization {
201  if (_initialized)
202  return;
203 
204  _initialized = YES;
205 
206  _orientationPreferences = UIInterfaceOrientationMaskAll;
207  _statusBarStyle = UIStatusBarStyleDefault;
208 
209  [self setupNotificationCenterObservers];
210 }
211 
212 - (FlutterEngine*)engine {
213  return _engine.get();
214 }
215 
217  return _weakFactory->GetWeakPtr();
218 }
219 
220 - (void)setupNotificationCenterObservers {
221  NSNotificationCenter* center = [NSNotificationCenter defaultCenter];
222  [center addObserver:self
223  selector:@selector(onOrientationPreferencesUpdated:)
224  name:@(flutter::kOrientationUpdateNotificationName)
225  object:nil];
226 
227  [center addObserver:self
228  selector:@selector(onPreferredStatusBarStyleUpdated:)
229  name:@(flutter::kOverlayStyleUpdateNotificationName)
230  object:nil];
231 
232  [center addObserver:self
233  selector:@selector(applicationBecameActive:)
234  name:UIApplicationDidBecomeActiveNotification
235  object:nil];
236 
237  [center addObserver:self
238  selector:@selector(applicationWillResignActive:)
239  name:UIApplicationWillResignActiveNotification
240  object:nil];
241 
242  [center addObserver:self
243  selector:@selector(applicationDidEnterBackground:)
244  name:UIApplicationDidEnterBackgroundNotification
245  object:nil];
246 
247  [center addObserver:self
248  selector:@selector(applicationWillEnterForeground:)
249  name:UIApplicationWillEnterForegroundNotification
250  object:nil];
251 
252  [center addObserver:self
253  selector:@selector(keyboardWillChangeFrame:)
254  name:UIKeyboardWillChangeFrameNotification
255  object:nil];
256 
257  [center addObserver:self
258  selector:@selector(keyboardWillBeHidden:)
259  name:UIKeyboardWillHideNotification
260  object:nil];
261 
262  [center addObserver:self
263  selector:@selector(onAccessibilityStatusChanged:)
264  name:UIAccessibilityVoiceOverStatusChanged
265  object:nil];
266 
267  [center addObserver:self
268  selector:@selector(onAccessibilityStatusChanged:)
269  name:UIAccessibilitySwitchControlStatusDidChangeNotification
270  object:nil];
271 
272  [center addObserver:self
273  selector:@selector(onAccessibilityStatusChanged:)
274  name:UIAccessibilitySpeakScreenStatusDidChangeNotification
275  object:nil];
276 
277  [center addObserver:self
278  selector:@selector(onAccessibilityStatusChanged:)
279  name:UIAccessibilityInvertColorsStatusDidChangeNotification
280  object:nil];
281 
282  [center addObserver:self
283  selector:@selector(onAccessibilityStatusChanged:)
284  name:UIAccessibilityReduceMotionStatusDidChangeNotification
285  object:nil];
286 
287  [center addObserver:self
288  selector:@selector(onAccessibilityStatusChanged:)
289  name:UIAccessibilityBoldTextStatusDidChangeNotification
290  object:nil];
291 
292  [center addObserver:self
293  selector:@selector(onAccessibilityStatusChanged:)
294  name:UIAccessibilityDarkerSystemColorsStatusDidChangeNotification
295  object:nil];
296 
297  [center addObserver:self
298  selector:@selector(onUserSettingsChanged:)
299  name:UIContentSizeCategoryDidChangeNotification
300  object:nil];
301 
302  [center addObserver:self
303  selector:@selector(onHideHomeIndicatorNotification:)
304  name:FlutterViewControllerHideHomeIndicator
305  object:nil];
306 
307  [center addObserver:self
308  selector:@selector(onShowHomeIndicatorNotification:)
309  name:FlutterViewControllerShowHomeIndicator
310  object:nil];
311 }
312 
313 - (void)setInitialRoute:(NSString*)route {
314  [[_engine.get() navigationChannel] invokeMethod:@"setInitialRoute" arguments:route];
315 }
316 
317 - (void)popRoute {
318  [[_engine.get() navigationChannel] invokeMethod:@"popRoute" arguments:nil];
319 }
320 
321 - (void)pushRoute:(NSString*)route {
322  [[_engine.get() navigationChannel] invokeMethod:@"pushRoute" arguments:route];
323 }
324 
325 #pragma mark - Loading the view
326 
327 static UIView* GetViewOrPlaceholder(UIView* existing_view) {
328  if (existing_view) {
329  return existing_view;
330  }
331 
332  auto placeholder = [[[UIView alloc] init] autorelease];
333 
334  placeholder.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
335  if (@available(iOS 13.0, *)) {
336  placeholder.backgroundColor = UIColor.systemBackgroundColor;
337  } else {
338  placeholder.backgroundColor = UIColor.whiteColor;
339  }
340  placeholder.autoresizesSubviews = YES;
341 
342  // Only add the label when we know we have failed to enable tracing (and it was necessary).
343  // Otherwise, a spurious warning will be shown in cases where an engine cannot be initialized for
344  // other reasons.
346  auto messageLabel = [[[UILabel alloc] init] autorelease];
347  messageLabel.numberOfLines = 0u;
348  messageLabel.textAlignment = NSTextAlignmentCenter;
349  messageLabel.autoresizingMask =
350  UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
351  messageLabel.text =
352  @"In iOS 14+, debug mode Flutter apps can only be launched from Flutter tooling, "
353  @"IDEs with Flutter plugins or from Xcode.\n\nAlternatively, build in profile or release "
354  @"modes to enable launching from the home screen.";
355  [placeholder addSubview:messageLabel];
356  }
357 
358  return placeholder;
359 }
360 
361 - (void)loadView {
362  self.view = GetViewOrPlaceholder(_flutterView.get());
363  self.view.multipleTouchEnabled = YES;
364  self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
365 
366  [self installSplashScreenViewIfNecessary];
367  UIScrollView* scrollView = [[UIScrollView alloc] init];
368  scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
369  // The color shouldn't matter since it is offscreen.
370  scrollView.backgroundColor = UIColor.whiteColor;
371  scrollView.delegate = self;
372  // This is an arbitrary small size.
373  scrollView.contentSize = CGSizeMake(kScrollViewContentSize, kScrollViewContentSize);
374  // This is an arbitrary offset that is not CGPointZero.
375  scrollView.contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
376  [self.view addSubview:scrollView];
377  _scrollView.reset(scrollView);
378 }
379 
380 static void sendFakeTouchEvent(FlutterEngine* engine,
381  CGPoint location,
383  const CGFloat scale = [UIScreen mainScreen].scale;
384  flutter::PointerData pointer_data;
385  pointer_data.Clear();
386  pointer_data.physical_x = location.x * scale;
387  pointer_data.physical_y = location.y * scale;
389  pointer_data.time_stamp = [[NSDate date] timeIntervalSince1970] * kMicrosecondsPerSecond;
390  auto packet = std::make_unique<flutter::PointerDataPacket>(/*count=*/1);
391  pointer_data.change = change;
392  packet->SetPointerData(0, pointer_data);
393  [engine dispatchPointerDataPacket:std::move(packet)];
394 }
395 
396 - (BOOL)scrollViewShouldScrollToTop:(UIScrollView*)scrollView {
397  if (!_engine) {
398  return NO;
399  }
400  CGPoint statusBarPoint = CGPointZero;
401  sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kDown);
402  sendFakeTouchEvent(_engine.get(), statusBarPoint, flutter::PointerData::Change::kUp);
403  return NO;
404 }
405 
406 #pragma mark - Managing launch views
407 
408 - (void)installSplashScreenViewIfNecessary {
409  // Show the launch screen view again on top of the FlutterView if available.
410  // This launch screen view will be removed once the first Flutter frame is rendered.
411  if (_splashScreenView && (self.isBeingPresented || self.isMovingToParentViewController)) {
412  [_splashScreenView.get() removeFromSuperview];
414  return;
415  }
416 
417  // Use the property getter to initialize the default value.
418  UIView* splashScreenView = self.splashScreenView;
419  if (splashScreenView == nil) {
420  return;
421  }
422  splashScreenView.frame = self.view.bounds;
423  [self.view addSubview:splashScreenView];
424 }
425 
426 + (BOOL)automaticallyNotifiesObserversOfDisplayingFlutterUI {
427  return NO;
428 }
429 
430 - (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI {
431  if (_displayingFlutterUI != displayingFlutterUI) {
432  if (displayingFlutterUI == YES) {
433  if (!self.isViewLoaded || !self.view.window) {
434  return;
435  }
436  }
437  [self willChangeValueForKey:@"displayingFlutterUI"];
438  _displayingFlutterUI = displayingFlutterUI;
439  [self didChangeValueForKey:@"displayingFlutterUI"];
440  }
441 }
442 
443 - (void)callViewRenderedCallback {
444  self.displayingFlutterUI = YES;
445  if (_flutterViewRenderedCallback != nil) {
448  }
449 }
450 
451 - (void)removeSplashScreenView:(dispatch_block_t _Nullable)onComplete {
452  NSAssert(_splashScreenView, @"The splash screen view must not be null");
453  UIView* splashScreen = _splashScreenView.get();
455  [UIView animateWithDuration:0.2
456  animations:^{
457  splashScreen.alpha = 0;
458  }
459  completion:^(BOOL finished) {
460  [splashScreen removeFromSuperview];
461  if (onComplete) {
462  onComplete();
463  }
464  }];
465 }
466 
467 - (void)installFirstFrameCallback {
468  if (!_engine) {
469  return;
470  }
471 
472  fml::WeakPtr<flutter::PlatformViewIOS> weakPlatformView = [_engine.get() platformView];
473  if (!weakPlatformView) {
474  return;
475  }
476 
477  // Start on the platform thread.
478  weakPlatformView->SetNextFrameCallback([weakSelf = [self getWeakPtr],
479  platformTaskRunner = [_engine.get() platformTaskRunner],
480  RasterTaskRunner = [_engine.get() RasterTaskRunner]]() {
481  FML_DCHECK(RasterTaskRunner->RunsTasksOnCurrentThread());
482  // Get callback on raster thread and jump back to platform thread.
483  platformTaskRunner->PostTask([weakSelf]() {
484  fml::scoped_nsobject<FlutterViewController> flutterViewController(
485  [(FlutterViewController*)weakSelf.get() retain]);
486  if (flutterViewController) {
487  if (flutterViewController.get()->_splashScreenView) {
488  [flutterViewController removeSplashScreenView:^{
489  [flutterViewController callViewRenderedCallback];
490  }];
491  } else {
492  [flutterViewController callViewRenderedCallback];
493  }
494  }
495  });
496  });
497 }
498 
499 #pragma mark - Properties
500 
501 - (UIView*)splashScreenView {
502  if (!_splashScreenView) {
503  return nil;
504  }
505  return _splashScreenView.get();
506 }
507 
508 - (BOOL)loadDefaultSplashScreenView {
509  NSString* launchscreenName =
510  [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"];
511  if (launchscreenName == nil) {
512  return NO;
513  }
514  UIView* splashView = [self splashScreenFromStoryboard:launchscreenName];
515  if (!splashView) {
516  splashView = [self splashScreenFromXib:launchscreenName];
517  }
518  if (!splashView) {
519  return NO;
520  }
521  self.splashScreenView = splashView;
522  return YES;
523 }
524 
525 - (UIView*)splashScreenFromStoryboard:(NSString*)name {
526  UIStoryboard* storyboard = nil;
527  @try {
528  storyboard = [UIStoryboard storyboardWithName:name bundle:nil];
529  } @catch (NSException* exception) {
530  return nil;
531  }
532  if (storyboard) {
533  UIViewController* splashScreenViewController = [storyboard instantiateInitialViewController];
534  return splashScreenViewController.view;
535  }
536  return nil;
537 }
538 
539 - (UIView*)splashScreenFromXib:(NSString*)name {
540  NSArray* objects = nil;
541  @try {
542  objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil];
543  } @catch (NSException* exception) {
544  return nil;
545  }
546  if ([objects count] != 0) {
547  UIView* view = [objects objectAtIndex:0];
548  return view;
549  }
550  return nil;
551 }
552 
553 - (void)setSplashScreenView:(UIView*)view {
554  if (!view) {
555  // Special case: user wants to remove the splash screen view.
556  if (_splashScreenView) {
557  [self removeSplashScreenView:nil];
558  }
559  return;
560  }
561 
562  _splashScreenView.reset([view retain]);
563  _splashScreenView.get().autoresizingMask =
564  UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
565 }
566 
567 - (void)setFlutterViewDidRenderCallback:(void (^)(void))callback {
569 }
570 
571 #pragma mark - Surface creation and teardown updates
572 
573 - (void)surfaceUpdated:(BOOL)appeared {
574  if (!_engine) {
575  return;
576  }
577 
578  // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and raster
579  // thread.
580  if (appeared) {
581  [self installFirstFrameCallback];
582  [_engine.get() platformViewsController]->SetFlutterView(_flutterView.get());
583  [_engine.get() platformViewsController]->SetFlutterViewController(self);
584  [_engine.get() platformView]->NotifyCreated();
585  } else {
586  self.displayingFlutterUI = NO;
587  [_engine.get() platformView]->NotifyDestroyed();
588  [_engine.get() platformViewsController]->SetFlutterView(nullptr);
589  [_engine.get() platformViewsController]->SetFlutterViewController(nullptr);
590  }
591 }
592 
593 #pragma mark - UIViewController lifecycle notifications
594 
595 - (void)viewDidLoad {
596  TRACE_EVENT0("flutter", "viewDidLoad");
597 
598  if (_engine && _engineNeedsLaunch) {
599  [_engine.get() launchEngine:nil libraryURI:nil];
600  [_engine.get() setViewController:self];
601  _engineNeedsLaunch = NO;
602  }
603 
604  [_engine.get() attachView];
605 
606  [super viewDidLoad];
607 }
608 
609 - (void)viewWillAppear:(BOOL)animated {
610  TRACE_EVENT0("flutter", "viewWillAppear");
611 
612  // Send platform settings to Flutter, e.g., platform brightness.
613  [self onUserSettingsChanged:nil];
614 
615  // Only recreate surface on subsequent appearances when viewport metrics are known.
616  // First time surface creation is done on viewDidLayoutSubviews.
618  [self surfaceUpdated:YES];
619  }
620  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
621 
622  [super viewWillAppear:animated];
623 }
624 
625 - (void)viewDidAppear:(BOOL)animated {
626  TRACE_EVENT0("flutter", "viewDidAppear");
627  [self onUserSettingsChanged:nil];
628  [self onAccessibilityStatusChanged:nil];
629  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"];
630 
631  [super viewDidAppear:animated];
632 }
633 
634 - (void)viewWillDisappear:(BOOL)animated {
635  TRACE_EVENT0("flutter", "viewWillDisappear");
636  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"];
637 
638  [super viewWillDisappear:animated];
639 }
640 
641 - (void)viewDidDisappear:(BOOL)animated {
642  TRACE_EVENT0("flutter", "viewDidDisappear");
643  if ([_engine.get() viewController] == self) {
644  [self surfaceUpdated:NO];
645  [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"];
646  [self flushOngoingTouches];
647  [_engine.get() notifyLowMemory];
648  }
649 
650  [super viewDidDisappear:animated];
651 }
652 
653 - (void)flushOngoingTouches {
654  if (_engine && _ongoingTouches.get().count > 0) {
655  auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.get().count);
656  size_t pointer_index = 0;
657  // If the view controller is going away, we want to flush cancel all the ongoing
658  // touches to the framework so nothing gets orphaned.
659  for (NSNumber* device in _ongoingTouches.get()) {
660  // Create fake PointerData to balance out each previously started one for the framework.
661  flutter::PointerData pointer_data;
662  pointer_data.Clear();
663 
664  // Use current time.
665  pointer_data.time_stamp = [[NSDate date] timeIntervalSince1970] * kMicrosecondsPerSecond;
666 
669  pointer_data.device = device.longLongValue;
670  pointer_data.pointer_identifier = 0;
671 
672  // Anything we put here will be arbitrary since there are no touches.
673  pointer_data.physical_x = 0;
674  pointer_data.physical_y = 0;
675  pointer_data.physical_delta_x = 0.0;
676  pointer_data.physical_delta_y = 0.0;
677  pointer_data.pressure = 1.0;
678  pointer_data.pressure_max = 1.0;
679 
680  packet->SetPointerData(pointer_index++, pointer_data);
681  }
682 
683  [_ongoingTouches removeAllObjects];
684  [_engine.get() dispatchPointerDataPacket:std::move(packet)];
685  }
686 }
687 
688 - (void)dealloc {
689  [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerWillDealloc
690  object:self
691  userInfo:nil];
692  [[NSNotificationCenter defaultCenter] removeObserver:self];
693  [super dealloc];
694 }
695 
696 #pragma mark - Application lifecycle notifications
697 
698 - (void)applicationBecameActive:(NSNotification*)notification {
699  TRACE_EVENT0("flutter", "applicationBecameActive");
701  [self surfaceUpdated:YES];
702  [self goToApplicationLifecycle:@"AppLifecycleState.resumed"];
703 }
704 
705 - (void)applicationWillResignActive:(NSNotification*)notification {
706  TRACE_EVENT0("flutter", "applicationWillResignActive");
707  [self surfaceUpdated:NO];
708  [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
709 }
710 
711 - (void)applicationDidEnterBackground:(NSNotification*)notification {
712  TRACE_EVENT0("flutter", "applicationDidEnterBackground");
713  [self goToApplicationLifecycle:@"AppLifecycleState.paused"];
714 }
715 
716 - (void)applicationWillEnterForeground:(NSNotification*)notification {
717  TRACE_EVENT0("flutter", "applicationWillEnterForeground");
718  [self goToApplicationLifecycle:@"AppLifecycleState.inactive"];
719 }
720 
721 // Make this transition only while this current view controller is visible.
722 - (void)goToApplicationLifecycle:(nonnull NSString*)state {
723  // Accessing self.view will create the view. Check whether the view is organically loaded
724  // first before checking whether the view is attached to window.
725  if (self.isViewLoaded && self.view.window) {
726  [[_engine.get() lifecycleChannel] sendMessage:state];
727  }
728 }
729 
730 #pragma mark - Touch event handling
731 
732 static flutter::PointerData::Change PointerDataChangeFromUITouchPhase(UITouchPhase phase) {
733  switch (phase) {
734  case UITouchPhaseBegan:
736  case UITouchPhaseMoved:
737  case UITouchPhaseStationary:
738  // There is no EVENT_TYPE_POINTER_STATIONARY. So we just pass a move type
739  // with the same coordinates
741  case UITouchPhaseEnded:
743  case UITouchPhaseCancelled:
745  default:
746  // TODO(53695): Handle the `UITouchPhaseRegion`... enum values.
747  FML_DLOG(INFO) << "Unhandled touch phase: " << phase;
748  break;
749  }
750 
752 }
753 
754 static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) {
755  if (@available(iOS 9, *)) {
756  switch (touch.type) {
757  case UITouchTypeDirect:
758  case UITouchTypeIndirect:
760  case UITouchTypeStylus:
762  default:
763  // TODO(53696): Handle the UITouchTypeIndirectPointer enum value.
764  FML_DLOG(INFO) << "Unhandled touch type: " << touch.type;
765  break;
766  }
767  }
768 
770 }
771 
772 // Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined
773 // from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events
774 // in the status bar area are available to framework code. The change type (optional) of the faked
775 // touch is specified in the second argument.
776 - (void)dispatchTouches:(NSSet*)touches
777  pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change {
778  if (!_engine) {
779  return;
780  }
781 
782  const CGFloat scale = [UIScreen mainScreen].scale;
783  auto packet = std::make_unique<flutter::PointerDataPacket>(touches.count);
784 
785  size_t pointer_index = 0;
786 
787  for (UITouch* touch in touches) {
788  CGPoint windowCoordinates = [touch locationInView:self.view];
789 
790  flutter::PointerData pointer_data;
791  pointer_data.Clear();
792 
793  constexpr int kMicrosecondsPerSecond = 1000 * 1000;
794  pointer_data.time_stamp = touch.timestamp * kMicrosecondsPerSecond;
795 
796  pointer_data.change = overridden_change != nullptr
797  ? *overridden_change
798  : PointerDataChangeFromUITouchPhase(touch.phase);
799 
800  pointer_data.kind = DeviceKindFromTouchType(touch);
801 
802  pointer_data.device = reinterpret_cast<int64_t>(touch);
803 
804  // Pointer will be generated in pointer_data_packet_converter.cc.
805  pointer_data.pointer_identifier = 0;
806 
807  pointer_data.physical_x = windowCoordinates.x * scale;
808  pointer_data.physical_y = windowCoordinates.y * scale;
809 
810  // Delta will be generated in pointer_data_packet_converter.cc.
811  pointer_data.physical_delta_x = 0.0;
812  pointer_data.physical_delta_y = 0.0;
813 
814  NSNumber* deviceKey = [NSNumber numberWithLongLong:pointer_data.device];
815  // Track touches that began and not yet stopped so we can flush them
816  // if the view controller goes away.
817  switch (pointer_data.change) {
819  [_ongoingTouches addObject:deviceKey];
820  break;
823  [_ongoingTouches removeObject:deviceKey];
824  break;
827  // We're only tracking starts and stops.
828  break;
831  // We don't use kAdd/kRemove.
832  break;
833  }
834 
835  // pressure_min is always 0.0
836  if (@available(iOS 9, *)) {
837  // These properties were introduced in iOS 9.0.
838  pointer_data.pressure = touch.force;
839  pointer_data.pressure_max = touch.maximumPossibleForce;
840  } else {
841  pointer_data.pressure = 1.0;
842  pointer_data.pressure_max = 1.0;
843  }
844 
845  // These properties were introduced in iOS 8.0
846  pointer_data.radius_major = touch.majorRadius;
847  pointer_data.radius_min = touch.majorRadius - touch.majorRadiusTolerance;
848  pointer_data.radius_max = touch.majorRadius + touch.majorRadiusTolerance;
849 
850  // These properties were introduced in iOS 9.1
851  if (@available(iOS 9.1, *)) {
852  // iOS Documentation: altitudeAngle
853  // A value of 0 radians indicates that the stylus is parallel to the surface. The value of
854  // this property is Pi/2 when the stylus is perpendicular to the surface.
855  //
856  // PointerData Documentation: tilt
857  // The angle of the stylus, in radians in the range:
858  // 0 <= tilt <= pi/2
859  // giving the angle of the axis of the stylus, relative to the axis perpendicular to the input
860  // surface (thus 0.0 indicates the stylus is orthogonal to the plane of the input surface,
861  // while pi/2 indicates that the stylus is flat on that surface).
862  //
863  // Discussion:
864  // The ranges are the same. Origins are swapped.
865  pointer_data.tilt = M_PI_2 - touch.altitudeAngle;
866 
867  // iOS Documentation: azimuthAngleInView:
868  // With the tip of the stylus touching the screen, the value of this property is 0 radians
869  // when the cap end of the stylus (that is, the end opposite of the tip) points along the
870  // positive x axis of the device's screen. The azimuth angle increases as the user swings the
871  // cap end of the stylus in a clockwise direction around the tip.
872  //
873  // PointerData Documentation: orientation
874  // The angle of the stylus, in radians in the range:
875  // -pi < orientation <= pi
876  // giving the angle of the axis of the stylus projected onto the input surface, relative to
877  // the positive y-axis of that surface (thus 0.0 indicates the stylus, if projected onto that
878  // surface, would go from the contact point vertically up in the positive y-axis direction, pi
879  // would indicate that the stylus would go down in the negative y-axis direction; pi/4 would
880  // indicate that the stylus goes up and to the right, -pi/2 would indicate that the stylus
881  // goes to the left, etc).
882  //
883  // Discussion:
884  // Sweep direction is the same. Phase of M_PI_2.
885  pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2;
886  }
887 
888  packet->SetPointerData(pointer_index++, pointer_data);
889  }
890 
891  [_engine.get() dispatchPointerDataPacket:std::move(packet)];
892 }
893 
894 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
895  [self dispatchTouches:touches pointerDataChangeOverride:nullptr];
896 }
897 
898 - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event {
899  [self dispatchTouches:touches pointerDataChangeOverride:nullptr];
900 }
901 
902 - (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event {
903  [self dispatchTouches:touches pointerDataChangeOverride:nullptr];
904 }
905 
906 - (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event {
907  [self dispatchTouches:touches pointerDataChangeOverride:nullptr];
908 }
909 
910 #pragma mark - Handle view resizing
911 
912 - (void)updateViewportMetrics {
913  [_engine.get() updateViewportMetrics:_viewportMetrics];
914 }
915 
916 - (CGFloat)statusBarPadding {
917  UIScreen* screen = self.view.window.screen;
918  CGRect statusFrame = [UIApplication sharedApplication].statusBarFrame;
919  CGRect viewFrame = [self.view convertRect:self.view.bounds
920  toCoordinateSpace:screen.coordinateSpace];
921  CGRect intersection = CGRectIntersection(statusFrame, viewFrame);
922  return CGRectIsNull(intersection) ? 0.0 : intersection.size.height;
923 }
924 
925 - (void)viewDidLayoutSubviews {
926  CGSize viewSize = self.view.bounds.size;
927  CGFloat scale = [UIScreen mainScreen].scale;
928 
929  // Purposefully place this not visible.
930  _scrollView.get().frame = CGRectMake(0.0, 0.0, viewSize.width, 0.0);
931  _scrollView.get().contentOffset = CGPointMake(kScrollViewContentSize, kScrollViewContentSize);
932 
933  // First time since creation that the dimensions of its view is known.
934  bool firstViewBoundsUpdate = !_viewportMetrics.physical_width;
936  _viewportMetrics.physical_width = viewSize.width * scale;
937  _viewportMetrics.physical_height = viewSize.height * scale;
938 
939  [self updateViewportPadding];
940  [self updateViewportMetrics];
941 
942  // There is no guarantee that UIKit will layout subviews when the application is active. Creating
943  // the surface when inactive will cause GPU accesses from the background. Only wait for the first
944  // frame to render when the application is actually active.
945  bool applicationIsActive =
946  [UIApplication sharedApplication].applicationState == UIApplicationStateActive;
947 
948  // This must run after updateViewportMetrics so that the surface creation tasks are queued after
949  // the viewport metrics update tasks.
950  if (firstViewBoundsUpdate && applicationIsActive && _engine) {
951  [self surfaceUpdated:YES];
952 
953  flutter::Shell& shell = [_engine.get() shell];
954  fml::TimeDelta waitTime =
955 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
957 #else
959 #endif
960  if (shell.WaitForFirstFrame(waitTime).code() == fml::StatusCode::kDeadlineExceeded) {
961  FML_LOG(INFO) << "Timeout waiting for the first frame to render. This may happen in "
962  << "unoptimized builds. If this is a release build, you should load a less "
963  << "complex frame to avoid the timeout.";
964  }
965  }
966 }
967 
968 - (void)viewSafeAreaInsetsDidChange {
969  [self updateViewportPadding];
970  [self updateViewportMetrics];
971  [super viewSafeAreaInsetsDidChange];
972 }
973 
974 // Updates _viewportMetrics physical padding.
975 //
976 // Viewport padding represents the iOS safe area insets.
977 - (void)updateViewportPadding {
978  CGFloat scale = [UIScreen mainScreen].scale;
979  if (@available(iOS 11, *)) {
980  _viewportMetrics.physical_padding_top = self.view.safeAreaInsets.top * scale;
981  _viewportMetrics.physical_padding_left = self.view.safeAreaInsets.left * scale;
982  _viewportMetrics.physical_padding_right = self.view.safeAreaInsets.right * scale;
983  _viewportMetrics.physical_padding_bottom = self.view.safeAreaInsets.bottom * scale;
984  } else {
985  _viewportMetrics.physical_padding_top = [self statusBarPadding] * scale;
986  }
987 }
988 
989 #pragma mark - Keyboard events
990 
991 - (void)keyboardWillChangeFrame:(NSNotification*)notification {
992  NSDictionary* info = [notification userInfo];
993 
994  if (@available(iOS 9, *)) {
995  // Ignore keyboard notifications related to other apps.
996  id isLocal = info[UIKeyboardIsLocalUserInfoKey];
997  if (isLocal && ![isLocal boolValue]) {
998  return;
999  }
1000  }
1001 
1002  CGRect keyboardFrame = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
1003  CGRect screenRect = [[UIScreen mainScreen] bounds];
1004 
1005  // Considering the iPad's split keyboard, Flutter needs to check if the keyboard frame is present
1006  // in the screen to see if the keyboard is visible.
1007  if (CGRectIntersectsRect(keyboardFrame, screenRect)) {
1008  CGFloat bottom = CGRectGetHeight(keyboardFrame);
1009  CGFloat scale = [UIScreen mainScreen].scale;
1010 
1011  // The keyboard is treated as an inset since we want to effectively reduce the window size by
1012  // the keyboard height. The Dart side will compute a value accounting for the keyboard-consuming
1013  // bottom padding.
1015  } else {
1017  }
1018 
1019  [self updateViewportMetrics];
1020 }
1021 
1022 - (void)keyboardWillBeHidden:(NSNotification*)notification {
1024  [self updateViewportMetrics];
1025 }
1026 
1027 #pragma mark - Orientation updates
1028 
1029 - (void)onOrientationPreferencesUpdated:(NSNotification*)notification {
1030  // Notifications may not be on the iOS UI thread
1031  dispatch_async(dispatch_get_main_queue(), ^{
1032  NSDictionary* info = notification.userInfo;
1033 
1034  NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)];
1035 
1036  if (update == nil) {
1037  return;
1038  }
1039  [self performOrientationUpdate:update.unsignedIntegerValue];
1040  });
1041 }
1042 
1043 - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences {
1044  if (new_preferences != _orientationPreferences) {
1045  _orientationPreferences = new_preferences;
1046  [UIViewController attemptRotationToDeviceOrientation];
1047 
1048  UIInterfaceOrientationMask currentInterfaceOrientation =
1049  1 << [[UIApplication sharedApplication] statusBarOrientation];
1050  if (!(_orientationPreferences & currentInterfaceOrientation)) {
1051  // Force orientation switch if the current orientation is not allowed
1052  if (_orientationPreferences & UIInterfaceOrientationMaskPortrait) {
1053  // This is no official API but more like a workaround / hack (using
1054  // key-value coding on a read-only property). This might break in
1055  // the future, but currently it´s the only way to force an orientation change
1056  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortrait) forKey:@"orientation"];
1057  } else if (_orientationPreferences & UIInterfaceOrientationMaskPortraitUpsideDown) {
1058  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationPortraitUpsideDown)
1059  forKey:@"orientation"];
1060  } else if (_orientationPreferences & UIInterfaceOrientationMaskLandscapeLeft) {
1061  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeLeft)
1062  forKey:@"orientation"];
1063  } else if (_orientationPreferences & UIInterfaceOrientationMaskLandscapeRight) {
1064  [[UIDevice currentDevice] setValue:@(UIInterfaceOrientationLandscapeRight)
1065  forKey:@"orientation"];
1066  }
1067  }
1068  }
1069 }
1070 
1071 - (void)onHideHomeIndicatorNotification:(NSNotification*)notification {
1072  self.isHomeIndicatorHidden = YES;
1073 }
1074 
1075 - (void)onShowHomeIndicatorNotification:(NSNotification*)notification {
1076  self.isHomeIndicatorHidden = NO;
1077 }
1078 
1079 - (void)setIsHomeIndicatorHidden:(BOOL)hideHomeIndicator {
1080  if (hideHomeIndicator != _isHomeIndicatorHidden) {
1081  _isHomeIndicatorHidden = hideHomeIndicator;
1082  if (@available(iOS 11, *)) {
1083  [self setNeedsUpdateOfHomeIndicatorAutoHidden];
1084  }
1085  }
1086 }
1087 
1088 - (BOOL)prefersHomeIndicatorAutoHidden {
1089  return self.isHomeIndicatorHidden;
1090 }
1091 
1092 - (BOOL)shouldAutorotate {
1093  return YES;
1094 }
1095 
1096 - (NSUInteger)supportedInterfaceOrientations {
1097  return _orientationPreferences;
1098 }
1099 
1100 #pragma mark - Accessibility
1101 
1102 - (void)onAccessibilityStatusChanged:(NSNotification*)notification {
1103  if (!_engine) {
1104  return;
1105  }
1106  auto platformView = [_engine.get() platformView];
1107  int32_t flags = 0;
1108  if (UIAccessibilityIsInvertColorsEnabled())
1109  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kInvertColors);
1110  if (UIAccessibilityIsReduceMotionEnabled())
1111  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kReduceMotion);
1112  if (UIAccessibilityIsBoldTextEnabled())
1113  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kBoldText);
1114  if (UIAccessibilityDarkerSystemColorsEnabled())
1115  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kHighContrast);
1116 #if TARGET_OS_SIMULATOR
1117  // There doesn't appear to be any way to determine whether the accessibility
1118  // inspector is enabled on the simulator. We conservatively always turn on the
1119  // accessibility bridge in the simulator, but never assistive technology.
1120  platformView->SetSemanticsEnabled(true);
1121  platformView->SetAccessibilityFeatures(flags);
1122 #else
1123  bool enabled = UIAccessibilityIsVoiceOverRunning() || UIAccessibilityIsSwitchControlRunning();
1124  if (enabled)
1125  flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kAccessibleNavigation);
1126  platformView->SetSemanticsEnabled(enabled || UIAccessibilityIsSpeakScreenEnabled());
1127  platformView->SetAccessibilityFeatures(flags);
1128 #endif
1129 }
1130 
1131 #pragma mark - Set user settings
1132 
1133 - (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection {
1134  [super traitCollectionDidChange:previousTraitCollection];
1135  [self onUserSettingsChanged:nil];
1136 }
1137 
1138 - (void)onUserSettingsChanged:(NSNotification*)notification {
1139  [[_engine.get() settingsChannel] sendMessage:@{
1140  @"textScaleFactor" : @([self textScaleFactor]),
1141  @"alwaysUse24HourFormat" : @([self isAlwaysUse24HourFormat]),
1142  @"platformBrightness" : [self brightnessMode],
1143  @"platformContrast" : [self contrastMode]
1144  }];
1145 }
1146 
1147 - (CGFloat)textScaleFactor {
1148  UIContentSizeCategory category = [UIApplication sharedApplication].preferredContentSizeCategory;
1149  // The delta is computed by approximating Apple's typography guidelines:
1150  // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/
1151  //
1152  // Specifically:
1153  // Non-accessibility sizes for "body" text are:
1154  const CGFloat xs = 14;
1155  const CGFloat s = 15;
1156  const CGFloat m = 16;
1157  const CGFloat l = 17;
1158  const CGFloat xl = 19;
1159  const CGFloat xxl = 21;
1160  const CGFloat xxxl = 23;
1161 
1162  // Accessibility sizes for "body" text are:
1163  const CGFloat ax1 = 28;
1164  const CGFloat ax2 = 33;
1165  const CGFloat ax3 = 40;
1166  const CGFloat ax4 = 47;
1167  const CGFloat ax5 = 53;
1168 
1169  // We compute the scale as relative difference from size L (large, the default size), where
1170  // L is assumed to have scale 1.0.
1171  if ([category isEqualToString:UIContentSizeCategoryExtraSmall])
1172  return xs / l;
1173  else if ([category isEqualToString:UIContentSizeCategorySmall])
1174  return s / l;
1175  else if ([category isEqualToString:UIContentSizeCategoryMedium])
1176  return m / l;
1177  else if ([category isEqualToString:UIContentSizeCategoryLarge])
1178  return 1.0;
1179  else if ([category isEqualToString:UIContentSizeCategoryExtraLarge])
1180  return xl / l;
1181  else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge])
1182  return xxl / l;
1183  else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge])
1184  return xxxl / l;
1185  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium])
1186  return ax1 / l;
1187  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge])
1188  return ax2 / l;
1189  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge])
1190  return ax3 / l;
1191  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge])
1192  return ax4 / l;
1193  else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge])
1194  return ax5 / l;
1195  else
1196  return 1.0;
1197 }
1198 
1199 - (BOOL)isAlwaysUse24HourFormat {
1200  // iOS does not report its "24-Hour Time" user setting in the API. Instead, it applies
1201  // it automatically to NSDateFormatter when used with [NSLocale currentLocale]. It is
1202  // essential that [NSLocale currentLocale] is used. Any custom locale, even the one
1203  // that's the same as [NSLocale currentLocale] will ignore the 24-hour option (there
1204  // must be some internal field that's not exposed to developers).
1205  //
1206  // Therefore this option behaves differently across Android and iOS. On Android this
1207  // setting is exposed standalone, and can therefore be applied to all locales, whether
1208  // the "current system locale" or a custom one. On iOS it only applies to the current
1209  // system locale. Widget implementors must take this into account in order to provide
1210  // platform-idiomatic behavior in their widgets.
1211  NSString* dateFormat = [NSDateFormatter dateFormatFromTemplate:@"j"
1212  options:0
1213  locale:[NSLocale currentLocale]];
1214  return [dateFormat rangeOfString:@"a"].location == NSNotFound;
1215 }
1216 
1217 // The brightness mode of the platform, e.g., light or dark, expressed as a string that
1218 // is understood by the Flutter framework. See the settings system channel for more
1219 // information.
1220 - (NSString*)brightnessMode {
1221  if (@available(iOS 13, *)) {
1222  UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle;
1223 
1224  if (style == UIUserInterfaceStyleDark) {
1225  return @"dark";
1226  } else {
1227  return @"light";
1228  }
1229  } else {
1230  return @"light";
1231  }
1232 }
1233 
1234 // The contrast mode of the platform, e.g., normal or high, expressed as a string that is
1235 // understood by the Flutter framework. See the settings system channel for more
1236 // information.
1237 - (NSString*)contrastMode {
1238  if (@available(iOS 13, *)) {
1239  UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast;
1240 
1241  if (contrast == UIAccessibilityContrastHigh) {
1242  return @"high";
1243  } else {
1244  return @"normal";
1245  }
1246  } else {
1247  return @"normal";
1248  }
1249 }
1250 
1251 #pragma mark - Status bar style
1252 
1253 - (UIStatusBarStyle)preferredStatusBarStyle {
1254  return _statusBarStyle;
1255 }
1256 
1257 - (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification {
1258  // Notifications may not be on the iOS UI thread
1259  dispatch_async(dispatch_get_main_queue(), ^{
1260  NSDictionary* info = notification.userInfo;
1261 
1262  NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)];
1263 
1264  if (update == nil) {
1265  return;
1266  }
1267 
1268  NSInteger style = update.integerValue;
1269 
1270  if (style != _statusBarStyle) {
1271  _statusBarStyle = static_cast<UIStatusBarStyle>(style);
1272  [self setNeedsStatusBarAppearanceUpdate];
1273  }
1274  });
1275 }
1276 
1277 #pragma mark - Platform views
1278 
1279 - (flutter::FlutterPlatformViewsController*)platformViewsController {
1280  return [_engine.get() platformViewsController];
1281 }
1282 
1283 - (NSObject<FlutterBinaryMessenger>*)binaryMessenger {
1284  return _engine.get().binaryMessenger;
1285 }
1286 
1287 #pragma mark - FlutterBinaryMessenger
1288 
1289 - (void)sendOnChannel:(NSString*)channel message:(NSData*)message {
1290  [_engine.get().binaryMessenger sendOnChannel:channel message:message];
1291 }
1292 
1293 - (void)sendOnChannel:(NSString*)channel
1294  message:(NSData*)message
1295  binaryReply:(FlutterBinaryReply)callback {
1296  NSAssert(channel, @"The channel must not be null");
1297  [_engine.get().binaryMessenger sendOnChannel:channel message:message binaryReply:callback];
1298 }
1299 
1300 - (FlutterBinaryMessengerConnection)setMessageHandlerOnChannel:(NSString*)channel
1301  binaryMessageHandler:
1302  (FlutterBinaryMessageHandler)handler {
1303  NSAssert(channel, @"The channel must not be null");
1304  return [_engine.get().binaryMessenger setMessageHandlerOnChannel:channel
1305  binaryMessageHandler:handler];
1306 }
1307 
1308 - (void)cleanupConnection:(FlutterBinaryMessengerConnection)connection {
1309  [_engine.get().binaryMessenger cleanupConnection:connection];
1310 }
1311 
1312 #pragma mark - FlutterTextureRegistry
1313 
1314 - (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture {
1315  return [_engine.get() registerTexture:texture];
1316 }
1317 
1318 - (void)unregisterTexture:(int64_t)textureId {
1319  [_engine.get() unregisterTexture:textureId];
1320 }
1321 
1322 - (void)textureFrameAvailable:(int64_t)textureId {
1323  [_engine.get() textureFrameAvailable:textureId];
1324 }
1325 
1326 - (NSString*)lookupKeyForAsset:(NSString*)asset {
1327  return [FlutterDartProject lookupKeyForAsset:asset];
1328 }
1329 
1330 - (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
1331  return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
1332 }
1333 
1334 - (id<FlutterPluginRegistry>)pluginRegistry {
1335  return _engine;
1336 }
1337 
1338 #pragma mark - FlutterPluginRegistry
1339 
1340 - (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
1341  return [_engine.get() registrarForPlugin:pluginKey];
1342 }
1343 
1344 - (BOOL)hasPlugin:(NSString*)pluginKey {
1345  return [_engine.get() hasPlugin:pluginKey];
1346 }
1347 
1348 - (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
1349  return [_engine.get() valuePublishedByPlugin:pluginKey];
1350 }
1351 
1352 - (void)presentViewController:(UIViewController*)viewControllerToPresent
1353  animated:(BOOL)flag
1354  completion:(void (^)(void))completion {
1355  self.isPresentingViewControllerAnimating = YES;
1356  [super presentViewController:viewControllerToPresent
1357  animated:flag
1358  completion:^{
1359  self.isPresentingViewControllerAnimating = NO;
1360  if (completion) {
1361  completion();
1362  }
1363  }];
1364 }
1365 
1366 - (BOOL)isPresentingViewController {
1367  return self.presentedViewController != nil || self.isPresentingViewControllerAnimating;
1368 }
1369 
1370 @end
BOOL forceSoftwareRendering
Definition: FlutterView.h:37
static constexpr CGFloat kScrollViewContentSize
#define TRACE_EVENT0(category_group, name)
Definition: trace_event.h:75
std::unique_ptr< fml::WeakPtrFactory< FlutterEngine > > _weakFactory
#define FML_DCHECK(condition)
Definition: logging.h:86
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterBinaryReply)(NSData *_Nullable reply)
fml::scoped_nsobject< FlutterView > _flutterView
int64_t pointer_identifier
Definition: pointer_data.h:67
void reset(NST object=nil)
flutter::ViewportMetrics _viewportMetrics
#define FML_LOG(severity)
Definition: logging.h:65
NSNotificationName const FlutterViewControllerShowHomeIndicator
void SetNextFrameCallback(const fml::closure &closure)
Sets a callback that gets executed when the rasterizer renders the next frame. Due to the asynchronou...
BOOL _engineNeedsLaunch
void(^ FlutterBinaryMessageHandler)(NSData *_Nullable message, FlutterBinaryReply reply)
fml::scoped_nsobject< FlutterEngine > _engine
UIStatusBarStyle _statusBarStyle
static constexpr int kMicrosecondsPerSecond
BOOL _initialized
uint8_t value
fml::scoped_nsobject< UIScrollView > _scrollView
fml::scoped_nsobject< UIView > _splashScreenView
FlutterPointerPhase phase
Definition: fl_view.cc:78
fml::scoped_nsobject< NSMutableSet< NSNumber * > > _ongoingTouches
fml::ScopedBlock< void(^)(void)> _flutterViewRenderedCallback
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:1607
GdkEventButton * event
Definition: fl_view.cc:62
UIAccessibilityContrast accessibilityContrast()
NSString * lookupKeyForAsset:(NSString *asset)
BOOL _viewOpaque
const char * name
Definition: fuchsia.cc:50
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
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
NSNotificationName const FlutterViewControllerHideHomeIndicator
int64_t FlutterBinaryMessengerConnection
instancetype initWithProject:nibName:bundle:(nullable FlutterDartProject *project, [nibName] nullable NSString *nibName, [bundle] nullable NSBundle *NS_DESIGNATED_INITIALIZER)
UIInterfaceOrientationMask _orientationPreferences
FlutterViewController * viewController
DEF_SWITCHES_START snapshot asset Path to the directory containing the four files specified by VmSnapshotInstructions and IsolateSnapshotInstructions vm snapshot The VM instructions snapshot that will be memory mapped as read and executable SnapshotAssetPath must be present isolate snapshot The isolate instructions snapshot that will be memory mapped as read and executable SnapshotAssetPath must be present icu symbol Prefix for the symbols representing ICU data linked into the Flutter library dart flags
Definition: switches.h:66
NSNotificationName const FlutterSemanticsUpdateNotification