| 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/FlutterViewController_Internal.h"
14 #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.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.
30 const char* const kOrientationUpdateNotificationName =
31 "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
32 const char* const kOrientationUpdateNotificationKey =
33 "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
34 const char* const kOverlayStyleUpdateNotificationName =
35 "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
36 const char* const kOverlayStyleUpdateNotificationKey =
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 {
151 result(FlutterMethodNotImplemented);
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.
274 SetStatusBarHiddenForSharedApplication(!edgeToEdge);
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 {
312 SetStatusBarStyleForSharedApplication(statusBarStyle);
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