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