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