Flutter iOS Embedder
FlutterPlatformPlugin.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 
6 
7 #import <AudioToolbox/AudioToolbox.h>
8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIApplication.h>
10 #import <UIKit/UIKit.h>
11 
12 #include "flutter/fml/logging.h"
17 
18 namespace {
19 
20 constexpr char kTextPlainFormat[] = "text/plain";
21 const UInt32 kKeyPressClickSoundId = 1306;
22 
23 #if not APPLICATION_EXTENSION_API_ONLY
24 const NSString* searchURLPrefix = @"x-web-search://?";
25 #endif
26 
27 } // namespace
28 
29 namespace flutter {
30 
31 // TODO(abarth): Move these definitions from system_chrome_impl.cc to here.
33  "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
35  "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
37  "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
39  "io.flutter.plugin.platform.SystemChromeOverlayNotificationKey";
40 
41 } // namespace flutter
42 
43 using namespace flutter;
44 
45 static void SetStatusBarHiddenForSharedApplication(BOOL hidden) {
46 #if not APPLICATION_EXTENSION_API_ONLY
47  [UIApplication sharedApplication].statusBarHidden = hidden;
48 #else
49  FML_LOG(WARNING) << "Application based status bar styling is not available in app extension.";
50 #endif
51 }
52 
53 static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style) {
54 #if not APPLICATION_EXTENSION_API_ONLY
55  // Note: -[UIApplication setStatusBarStyle] is deprecated in iOS9
56  // in favor of delegating to the view controller.
57  [[UIApplication sharedApplication] setStatusBarStyle:style];
58 #else
59  FML_LOG(WARNING) << "Application based status bar styling is not available in app extension.";
60 #endif
61 }
62 
63 @interface FlutterPlatformPlugin ()
64 
65 /**
66  * @brief Whether the status bar appearance is based on the style preferred for this ViewController.
67  *
68  * The default value is YES.
69  * Explicitly add `UIViewControllerBasedStatusBarAppearance` as `false` in
70  * info.plist makes this value to be false.
71  */
72 @property(nonatomic, assign) BOOL enableViewControllerBasedStatusBarAppearance;
73 
74 @end
75 
76 @implementation FlutterPlatformPlugin {
77  fml::WeakNSObject<FlutterEngine> _engine;
78  // Used to detect whether this device has live text input ability or not.
79  UITextField* _textField;
80 }
81 
82 - (instancetype)initWithEngine:(fml::WeakNSObject<FlutterEngine>)engine {
83  FML_DCHECK(engine) << "engine must be set";
84  self = [super init];
85 
86  if (self) {
87  _engine = engine;
88  NSObject* infoValue = [[NSBundle mainBundle]
89  objectForInfoDictionaryKey:@"UIViewControllerBasedStatusBarAppearance"];
90 #if FLUTTER_RUNTIME_MODE == FLUTTER_RUNTIME_MODE_DEBUG
91  if (infoValue != nil && ![infoValue isKindOfClass:[NSNumber class]]) {
92  FML_LOG(ERROR) << "The value of UIViewControllerBasedStatusBarAppearance in info.plist must "
93  "be a Boolean type.";
94  }
95 #endif
96  _enableViewControllerBasedStatusBarAppearance =
97  (infoValue == nil || [(NSNumber*)infoValue boolValue]);
98  }
99 
100  return self;
101 }
102 
103 - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
104  NSString* method = call.method;
105  id args = call.arguments;
106  if ([method isEqualToString:@"SystemSound.play"]) {
107  [self playSystemSound:args];
108  result(nil);
109  } else if ([method isEqualToString:@"HapticFeedback.vibrate"]) {
110  [self vibrateHapticFeedback:args];
111  result(nil);
112  } else if ([method isEqualToString:@"SystemChrome.setPreferredOrientations"]) {
113  [self setSystemChromePreferredOrientations:args];
114  result(nil);
115  } else if ([method isEqualToString:@"SystemChrome.setApplicationSwitcherDescription"]) {
116  [self setSystemChromeApplicationSwitcherDescription:args];
117  result(nil);
118  } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIOverlays"]) {
119  [self setSystemChromeEnabledSystemUIOverlays:args];
120  result(nil);
121  } else if ([method isEqualToString:@"SystemChrome.setEnabledSystemUIMode"]) {
122  [self setSystemChromeEnabledSystemUIMode:args];
123  result(nil);
124  } else if ([method isEqualToString:@"SystemChrome.restoreSystemUIOverlays"]) {
125  [self restoreSystemChromeSystemUIOverlays];
126  result(nil);
127  } else if ([method isEqualToString:@"SystemChrome.setSystemUIOverlayStyle"]) {
128  [self setSystemChromeSystemUIOverlayStyle:args];
129  result(nil);
130  } else if ([method isEqualToString:@"SystemNavigator.pop"]) {
131  NSNumber* isAnimated = args;
132  [self popSystemNavigator:isAnimated.boolValue];
133  result(nil);
134  } else if ([method isEqualToString:@"Clipboard.getData"]) {
135  result([self getClipboardData:args]);
136  } else if ([method isEqualToString:@"Clipboard.setData"]) {
137  [self setClipboardData:args];
138  result(nil);
139  } else if ([method isEqualToString:@"Clipboard.hasStrings"]) {
140  result([self clipboardHasStrings]);
141  } else if ([method isEqualToString:@"LiveText.isLiveTextInputAvailable"]) {
142  result(@([self isLiveTextInputAvailable]));
143  } else if ([method isEqualToString:@"SearchWeb.invoke"]) {
144  [self searchWeb:args];
145  result(nil);
146  } else if ([method isEqualToString:@"LookUp.invoke"]) {
147  [self showLookUpViewController:args];
148  result(nil);
149  } else if ([method isEqualToString:@"Share.invoke"]) {
150  [self showShareViewController:args];
151  result(nil);
152  } else {
154  }
155 }
156 
157 - (void)showShareViewController:(NSString*)content {
158  UIViewController* engineViewController = [_engine.get() viewController];
159 
160  NSArray* itemsToShare = @[ content ?: [NSNull null] ];
161  UIActivityViewController* activityViewController =
162  [[[UIActivityViewController alloc] initWithActivityItems:itemsToShare
163  applicationActivities:nil] autorelease];
164 
165  if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
166  // On iPad, the share screen is presented in a popover view, and requires a
167  // sourceView and sourceRect
168  FlutterTextInputPlugin* _textInputPlugin = [_engine.get() textInputPlugin];
169  UITextRange* range = _textInputPlugin.textInputView.selectedTextRange;
170 
171  // firstRectForRange cannot be used here as it's current implementation does
172  // not always return the full rect of the range.
173  CGRect firstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
174  caretRectForPosition:(FlutterTextPosition*)range.start];
175  CGRect transformedFirstRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
176  localRectFromFrameworkTransform:firstRect];
177  CGRect lastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
178  caretRectForPosition:(FlutterTextPosition*)range.end];
179  CGRect transformedLastRect = [(FlutterTextInputView*)_textInputPlugin.textInputView
180  localRectFromFrameworkTransform:lastRect];
181 
182  activityViewController.popoverPresentationController.sourceView = engineViewController.view;
183  // In case of RTL Language, get the minimum x coordinate
184  activityViewController.popoverPresentationController.sourceRect =
185  CGRectMake(fmin(transformedFirstRect.origin.x, transformedLastRect.origin.x),
186  transformedFirstRect.origin.y,
187  abs(transformedLastRect.origin.x - transformedFirstRect.origin.x),
188  transformedFirstRect.size.height);
189  }
190 
191  [engineViewController presentViewController:activityViewController animated:YES completion:nil];
192 }
193 
194 - (void)searchWeb:(NSString*)searchTerm {
195 #if APPLICATION_EXTENSION_API_ONLY
196  FML_LOG(WARNING) << "SearchWeb.invoke is not availabe in app extension.";
197 #else
198  NSString* escapedText = [searchTerm
199  stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet
200  URLHostAllowedCharacterSet]];
201  NSString* searchURL = [NSString stringWithFormat:@"%@%@", searchURLPrefix, escapedText];
202 
203  [[UIApplication sharedApplication] openURL:[NSURL URLWithString:searchURL]
204  options:@{}
205  completionHandler:nil];
206 #endif
207 }
208 
209 - (void)playSystemSound:(NSString*)soundType {
210  if ([soundType isEqualToString:@"SystemSoundType.click"]) {
211  // All feedback types are specific to Android and are treated as equal on
212  // iOS.
213  AudioServicesPlaySystemSound(kKeyPressClickSoundId);
214  }
215 }
216 
217 - (void)vibrateHapticFeedback:(NSString*)feedbackType {
218  if (!feedbackType) {
219  AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
220  return;
221  }
222 
223  if ([@"HapticFeedbackType.lightImpact" isEqualToString:feedbackType]) {
224  [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight] autorelease]
225  impactOccurred];
226  } else if ([@"HapticFeedbackType.mediumImpact" isEqualToString:feedbackType]) {
227  [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleMedium] autorelease]
228  impactOccurred];
229  } else if ([@"HapticFeedbackType.heavyImpact" isEqualToString:feedbackType]) {
230  [[[[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy] autorelease]
231  impactOccurred];
232  } else if ([@"HapticFeedbackType.selectionClick" isEqualToString:feedbackType]) {
233  [[[[UISelectionFeedbackGenerator alloc] init] autorelease] selectionChanged];
234  }
235 }
236 
237 - (void)setSystemChromePreferredOrientations:(NSArray*)orientations {
238  UIInterfaceOrientationMask mask = 0;
239 
240  if (orientations.count == 0) {
241  mask |= UIInterfaceOrientationMaskAll;
242  } else {
243  for (NSString* orientation in orientations) {
244  if ([orientation isEqualToString:@"DeviceOrientation.portraitUp"]) {
245  mask |= UIInterfaceOrientationMaskPortrait;
246  } else if ([orientation isEqualToString:@"DeviceOrientation.portraitDown"]) {
247  mask |= UIInterfaceOrientationMaskPortraitUpsideDown;
248  } else if ([orientation isEqualToString:@"DeviceOrientation.landscapeLeft"]) {
249  mask |= UIInterfaceOrientationMaskLandscapeLeft;
250  } else if ([orientation isEqualToString:@"DeviceOrientation.landscapeRight"]) {
251  mask |= UIInterfaceOrientationMaskLandscapeRight;
252  }
253  }
254  }
255 
256  if (!mask) {
257  return;
258  }
259  [[NSNotificationCenter defaultCenter]
260  postNotificationName:@(kOrientationUpdateNotificationName)
261  object:nil
262  userInfo:@{@(kOrientationUpdateNotificationKey) : @(mask)}];
263 }
264 
265 - (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object {
266  // No counterpart on iOS but is a benign operation. So no asserts.
267 }
268 
269 - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
270  BOOL statusBarShouldBeHidden = ![overlays containsObject:@"SystemUiOverlay.top"];
271  if ([overlays containsObject:@"SystemUiOverlay.bottom"]) {
272  [[NSNotificationCenter defaultCenter]
273  postNotificationName:FlutterViewControllerShowHomeIndicator
274  object:nil];
275  } else {
276  [[NSNotificationCenter defaultCenter]
277  postNotificationName:FlutterViewControllerHideHomeIndicator
278  object:nil];
279  }
280  if (self.enableViewControllerBasedStatusBarAppearance) {
281  [_engine.get() viewController].prefersStatusBarHidden = statusBarShouldBeHidden;
282  } else {
283  // Checks if the top status bar should be visible. This platform ignores all
284  // other overlays
285 
286  // We opt out of view controller based status bar visibility since we want
287  // to be able to modify this on the fly. The key used is
288  // UIViewControllerBasedStatusBarAppearance.
289  SetStatusBarHiddenForSharedApplication(statusBarShouldBeHidden);
290  }
291 }
292 
293 - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode {
294  BOOL edgeToEdge = [mode isEqualToString:@"SystemUiMode.edgeToEdge"];
295  if (self.enableViewControllerBasedStatusBarAppearance) {
296  [_engine.get() viewController].prefersStatusBarHidden = !edgeToEdge;
297  } else {
298  // Checks if the top status bar should be visible, reflected by edge to edge setting. This
299  // platform ignores all other system ui modes.
300 
301  // We opt out of view controller based status bar visibility since we want
302  // to be able to modify this on the fly. The key used is
303  // UIViewControllerBasedStatusBarAppearance.
305  }
306  [[NSNotificationCenter defaultCenter]
307  postNotificationName:edgeToEdge ? FlutterViewControllerShowHomeIndicator
308  : FlutterViewControllerHideHomeIndicator
309  object:nil];
310 }
311 
312 - (void)restoreSystemChromeSystemUIOverlays {
313  // Nothing to do on iOS.
314 }
315 
316 - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
317  NSString* brightness = message[@"statusBarBrightness"];
318  if (brightness == (id)[NSNull null]) {
319  return;
320  }
321 
322  UIStatusBarStyle statusBarStyle;
323  if ([brightness isEqualToString:@"Brightness.dark"]) {
324  statusBarStyle = UIStatusBarStyleLightContent;
325  } else if ([brightness isEqualToString:@"Brightness.light"]) {
326  if (@available(iOS 13, *)) {
327  statusBarStyle = UIStatusBarStyleDarkContent;
328  } else {
329  statusBarStyle = UIStatusBarStyleDefault;
330  }
331  } else {
332  return;
333  }
334 
335  if (self.enableViewControllerBasedStatusBarAppearance) {
336  // This notification is respected by the iOS embedder.
337  [[NSNotificationCenter defaultCenter]
338  postNotificationName:@(kOverlayStyleUpdateNotificationName)
339  object:nil
340  userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}];
341  } else {
343  }
344 }
345 
346 - (void)popSystemNavigator:(BOOL)isAnimated {
347  // Apple's human user guidelines say not to terminate iOS applications. However, if the
348  // root view of the app is a navigation controller, it is instructed to back up a level
349  // in the navigation hierarchy.
350  // It's also possible in an Add2App scenario that the FlutterViewController was presented
351  // outside the context of a UINavigationController, and still wants to be popped.
352 
353  FlutterViewController* engineViewController = [_engine.get() viewController];
354  UINavigationController* navigationController = [engineViewController navigationController];
355  if (navigationController) {
356  [navigationController popViewControllerAnimated:isAnimated];
357  } else {
358  UIViewController* rootViewController = nil;
359 #if APPLICATION_EXTENSION_API_ONLY
360  if (@available(iOS 15.0, *)) {
361  rootViewController =
362  [engineViewController flutterWindowSceneIfViewLoaded].keyWindow.rootViewController;
363  } else {
364  FML_LOG(WARNING)
365  << "rootViewController is not available in application extension prior to iOS 15.0.";
366  }
367 #else
368  rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
369 #endif
370  if (engineViewController != rootViewController) {
371  [engineViewController dismissViewControllerAnimated:isAnimated completion:nil];
372  }
373  }
374 }
375 
376 - (NSDictionary*)getClipboardData:(NSString*)format {
377  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
378  if (!format || [format isEqualToString:@(kTextPlainFormat)]) {
379  NSString* stringInPasteboard = pasteboard.string;
380  // The pasteboard may contain an item but it may not be a string (an image for instance).
381  return stringInPasteboard == nil ? nil : @{@"text" : stringInPasteboard};
382  }
383  return nil;
384 }
385 
386 - (void)setClipboardData:(NSDictionary*)data {
387  UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
388  id copyText = data[@"text"];
389  if ([copyText isKindOfClass:[NSString class]]) {
390  pasteboard.string = copyText;
391  } else {
392  pasteboard.string = @"null";
393  }
394 }
395 
396 - (NSDictionary*)clipboardHasStrings {
397  return @{@"value" : @([UIPasteboard generalPasteboard].hasStrings)};
398 }
399 
400 - (BOOL)isLiveTextInputAvailable {
401  return [[self textField] canPerformAction:@selector(captureTextFromCamera:) withSender:nil];
402 }
403 
404 - (void)showLookUpViewController:(NSString*)term {
405  UIViewController* engineViewController = [_engine.get() viewController];
406  UIReferenceLibraryViewController* referenceLibraryViewController =
407  [[[UIReferenceLibraryViewController alloc] initWithTerm:term] autorelease];
408  [engineViewController presentViewController:referenceLibraryViewController
409  animated:YES
410  completion:nil];
411 }
412 
413 - (UITextField*)textField {
414  if (_textField == nil) {
415  _textField = [[UITextField alloc] init];
416  }
417  return _textField;
418 }
419 
420 - (void)dealloc {
421  [_textField release];
422  [super dealloc];
423 }
424 @end
FlutterEngine
Definition: FlutterEngine.h:61
SetStatusBarHiddenForSharedApplication
static void SetStatusBarHiddenForSharedApplication(BOOL hidden)
Definition: FlutterPlatformPlugin.mm:45
FlutterViewController
Definition: FlutterViewController.h:56
_engine
fml::scoped_nsobject< FlutterEngine > _engine
Definition: FlutterViewController.mm:120
FlutterMethodNotImplemented
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
_textInputPlugin
fml::scoped_nsobject< FlutterTextInputPlugin > _textInputPlugin
Definition: FlutterEngine.mm:132
flutter::kOrientationUpdateNotificationKey
const char *const kOrientationUpdateNotificationKey
Definition: FlutterPlatformPlugin.mm:34
FlutterTextInputPlugin.h
FlutterEngine_Internal.h
FlutterMethodCall::method
NSString * method
Definition: FlutterCodecs.h:233
flutter::kOrientationUpdateNotificationName
const char *const kOrientationUpdateNotificationName
Definition: FlutterPlatformPlugin.mm:32
flutter::kOverlayStyleUpdateNotificationKey
const char *const kOverlayStyleUpdateNotificationKey
Definition: FlutterPlatformPlugin.mm:38
FlutterTextInputView
Definition: FlutterTextInputPlugin.mm:801
_textField
UITextField * _textField
Definition: FlutterPlatformPlugin.mm:76
FlutterMethodCall
Definition: FlutterCodecs.h:220
flutter
Definition: accessibility_bridge.h:28
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:33
flutter::kOverlayStyleUpdateNotificationName
const char *const kOverlayStyleUpdateNotificationName
Definition: FlutterPlatformPlugin.mm:36
FlutterResult
void(^ FlutterResult)(id _Nullable result)
Definition: FlutterChannels.h:194
UIViewController+FlutterScreenAndSceneIfLoaded.h
FlutterPlatformPlugin.h
engine
id engine
Definition: FlutterTextInputPluginTest.mm:89
FlutterViewController_Internal.h
FlutterPlatformPlugin
Definition: FlutterPlatformPlugin.h:12
SetStatusBarStyleForSharedApplication
static void SetStatusBarStyleForSharedApplication(UIStatusBarStyle style)
Definition: FlutterPlatformPlugin.mm:53
FlutterMethodCall::arguments
id arguments
Definition: FlutterCodecs.h:238