| 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/FlutterTextInputPlugin.h"
15 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h"
16 #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.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.
32 const char* const kOrientationUpdateNotificationName =
33 "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
34 const char* const kOrientationUpdateNotificationKey =
35 "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
36 const char* const kOverlayStyleUpdateNotificationName =
37 "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
38 const char* const kOverlayStyleUpdateNotificationKey =
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::WeakPtr<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::WeakPtr<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 {
153 result(FlutterMethodNotImplemented);
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.
304 SetStatusBarHiddenForSharedApplication(!edgeToEdge);
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 {
342 SetStatusBarStyleForSharedApplication(statusBarStyle);
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:59
SetStatusBarHiddenForSharedApplication
static void SetStatusBarHiddenForSharedApplication(BOOL hidden)
Definition: FlutterPlatformPlugin.mm:45
FlutterViewController
Definition: FlutterViewController.h:55
_engine
fml::scoped_nsobject< FlutterEngine > _engine
Definition: FlutterViewController.mm:114
FlutterMethodNotImplemented
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
_textInputPlugin
fml::scoped_nsobject< FlutterTextInputPlugin > _textInputPlugin
Definition: FlutterEngine.mm:131
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:29
flutter::kOverlayStyleUpdateNotificationName
const char *const kOverlayStyleUpdateNotificationName
Definition: FlutterPlatformPlugin.mm:36
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:53
FlutterMethodCall::arguments
id arguments
Definition: FlutterCodecs.h:238