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