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/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/FlutterEngine_Internal.h" 19#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPlatformPlugin.h" 20#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputDelegate.h" 21#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" 22#import "flutter/shell/platform/darwin/ios/framework/Source/FlutterView.h" 23#import "flutter/shell/platform/darwin/ios/framework/Source/platform_message_response_darwin.h" 24#import "flutter/shell/platform/darwin/ios/platform_view_ios.h" 25 26NSNotificationName const FlutterSemanticsUpdateNotification = @"FlutterSemanticsUpdate"; 27 28// This is left a FlutterBinaryMessenger privately for now to give people a chance to notice the 29// change. Unfortunately unless you have Werror turned on, incompatible pointers as arguments are 30// just a warning. 31@interface FlutterViewController () <FlutterBinaryMessenger> 32@property(nonatomic, readwrite, getter=isDisplayingFlutterUI) BOOL displayingFlutterUI; 33@end 34 35// The following conditional compilation defines an API 13 concept on earlier API targets so that 36// a compiler compiling against API 12 or below does not blow up due to non-existent members. 37#if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000 38typedef enum UIAccessibilityContrast : NSInteger { 39 UIAccessibilityContrastUnspecified = 0, 40 UIAccessibilityContrastNormal = 1, 41 UIAccessibilityContrastHigh = 2 42} UIAccessibilityContrast; 43 44@interface UITraitCollection (MethodsFromNewerSDK) 45- (UIAccessibilityContrast)accessibilityContrast; 46@end 47#endif 48 49@implementation FlutterViewController { 50 std::unique_ptr<fml::WeakPtrFactory<FlutterViewController>> _weakFactory; 51 fml::scoped_nsobject<FlutterEngine> _engine; 52 53 // We keep a separate reference to this and create it ahead of time because we want to be able to 54 // setup a shell along with its platform view before the view has to appear. 55 fml::scoped_nsobject<FlutterView> _flutterView; 56 fml::scoped_nsobject<UIView> _splashScreenView; 57 fml::ScopedBlock<void (^)(void)> _flutterViewRenderedCallback; 58 UIInterfaceOrientationMask _orientationPreferences; 59 UIStatusBarStyle _statusBarStyle; 60 flutter::ViewportMetrics _viewportMetrics; 61 BOOL _initialized; 62 BOOL _viewOpaque; 63 BOOL _engineNeedsLaunch; 64 NSMutableSet<NSNumber*>* _ongoingTouches; 65} 66 67@synthesize displayingFlutterUI = _displayingFlutterUI; 68 69#pragma mark - Manage and override all designated initializers 70 71- (instancetype)initWithEngine:(FlutterEngine*)engine 72 nibName:(NSString*)nibNameOrNil 73 bundle:(NSBundle*)nibBundleOrNil { 74 NSAssert(engine != nil, @"Engine is required"); 75 self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 76 if (self) { 77 _viewOpaque = YES; 78 _engine.reset([engine retain]); 79 _engineNeedsLaunch = NO; 80 _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); 81 _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self); 82 _ongoingTouches = [[NSMutableSet alloc] init]; 83 84 [self performCommonViewControllerInitialization]; 85 [engine setViewController:self]; 86 } 87 88 return self; 89} 90 91- (instancetype)initWithProject:(FlutterDartProject*)projectOrNil 92 nibName:(NSString*)nibNameOrNil 93 bundle:(NSBundle*)nibBundleOrNil { 94 self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 95 if (self) { 96 _viewOpaque = YES; 97 _weakFactory = std::make_unique<fml::WeakPtrFactory<FlutterViewController>>(self); 98 _engine.reset([[FlutterEngine alloc] initWithName:@"io.flutter" 99 project:projectOrNil 100 allowHeadlessExecution:NO]); 101 _flutterView.reset([[FlutterView alloc] initWithDelegate:_engine opaque:self.isViewOpaque]); 102 [_engine.get() createShell:nil libraryURI:nil]; 103 _engineNeedsLaunch = YES; 104 _ongoingTouches = [[NSMutableSet alloc] init]; 105 [self loadDefaultSplashScreenView]; 106 [self performCommonViewControllerInitialization]; 107 } 108 109 return self; 110} 111 112- (instancetype)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil { 113 return [self initWithProject:nil nibName:nil bundle:nil]; 114} 115 116- (instancetype)initWithCoder:(NSCoder*)aDecoder { 117 return [self initWithProject:nil nibName:nil bundle:nil]; 118} 119 120- (instancetype)init { 121 return [self initWithProject:nil nibName:nil bundle:nil]; 122} 123 124- (BOOL)isViewOpaque { 125 return _viewOpaque; 126} 127 128- (void)setViewOpaque:(BOOL)value { 129 _viewOpaque = value; 130 if (_flutterView.get().layer.opaque != value) { 131 _flutterView.get().layer.opaque = value; 132 [_flutterView.get().layer setNeedsLayout]; 133 } 134} 135 136#pragma mark - Common view controller initialization tasks 137 138- (void)performCommonViewControllerInitialization { 139 if (_initialized) 140 return; 141 142 _initialized = YES; 143 144 _orientationPreferences = UIInterfaceOrientationMaskAll; 145 _statusBarStyle = UIStatusBarStyleDefault; 146 147 [self setupNotificationCenterObservers]; 148} 149 150- (FlutterEngine*)engine { 151 return _engine.get(); 152} 153 154- (fml::WeakPtr<FlutterViewController>)getWeakPtr { 155 return _weakFactory->GetWeakPtr(); 156} 157 158- (void)setupNotificationCenterObservers { 159 NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; 160 [center addObserver:self 161 selector:@selector(onOrientationPreferencesUpdated:) 162 name:@(flutter::kOrientationUpdateNotificationName) 163 object:nil]; 164 165 [center addObserver:self 166 selector:@selector(onPreferredStatusBarStyleUpdated:) 167 name:@(flutter::kOverlayStyleUpdateNotificationName) 168 object:nil]; 169 170 [center addObserver:self 171 selector:@selector(applicationBecameActive:) 172 name:UIApplicationDidBecomeActiveNotification 173 object:nil]; 174 175 [center addObserver:self 176 selector:@selector(applicationWillResignActive:) 177 name:UIApplicationWillResignActiveNotification 178 object:nil]; 179 180 [center addObserver:self 181 selector:@selector(applicationDidEnterBackground:) 182 name:UIApplicationDidEnterBackgroundNotification 183 object:nil]; 184 185 [center addObserver:self 186 selector:@selector(applicationWillEnterForeground:) 187 name:UIApplicationWillEnterForegroundNotification 188 object:nil]; 189 190 [center addObserver:self 191 selector:@selector(keyboardWillChangeFrame:) 192 name:UIKeyboardWillChangeFrameNotification 193 object:nil]; 194 195 [center addObserver:self 196 selector:@selector(keyboardWillBeHidden:) 197 name:UIKeyboardWillHideNotification 198 object:nil]; 199 200 [center addObserver:self 201 selector:@selector(onLocaleUpdated:) 202 name:NSCurrentLocaleDidChangeNotification 203 object:nil]; 204 205 [center addObserver:self 206 selector:@selector(onAccessibilityStatusChanged:) 207 name:UIAccessibilityVoiceOverStatusChanged 208 object:nil]; 209 210 [center addObserver:self 211 selector:@selector(onAccessibilityStatusChanged:) 212 name:UIAccessibilitySwitchControlStatusDidChangeNotification 213 object:nil]; 214 215 [center addObserver:self 216 selector:@selector(onAccessibilityStatusChanged:) 217 name:UIAccessibilitySpeakScreenStatusDidChangeNotification 218 object:nil]; 219 220 [center addObserver:self 221 selector:@selector(onAccessibilityStatusChanged:) 222 name:UIAccessibilityInvertColorsStatusDidChangeNotification 223 object:nil]; 224 225 [center addObserver:self 226 selector:@selector(onAccessibilityStatusChanged:) 227 name:UIAccessibilityReduceMotionStatusDidChangeNotification 228 object:nil]; 229 230 [center addObserver:self 231 selector:@selector(onAccessibilityStatusChanged:) 232 name:UIAccessibilityBoldTextStatusDidChangeNotification 233 object:nil]; 234 235 [center addObserver:self 236 selector:@selector(onUserSettingsChanged:) 237 name:UIContentSizeCategoryDidChangeNotification 238 object:nil]; 239} 240 241- (void)setInitialRoute:(NSString*)route { 242 [[_engine.get() navigationChannel] invokeMethod:@"setInitialRoute" arguments:route]; 243} 244 245- (void)popRoute { 246 [[_engine.get() navigationChannel] invokeMethod:@"popRoute" arguments:nil]; 247} 248 249- (void)pushRoute:(NSString*)route { 250 [[_engine.get() navigationChannel] invokeMethod:@"pushRoute" arguments:route]; 251} 252 253#pragma mark - Loading the view 254 255- (void)loadView { 256 self.view = _flutterView.get(); 257 self.view.multipleTouchEnabled = YES; 258 self.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 259 260 [self installSplashScreenViewIfNecessary]; 261} 262 263#pragma mark - Managing launch views 264 265- (void)installSplashScreenViewIfNecessary { 266 // Show the launch screen view again on top of the FlutterView if available. 267 // This launch screen view will be removed once the first Flutter frame is rendered. 268 if (_splashScreenView && (self.isBeingPresented || self.isMovingToParentViewController)) { 269 [_splashScreenView.get() removeFromSuperview]; 270 _splashScreenView.reset(); 271 return; 272 } 273 274 // Use the property getter to initialize the default value. 275 UIView* splashScreenView = self.splashScreenView; 276 if (splashScreenView == nil) { 277 return; 278 } 279 splashScreenView.frame = self.view.bounds; 280 [self.view addSubview:splashScreenView]; 281} 282 283+ (BOOL)automaticallyNotifiesObserversOfDisplayingFlutterUI { 284 return NO; 285} 286 287- (void)setDisplayingFlutterUI:(BOOL)displayingFlutterUI { 288 if (_displayingFlutterUI != displayingFlutterUI) { 289 if (displayingFlutterUI == YES) { 290 if (!self.isViewLoaded || !self.view.window) { 291 return; 292 } 293 } 294 [self willChangeValueForKey:@"displayingFlutterUI"]; 295 _displayingFlutterUI = displayingFlutterUI; 296 [self didChangeValueForKey:@"displayingFlutterUI"]; 297 } 298} 299 300- (void)callViewRenderedCallback { 301 self.displayingFlutterUI = YES; 302 if (_flutterViewRenderedCallback != nil) { 303 _flutterViewRenderedCallback.get()(); 304 _flutterViewRenderedCallback.reset(); 305 } 306} 307 308- (void)removeSplashScreenView:(dispatch_block_t _Nullable)onComplete { 309 NSAssert(_splashScreenView, @"The splash screen view must not be null"); 310 UIView* splashScreen = _splashScreenView.get(); 311 _splashScreenView.reset(); 312 [UIView animateWithDuration:0.2 313 animations:^{ 314 splashScreen.alpha = 0; 315 } 316 completion:^(BOOL finished) { 317 [splashScreen removeFromSuperview]; 318 if (onComplete) { 319 onComplete(); 320 } 321 }]; 322} 323 324- (void)installFirstFrameCallback { 325 fml::WeakPtr<flutter::PlatformViewIOS> weakPlatformView = [_engine.get() platformView]; 326 if (!weakPlatformView) { 327 return; 328 } 329 330 // Start on the platform thread. 331 weakPlatformView->SetNextFrameCallback([weakSelf = [self getWeakPtr], 332 platformTaskRunner = [_engine.get() platformTaskRunner], 333 gpuTaskRunner = [_engine.get() GPUTaskRunner]]() { 334 FML_DCHECK(gpuTaskRunner->RunsTasksOnCurrentThread()); 335 // Get callback on GPU thread and jump back to platform thread. 336 platformTaskRunner->PostTask([weakSelf]() { 337 fml::scoped_nsobject<FlutterViewController> flutterViewController( 338 [(FlutterViewController*)weakSelf.get() retain]); 339 if (flutterViewController) { 340 if (flutterViewController.get()->_splashScreenView) { 341 [flutterViewController removeSplashScreenView:^{ 342 [flutterViewController callViewRenderedCallback]; 343 }]; 344 } else { 345 [flutterViewController callViewRenderedCallback]; 346 } 347 } 348 }); 349 }); 350} 351 352#pragma mark - Properties 353 354- (FlutterView*)flutterView { 355 return _flutterView; 356} 357 358- (UIView*)splashScreenView { 359 if (!_splashScreenView) { 360 return nil; 361 } 362 return _splashScreenView.get(); 363} 364 365- (BOOL)loadDefaultSplashScreenView { 366 NSString* launchscreenName = 367 [[[NSBundle mainBundle] infoDictionary] objectForKey:@"UILaunchStoryboardName"]; 368 if (launchscreenName == nil) { 369 return NO; 370 } 371 UIView* splashView = [self splashScreenFromStoryboard:launchscreenName]; 372 if (!splashView) { 373 splashView = [self splashScreenFromXib:launchscreenName]; 374 } 375 if (!splashView) { 376 return NO; 377 } 378 self.splashScreenView = splashView; 379 return YES; 380} 381 382- (UIView*)splashScreenFromStoryboard:(NSString*)name { 383 UIStoryboard* storyboard = nil; 384 @try { 385 storyboard = [UIStoryboard storyboardWithName:name bundle:nil]; 386 } @catch (NSException* exception) { 387 return nil; 388 } 389 if (storyboard) { 390 UIViewController* splashScreenViewController = [storyboard instantiateInitialViewController]; 391 return splashScreenViewController.view; 392 } 393 return nil; 394} 395 396- (UIView*)splashScreenFromXib:(NSString*)name { 397 NSArray* objects = [[NSBundle mainBundle] loadNibNamed:name owner:self options:nil]; 398 if ([objects count] != 0) { 399 UIView* view = [objects objectAtIndex:0]; 400 return view; 401 } 402 return nil; 403} 404 405- (void)setSplashScreenView:(UIView*)view { 406 if (!view) { 407 // Special case: user wants to remove the splash screen view. 408 if (_splashScreenView) { 409 [self removeSplashScreenView:nil]; 410 } 411 return; 412 } 413 414 _splashScreenView.reset([view retain]); 415 _splashScreenView.get().autoresizingMask = 416 UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; 417} 418 419- (void)setFlutterViewDidRenderCallback:(void (^)(void))callback { 420 _flutterViewRenderedCallback.reset(callback, fml::OwnershipPolicy::Retain); 421} 422 423#pragma mark - Surface creation and teardown updates 424 425- (void)surfaceUpdated:(BOOL)appeared { 426 // NotifyCreated/NotifyDestroyed are synchronous and require hops between the UI and GPU thread. 427 if (appeared) { 428 [self installFirstFrameCallback]; 429 [_engine.get() platformViewsController] -> SetFlutterView(_flutterView.get()); 430 [_engine.get() platformViewsController] -> SetFlutterViewController(self); 431 [_engine.get() platformView] -> NotifyCreated(); 432 } else { 433 self.displayingFlutterUI = NO; 434 [_engine.get() platformView] -> NotifyDestroyed(); 435 [_engine.get() platformViewsController] -> SetFlutterView(nullptr); 436 [_engine.get() platformViewsController] -> SetFlutterViewController(nullptr); 437 } 438} 439 440#pragma mark - UIViewController lifecycle notifications 441 442- (void)viewWillAppear:(BOOL)animated { 443 TRACE_EVENT0("flutter", "viewWillAppear"); 444 445 if (_engineNeedsLaunch) { 446 [_engine.get() launchEngine:nil libraryURI:nil]; 447 [_engine.get() setViewController:self]; 448 _engineNeedsLaunch = NO; 449 } 450 451 // Send platform settings to Flutter, e.g., platform brightness. 452 [self onUserSettingsChanged:nil]; 453 454 // Only recreate surface on subsequent appearances when viewport metrics are known. 455 // First time surface creation is done on viewDidLayoutSubviews. 456 if (_viewportMetrics.physical_width) { 457 [self surfaceUpdated:YES]; 458 } 459 [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"]; 460 461 [super viewWillAppear:animated]; 462} 463 464- (void)viewDidAppear:(BOOL)animated { 465 TRACE_EVENT0("flutter", "viewDidAppear"); 466 [self onLocaleUpdated:nil]; 467 [self onUserSettingsChanged:nil]; 468 [self onAccessibilityStatusChanged:nil]; 469 [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"]; 470 471 [super viewDidAppear:animated]; 472} 473 474- (void)viewWillDisappear:(BOOL)animated { 475 TRACE_EVENT0("flutter", "viewWillDisappear"); 476 [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"]; 477 478 [super viewWillDisappear:animated]; 479} 480 481- (void)viewDidDisappear:(BOOL)animated { 482 TRACE_EVENT0("flutter", "viewDidDisappear"); 483 [self surfaceUpdated:NO]; 484 [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"]; 485 [self flushOngoingTouches]; 486 487 [super viewDidDisappear:animated]; 488} 489 490- (void)flushOngoingTouches { 491 if (_ongoingTouches.count > 0) { 492 auto packet = std::make_unique<flutter::PointerDataPacket>(_ongoingTouches.count); 493 size_t pointer_index = 0; 494 // If the view controller is going away, we want to flush cancel all the ongoing 495 // touches to the framework so nothing gets orphaned. 496 for (NSNumber* device in _ongoingTouches) { 497 // Create fake PointerData to balance out each previously started one for the framework. 498 flutter::PointerData pointer_data; 499 pointer_data.Clear(); 500 501 constexpr int kMicrosecondsPerSecond = 1000 * 1000; 502 // Use current time. 503 pointer_data.time_stamp = [[NSDate date] timeIntervalSince1970] * kMicrosecondsPerSecond; 504 505 pointer_data.change = flutter::PointerData::Change::kCancel; 506 pointer_data.kind = flutter::PointerData::DeviceKind::kTouch; 507 pointer_data.device = device.longLongValue; 508 509 // Anything we put here will be arbitrary since there are no touches. 510 pointer_data.physical_x = 0; 511 pointer_data.physical_y = 0; 512 pointer_data.pressure = 1.0; 513 pointer_data.pressure_max = 1.0; 514 515 packet->SetPointerData(pointer_index++, pointer_data); 516 } 517 518 [_ongoingTouches removeAllObjects]; 519 [_engine.get() dispatchPointerDataPacket:std::move(packet)]; 520 } 521} 522 523- (void)dealloc { 524 [_engine.get() notifyViewControllerDeallocated]; 525 [[NSNotificationCenter defaultCenter] removeObserver:self]; 526 [super dealloc]; 527} 528 529#pragma mark - Application lifecycle notifications 530 531- (void)applicationBecameActive:(NSNotification*)notification { 532 TRACE_EVENT0("flutter", "applicationBecameActive"); 533 if (_viewportMetrics.physical_width) 534 [self surfaceUpdated:YES]; 535 [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.resumed"]; 536} 537 538- (void)applicationWillResignActive:(NSNotification*)notification { 539 TRACE_EVENT0("flutter", "applicationWillResignActive"); 540 [self surfaceUpdated:NO]; 541 [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"]; 542} 543 544- (void)applicationDidEnterBackground:(NSNotification*)notification { 545 TRACE_EVENT0("flutter", "applicationDidEnterBackground"); 546 [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.paused"]; 547} 548 549- (void)applicationWillEnterForeground:(NSNotification*)notification { 550 TRACE_EVENT0("flutter", "applicationWillEnterForeground"); 551 [[_engine.get() lifecycleChannel] sendMessage:@"AppLifecycleState.inactive"]; 552} 553 554#pragma mark - Touch event handling 555 556static flutter::PointerData::Change PointerDataChangeFromUITouchPhase(UITouchPhase phase) { 557 switch (phase) { 558 case UITouchPhaseBegan: 559 return flutter::PointerData::Change::kDown; 560 case UITouchPhaseMoved: 561 case UITouchPhaseStationary: 562 // There is no EVENT_TYPE_POINTER_STATIONARY. So we just pass a move type 563 // with the same coordinates 564 return flutter::PointerData::Change::kMove; 565 case UITouchPhaseEnded: 566 return flutter::PointerData::Change::kUp; 567 case UITouchPhaseCancelled: 568 return flutter::PointerData::Change::kCancel; 569 } 570 571 return flutter::PointerData::Change::kCancel; 572} 573 574static flutter::PointerData::DeviceKind DeviceKindFromTouchType(UITouch* touch) { 575 if (@available(iOS 9, *)) { 576 switch (touch.type) { 577 case UITouchTypeDirect: 578 case UITouchTypeIndirect: 579 return flutter::PointerData::DeviceKind::kTouch; 580 case UITouchTypeStylus: 581 return flutter::PointerData::DeviceKind::kStylus; 582 } 583 } else { 584 return flutter::PointerData::DeviceKind::kTouch; 585 } 586 587 return flutter::PointerData::DeviceKind::kTouch; 588} 589 590// Dispatches the UITouches to the engine. Usually, the type of change of the touch is determined 591// from the UITouch's phase. However, FlutterAppDelegate fakes touches to ensure that touch events 592// in the status bar area are available to framework code. The change type (optional) of the faked 593// touch is specified in the second argument. 594- (void)dispatchTouches:(NSSet*)touches 595 pointerDataChangeOverride:(flutter::PointerData::Change*)overridden_change { 596 const CGFloat scale = [UIScreen mainScreen].scale; 597 auto packet = std::make_unique<flutter::PointerDataPacket>(touches.count); 598 599 size_t pointer_index = 0; 600 601 for (UITouch* touch in touches) { 602 CGPoint windowCoordinates = [touch locationInView:self.view]; 603 604 flutter::PointerData pointer_data; 605 pointer_data.Clear(); 606 607 constexpr int kMicrosecondsPerSecond = 1000 * 1000; 608 pointer_data.time_stamp = touch.timestamp * kMicrosecondsPerSecond; 609 610 pointer_data.change = overridden_change != nullptr 611 ? *overridden_change 612 : PointerDataChangeFromUITouchPhase(touch.phase); 613 614 pointer_data.kind = DeviceKindFromTouchType(touch); 615 616 pointer_data.device = reinterpret_cast<int64_t>(touch); 617 618 pointer_data.physical_x = windowCoordinates.x * scale; 619 pointer_data.physical_y = windowCoordinates.y * scale; 620 621 NSNumber* deviceKey = [NSNumber numberWithLongLong:pointer_data.device]; 622 // Track touches that began and not yet stopped so we can flush them 623 // if the view controller goes away. 624 switch (pointer_data.change) { 625 case flutter::PointerData::Change::kDown: 626 [_ongoingTouches addObject:deviceKey]; 627 break; 628 case flutter::PointerData::Change::kCancel: 629 case flutter::PointerData::Change::kUp: 630 [_ongoingTouches removeObject:deviceKey]; 631 break; 632 case flutter::PointerData::Change::kHover: 633 case flutter::PointerData::Change::kMove: 634 // We're only tracking starts and stops. 635 break; 636 case flutter::PointerData::Change::kAdd: 637 case flutter::PointerData::Change::kRemove: 638 // We don't use kAdd/kRemove. 639 break; 640 } 641 642 // pressure_min is always 0.0 643 if (@available(iOS 9, *)) { 644 // These properties were introduced in iOS 9.0. 645 pointer_data.pressure = touch.force; 646 pointer_data.pressure_max = touch.maximumPossibleForce; 647 } else { 648 pointer_data.pressure = 1.0; 649 pointer_data.pressure_max = 1.0; 650 } 651 652 // These properties were introduced in iOS 8.0 653 pointer_data.radius_major = touch.majorRadius; 654 pointer_data.radius_min = touch.majorRadius - touch.majorRadiusTolerance; 655 pointer_data.radius_max = touch.majorRadius + touch.majorRadiusTolerance; 656 657 // These properties were introduced in iOS 9.1 658 if (@available(iOS 9.1, *)) { 659 // iOS Documentation: altitudeAngle 660 // A value of 0 radians indicates that the stylus is parallel to the surface. The value of 661 // this property is Pi/2 when the stylus is perpendicular to the surface. 662 // 663 // PointerData Documentation: tilt 664 // The angle of the stylus, in radians in the range: 665 // 0 <= tilt <= pi/2 666 // giving the angle of the axis of the stylus, relative to the axis perpendicular to the input 667 // surface (thus 0.0 indicates the stylus is orthogonal to the plane of the input surface, 668 // while pi/2 indicates that the stylus is flat on that surface). 669 // 670 // Discussion: 671 // The ranges are the same. Origins are swapped. 672 pointer_data.tilt = M_PI_2 - touch.altitudeAngle; 673 674 // iOS Documentation: azimuthAngleInView: 675 // With the tip of the stylus touching the screen, the value of this property is 0 radians 676 // when the cap end of the stylus (that is, the end opposite of the tip) points along the 677 // positive x axis of the device's screen. The azimuth angle increases as the user swings the 678 // cap end of the stylus in a clockwise direction around the tip. 679 // 680 // PointerData Documentation: orientation 681 // The angle of the stylus, in radians in the range: 682 // -pi < orientation <= pi 683 // giving the angle of the axis of the stylus projected onto the input surface, relative to 684 // the positive y-axis of that surface (thus 0.0 indicates the stylus, if projected onto that 685 // surface, would go from the contact point vertically up in the positive y-axis direction, pi 686 // would indicate that the stylus would go down in the negative y-axis direction; pi/4 would 687 // indicate that the stylus goes up and to the right, -pi/2 would indicate that the stylus 688 // goes to the left, etc). 689 // 690 // Discussion: 691 // Sweep direction is the same. Phase of M_PI_2. 692 pointer_data.orientation = [touch azimuthAngleInView:nil] - M_PI_2; 693 } 694 695 packet->SetPointerData(pointer_index++, pointer_data); 696 } 697 698 [_engine.get() dispatchPointerDataPacket:std::move(packet)]; 699} 700 701- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { 702 [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; 703} 704 705- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { 706 [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; 707} 708 709- (void)touchesEnded:(NSSet*)touches withEvent:(UIEvent*)event { 710 [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; 711} 712 713- (void)touchesCancelled:(NSSet*)touches withEvent:(UIEvent*)event { 714 [self dispatchTouches:touches pointerDataChangeOverride:nullptr]; 715} 716 717#pragma mark - Handle view resizing 718 719- (void)updateViewportMetrics { 720 [_engine.get() updateViewportMetrics:_viewportMetrics]; 721} 722 723- (CGFloat)statusBarPadding { 724 UIScreen* screen = self.view.window.screen; 725 CGRect statusFrame = [UIApplication sharedApplication].statusBarFrame; 726 CGRect viewFrame = [self.view convertRect:self.view.bounds 727 toCoordinateSpace:screen.coordinateSpace]; 728 CGRect intersection = CGRectIntersection(statusFrame, viewFrame); 729 return CGRectIsNull(intersection) ? 0.0 : intersection.size.height; 730} 731 732- (void)viewDidLayoutSubviews { 733 CGSize viewSize = self.view.bounds.size; 734 CGFloat scale = [UIScreen mainScreen].scale; 735 736 // First time since creation that the dimensions of its view is known. 737 bool firstViewBoundsUpdate = !_viewportMetrics.physical_width; 738 _viewportMetrics.device_pixel_ratio = scale; 739 _viewportMetrics.physical_width = viewSize.width * scale; 740 _viewportMetrics.physical_height = viewSize.height * scale; 741 742 [self updateViewportPadding]; 743 [self updateViewportMetrics]; 744 745 // This must run after updateViewportMetrics so that the surface creation tasks are queued after 746 // the viewport metrics update tasks. 747 if (firstViewBoundsUpdate) { 748 [self surfaceUpdated:YES]; 749 750 flutter::Shell& shell = [_engine.get() shell]; 751 fml::TimeDelta waitTime = 752#if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG 753 fml::TimeDelta::FromMilliseconds(200); 754#else 755 fml::TimeDelta::FromMilliseconds(100); 756#endif 757 if (shell.WaitForFirstFrame(waitTime).code() == fml::StatusCode::kDeadlineExceeded) { 758 FML_LOG(INFO) << "Timeout waiting for the first frame to render. This may happen in " 759 << "unoptimized builds. If this is a release build, you should load a less " 760 << "complex frame to avoid the timeout."; 761 } 762 } 763} 764 765- (void)viewSafeAreaInsetsDidChange { 766 [self updateViewportPadding]; 767 [self updateViewportMetrics]; 768 [super viewSafeAreaInsetsDidChange]; 769} 770 771// Updates _viewportMetrics physical padding. 772// 773// Viewport padding represents the iOS safe area insets. 774- (void)updateViewportPadding { 775 CGFloat scale = [UIScreen mainScreen].scale; 776 if (@available(iOS 11, *)) { 777 _viewportMetrics.physical_padding_top = self.view.safeAreaInsets.top * scale; 778 _viewportMetrics.physical_padding_left = self.view.safeAreaInsets.left * scale; 779 _viewportMetrics.physical_padding_right = self.view.safeAreaInsets.right * scale; 780 _viewportMetrics.physical_padding_bottom = self.view.safeAreaInsets.bottom * scale; 781 } else { 782 _viewportMetrics.physical_padding_top = [self statusBarPadding] * scale; 783 } 784} 785 786#pragma mark - Keyboard events 787 788- (void)keyboardWillChangeFrame:(NSNotification*)notification { 789 NSDictionary* info = [notification userInfo]; 790 CGFloat bottom = CGRectGetHeight([[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]); 791 CGFloat scale = [UIScreen mainScreen].scale; 792 793 // The keyboard is treated as an inset since we want to effectively reduce the window size by the 794 // keyboard height. The Dart side will compute a value accounting for the keyboard-consuming 795 // bottom padding. 796 _viewportMetrics.physical_view_inset_bottom = bottom * scale; 797 [self updateViewportMetrics]; 798} 799 800- (void)keyboardWillBeHidden:(NSNotification*)notification { 801 _viewportMetrics.physical_view_inset_bottom = 0; 802 [self updateViewportMetrics]; 803} 804 805#pragma mark - Orientation updates 806 807- (void)onOrientationPreferencesUpdated:(NSNotification*)notification { 808 // Notifications may not be on the iOS UI thread 809 dispatch_async(dispatch_get_main_queue(), ^{ 810 NSDictionary* info = notification.userInfo; 811 812 NSNumber* update = info[@(flutter::kOrientationUpdateNotificationKey)]; 813 814 if (update == nil) { 815 return; 816 } 817 818 NSUInteger new_preferences = update.unsignedIntegerValue; 819 820 if (new_preferences != _orientationPreferences) { 821 _orientationPreferences = new_preferences; 822 [UIViewController attemptRotationToDeviceOrientation]; 823 } 824 }); 825} 826 827- (BOOL)shouldAutorotate { 828 return YES; 829} 830 831- (NSUInteger)supportedInterfaceOrientations { 832 return _orientationPreferences; 833} 834 835#pragma mark - Accessibility 836 837- (void)onAccessibilityStatusChanged:(NSNotification*)notification { 838 auto platformView = [_engine.get() platformView]; 839 int32_t flags = 0; 840 if (UIAccessibilityIsInvertColorsEnabled()) 841 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kInvertColors); 842 if (UIAccessibilityIsReduceMotionEnabled()) 843 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kReduceMotion); 844 if (UIAccessibilityIsBoldTextEnabled()) 845 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kBoldText); 846#if TARGET_OS_SIMULATOR 847 // There doesn't appear to be any way to determine whether the accessibility 848 // inspector is enabled on the simulator. We conservatively always turn on the 849 // accessibility bridge in the simulator, but never assistive technology. 850 platformView->SetSemanticsEnabled(true); 851 platformView->SetAccessibilityFeatures(flags); 852#else 853 bool enabled = UIAccessibilityIsVoiceOverRunning() || UIAccessibilityIsSwitchControlRunning(); 854 if (enabled) 855 flags |= static_cast<int32_t>(flutter::AccessibilityFeatureFlag::kAccessibleNavigation); 856 platformView->SetSemanticsEnabled(enabled || UIAccessibilityIsSpeakScreenEnabled()); 857 platformView->SetAccessibilityFeatures(flags); 858#endif 859} 860 861#pragma mark - Locale updates 862 863- (void)onLocaleUpdated:(NSNotification*)notification { 864 NSArray<NSString*>* preferredLocales = [NSLocale preferredLanguages]; 865 NSMutableArray<NSString*>* data = [[NSMutableArray new] autorelease]; 866 867 // Force prepend the [NSLocale currentLocale] to the front of the list 868 // to ensure we are including the full default locale. preferredLocales 869 // is not guaranteed to include anything beyond the languageCode. 870 NSLocale* currentLocale = [NSLocale currentLocale]; 871 NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; 872 NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; 873 NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode]; 874 NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode]; 875 if (languageCode) { 876 [data addObject:languageCode]; 877 [data addObject:(countryCode ? countryCode : @"")]; 878 [data addObject:(scriptCode ? scriptCode : @"")]; 879 [data addObject:(variantCode ? variantCode : @"")]; 880 } 881 882 // Add any secondary locales/languages to the list. 883 for (NSString* localeID in preferredLocales) { 884 NSLocale* currentLocale = [[[NSLocale alloc] initWithLocaleIdentifier:localeID] autorelease]; 885 NSString* languageCode = [currentLocale objectForKey:NSLocaleLanguageCode]; 886 NSString* countryCode = [currentLocale objectForKey:NSLocaleCountryCode]; 887 NSString* scriptCode = [currentLocale objectForKey:NSLocaleScriptCode]; 888 NSString* variantCode = [currentLocale objectForKey:NSLocaleVariantCode]; 889 if (!languageCode) { 890 continue; 891 } 892 [data addObject:languageCode]; 893 [data addObject:(countryCode ? countryCode : @"")]; 894 [data addObject:(scriptCode ? scriptCode : @"")]; 895 [data addObject:(variantCode ? variantCode : @"")]; 896 } 897 if (data.count == 0) { 898 return; 899 } 900 [[_engine.get() localizationChannel] invokeMethod:@"setLocale" arguments:data]; 901} 902 903#pragma mark - Set user settings 904 905- (void)traitCollectionDidChange:(UITraitCollection*)previousTraitCollection { 906 [super traitCollectionDidChange:previousTraitCollection]; 907 [self onUserSettingsChanged:nil]; 908} 909 910- (void)onUserSettingsChanged:(NSNotification*)notification { 911 [[_engine.get() settingsChannel] sendMessage:@{ 912 @"textScaleFactor" : @([self textScaleFactor]), 913 @"alwaysUse24HourFormat" : @([self isAlwaysUse24HourFormat]), 914 @"platformBrightness" : [self brightnessMode], 915 @"platformContrast" : [self contrastMode] 916 }]; 917} 918 919- (CGFloat)textScaleFactor { 920 UIContentSizeCategory category = [UIApplication sharedApplication].preferredContentSizeCategory; 921 // The delta is computed by approximating Apple's typography guidelines: 922 // https://developer.apple.com/ios/human-interface-guidelines/visual-design/typography/ 923 // 924 // Specifically: 925 // Non-accessibility sizes for "body" text are: 926 const CGFloat xs = 14; 927 const CGFloat s = 15; 928 const CGFloat m = 16; 929 const CGFloat l = 17; 930 const CGFloat xl = 19; 931 const CGFloat xxl = 21; 932 const CGFloat xxxl = 23; 933 934 // Accessibility sizes for "body" text are: 935 const CGFloat ax1 = 28; 936 const CGFloat ax2 = 33; 937 const CGFloat ax3 = 40; 938 const CGFloat ax4 = 47; 939 const CGFloat ax5 = 53; 940 941 // We compute the scale as relative difference from size L (large, the default size), where 942 // L is assumed to have scale 1.0. 943 if ([category isEqualToString:UIContentSizeCategoryExtraSmall]) 944 return xs / l; 945 else if ([category isEqualToString:UIContentSizeCategorySmall]) 946 return s / l; 947 else if ([category isEqualToString:UIContentSizeCategoryMedium]) 948 return m / l; 949 else if ([category isEqualToString:UIContentSizeCategoryLarge]) 950 return 1.0; 951 else if ([category isEqualToString:UIContentSizeCategoryExtraLarge]) 952 return xl / l; 953 else if ([category isEqualToString:UIContentSizeCategoryExtraExtraLarge]) 954 return xxl / l; 955 else if ([category isEqualToString:UIContentSizeCategoryExtraExtraExtraLarge]) 956 return xxxl / l; 957 else if ([category isEqualToString:UIContentSizeCategoryAccessibilityMedium]) 958 return ax1 / l; 959 else if ([category isEqualToString:UIContentSizeCategoryAccessibilityLarge]) 960 return ax2 / l; 961 else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraLarge]) 962 return ax3 / l; 963 else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraLarge]) 964 return ax4 / l; 965 else if ([category isEqualToString:UIContentSizeCategoryAccessibilityExtraExtraExtraLarge]) 966 return ax5 / l; 967 else 968 return 1.0; 969} 970 971- (BOOL)isAlwaysUse24HourFormat { 972 // iOS does not report its "24-Hour Time" user setting in the API. Instead, it applies 973 // it automatically to NSDateFormatter when used with [NSLocale currentLocale]. It is 974 // essential that [NSLocale currentLocale] is used. Any custom locale, even the one 975 // that's the same as [NSLocale currentLocale] will ignore the 24-hour option (there 976 // must be some internal field that's not exposed to developers). 977 // 978 // Therefore this option behaves differently across Android and iOS. On Android this 979 // setting is exposed standalone, and can therefore be applied to all locales, whether 980 // the "current system locale" or a custom one. On iOS it only applies to the current 981 // system locale. Widget implementors must take this into account in order to provide 982 // platform-idiomatic behavior in their widgets. 983 NSString* dateFormat = [NSDateFormatter dateFormatFromTemplate:@"j" 984 options:0 985 locale:[NSLocale currentLocale]]; 986 return [dateFormat rangeOfString:@"a"].location == NSNotFound; 987} 988 989// The brightness mode of the platform, e.g., light or dark, expressed as a string that 990// is understood by the Flutter framework. See the settings system channel for more 991// information. 992- (NSString*)brightnessMode { 993 if (@available(iOS 13, *)) { 994 UIUserInterfaceStyle style = self.traitCollection.userInterfaceStyle; 995 996 if (style == UIUserInterfaceStyleDark) { 997 return @"dark"; 998 } else { 999 return @"light"; 1000 } 1001 } else { 1002 return @"light"; 1003 } 1004} 1005 1006// The contrast mode of the platform, e.g., normal or high, expressed as a string that is 1007// understood by the Flutter framework. See the settings system channel for more 1008// information. 1009- (NSString*)contrastMode { 1010 if (@available(iOS 13, *)) { 1011 UIAccessibilityContrast contrast = self.traitCollection.accessibilityContrast; 1012 1013 if (contrast == UIAccessibilityContrastHigh) { 1014 return @"high"; 1015 } else { 1016 return @"normal"; 1017 } 1018 } else { 1019 return @"normal"; 1020 } 1021} 1022 1023#pragma mark - Status Bar touch event handling 1024 1025// Standard iOS status bar height in pixels. 1026constexpr CGFloat kStandardStatusBarHeight = 20.0; 1027 1028- (void)handleStatusBarTouches:(UIEvent*)event { 1029 CGFloat standardStatusBarHeight = kStandardStatusBarHeight; 1030 if (@available(iOS 11, *)) { 1031 standardStatusBarHeight = self.view.safeAreaInsets.top; 1032 } 1033 1034 // If the status bar is double-height, don't handle status bar taps. iOS 1035 // should open the app associated with the status bar. 1036 CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame; 1037 if (statusBarFrame.size.height != standardStatusBarHeight) { 1038 return; 1039 } 1040 1041 // If we detect a touch in the status bar, synthesize a fake touch begin/end. 1042 for (UITouch* touch in event.allTouches) { 1043 if (touch.phase == UITouchPhaseBegan && touch.tapCount > 0) { 1044 CGPoint windowLoc = [touch locationInView:nil]; 1045 CGPoint screenLoc = [touch.window convertPoint:windowLoc toWindow:nil]; 1046 if (CGRectContainsPoint(statusBarFrame, screenLoc)) { 1047 NSSet* statusbarTouches = [NSSet setWithObject:touch]; 1048 1049 flutter::PointerData::Change change = flutter::PointerData::Change::kDown; 1050 [self dispatchTouches:statusbarTouches pointerDataChangeOverride:&change]; 1051 change = flutter::PointerData::Change::kUp; 1052 [self dispatchTouches:statusbarTouches pointerDataChangeOverride:&change]; 1053 return; 1054 } 1055 } 1056 } 1057} 1058 1059#pragma mark - Status bar style 1060 1061- (UIStatusBarStyle)preferredStatusBarStyle { 1062 return _statusBarStyle; 1063} 1064 1065- (void)onPreferredStatusBarStyleUpdated:(NSNotification*)notification { 1066 // Notifications may not be on the iOS UI thread 1067 dispatch_async(dispatch_get_main_queue(), ^{ 1068 NSDictionary* info = notification.userInfo; 1069 1070 NSNumber* update = info[@(flutter::kOverlayStyleUpdateNotificationKey)]; 1071 1072 if (update == nil) { 1073 return; 1074 } 1075 1076 NSInteger style = update.integerValue; 1077 1078 if (style != _statusBarStyle) { 1079 _statusBarStyle = static_cast<UIStatusBarStyle>(style); 1080 [self setNeedsStatusBarAppearanceUpdate]; 1081 } 1082 }); 1083} 1084 1085#pragma mark - Platform views 1086 1087- (flutter::FlutterPlatformViewsController*)platformViewsController { 1088 return [_engine.get() platformViewsController]; 1089} 1090 1091- (NSObject<FlutterBinaryMessenger>*)binaryMessenger { 1092 return _engine.get().binaryMessenger; 1093} 1094 1095#pragma mark - FlutterBinaryMessenger 1096 1097- (void)sendOnChannel:(NSString*)channel message:(NSData*)message { 1098 [_engine.get().binaryMessenger sendOnChannel:channel message:message]; 1099} 1100 1101- (void)sendOnChannel:(NSString*)channel 1102 message:(NSData*)message 1103 binaryReply:(FlutterBinaryReply)callback { 1104 NSAssert(channel, @"The channel must not be null"); 1105 [_engine.get().binaryMessenger sendOnChannel:channel message:message binaryReply:callback]; 1106} 1107 1108- (void)setMessageHandlerOnChannel:(NSString*)channel 1109 binaryMessageHandler:(FlutterBinaryMessageHandler)handler { 1110 NSAssert(channel, @"The channel must not be null"); 1111 [_engine.get().binaryMessenger setMessageHandlerOnChannel:channel binaryMessageHandler:handler]; 1112} 1113 1114#pragma mark - FlutterTextureRegistry 1115 1116- (int64_t)registerTexture:(NSObject<FlutterTexture>*)texture { 1117 return [_engine.get() registerTexture:texture]; 1118} 1119 1120- (void)unregisterTexture:(int64_t)textureId { 1121 [_engine.get() unregisterTexture:textureId]; 1122} 1123 1124- (void)textureFrameAvailable:(int64_t)textureId { 1125 [_engine.get() textureFrameAvailable:textureId]; 1126} 1127 1128- (NSString*)lookupKeyForAsset:(NSString*)asset { 1129 return [FlutterDartProject lookupKeyForAsset:asset]; 1130} 1131 1132- (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package { 1133 return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package]; 1134} 1135 1136- (id<FlutterPluginRegistry>)pluginRegistry { 1137 return _engine; 1138} 1139 1140#pragma mark - FlutterPluginRegistry 1141 1142- (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey { 1143 return [_engine.get() registrarForPlugin:pluginKey]; 1144} 1145 1146- (BOOL)hasPlugin:(NSString*)pluginKey { 1147 return [_engine.get() hasPlugin:pluginKey]; 1148} 1149 1150- (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey { 1151 return [_engine.get() valuePublishedByPlugin:pluginKey]; 1152} 1153 1154@end 1155