7 #import <AudioToolbox/AudioToolbox.h>
8 #import <Foundation/Foundation.h>
9 #import <UIKit/UIApplication.h>
10 #import <UIKit/UIKit.h>
12 #include "flutter/fml/logging.h"
34 "io.flutter.plugin.platform.SystemChromeOrientationNotificationName";
36 "io.flutter.plugin.platform.SystemChromeOrientationNotificationKey";
38 "io.flutter.plugin.platform.SystemChromeOverlayNotificationName";
40 "io.flutter.plugin.platform.SystemChromeOverlayNotificationKey";
48 if (flutterApplication) {
49 flutterApplication.statusBarHidden = hidden;
51 FML_LOG(WARNING) <<
"Application based status bar styling is not available in app extension.";
57 if (flutterApplication) {
60 [flutterApplication setStatusBarStyle:style];
62 FML_LOG(WARNING) <<
"Application based status bar styling is not available in app extension.";
75 @property(nonatomic, assign) BOOL enableViewControllerBasedStatusBarAppearance;
81 @property(nonatomic, strong) UITextField* textField;
87 FML_DCHECK(
engine) <<
"engine must be set";
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 "
100 _enableViewControllerBasedStatusBarAppearance =
101 (infoValue == nil || [(NSNumber*)infoValue boolValue]);
108 NSString* method = call.
method;
110 if ([method isEqualToString:
@"SystemSound.play"]) {
111 [
self playSystemSound:args];
113 }
else if ([method isEqualToString:
@"HapticFeedback.vibrate"]) {
114 [
self vibrateHapticFeedback:args];
116 }
else if ([method isEqualToString:
@"SystemChrome.setPreferredOrientations"]) {
117 [
self setSystemChromePreferredOrientations:args];
119 }
else if ([method isEqualToString:
@"SystemChrome.setApplicationSwitcherDescription"]) {
120 [
self setSystemChromeApplicationSwitcherDescription:args];
122 }
else if ([method isEqualToString:
@"SystemChrome.setEnabledSystemUIOverlays"]) {
123 [
self setSystemChromeEnabledSystemUIOverlays:args];
125 }
else if ([method isEqualToString:
@"SystemChrome.setEnabledSystemUIMode"]) {
126 [
self setSystemChromeEnabledSystemUIMode:args];
128 }
else if ([method isEqualToString:
@"SystemChrome.restoreSystemUIOverlays"]) {
129 [
self restoreSystemChromeSystemUIOverlays];
131 }
else if ([method isEqualToString:
@"SystemChrome.setSystemUIOverlayStyle"]) {
132 [
self setSystemChromeSystemUIOverlayStyle:args];
134 }
else if ([method isEqualToString:
@"SystemNavigator.pop"]) {
135 NSNumber* isAnimated = args;
136 [
self popSystemNavigator:isAnimated.boolValue];
138 }
else if ([method isEqualToString:
@"Clipboard.getData"]) {
139 result([
self getClipboardData:args]);
140 }
else if ([method isEqualToString:
@"Clipboard.setData"]) {
141 [
self setClipboardData:args];
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];
150 }
else if ([method isEqualToString:
@"LookUp.invoke"]) {
151 [
self showLookUpViewController:args];
153 }
else if ([method isEqualToString:
@"Share.invoke"]) {
154 [
self showShareViewController:args];
156 }
else if ([method isEqualToString:
@"ContextMenu.showSystemContextMenu"]) {
157 [
self showSystemContextMenu:args];
159 }
else if ([method isEqualToString:
@"ContextMenu.hideSystemContextMenu"]) {
160 [
self hideSystemContextMenu];
167 - (void)showSystemContextMenu:(NSDictionary*)args {
168 if (@available(iOS 16.0, *)) {
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.";
179 - (void)hideSystemContextMenu {
180 if (@available(iOS 16.0, *)) {
182 [textInputPlugin hideEditMenu];
186 - (void)showShareViewController:(NSString*)content {
187 UIViewController* engineViewController = [
self.engine viewController];
189 NSArray* itemsToShare = @[ content ?: [NSNull null] ];
190 UIActivityViewController* activityViewController =
191 [[UIActivityViewController alloc] initWithActivityItems:itemsToShare
192 applicationActivities:nil];
194 if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
198 UITextRange* range = _textInputPlugin.
textInputView.selectedTextRange;
203 caretRectForPosition:(FlutterTextPosition*)range.start];
205 localRectFromFrameworkTransform:firstRect];
207 caretRectForPosition:(FlutterTextPosition*)range.end];
209 localRectFromFrameworkTransform:lastRect];
211 activityViewController.popoverPresentationController.sourceView = engineViewController.view;
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);
220 [engineViewController presentViewController:activityViewController animated:YES completion:nil];
223 - (void)searchWeb:(NSString*)searchTerm {
225 if (flutterApplication == nil) {
226 FML_LOG(WARNING) <<
"SearchWeb.invoke is not availabe in app extension.";
230 NSString* escapedText = [searchTerm
231 stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet
232 URLHostAllowedCharacterSet]];
233 NSString* searchURL = [NSString stringWithFormat:@"%@%@", kSearchURLPrefix, escapedText];
235 [flutterApplication openURL:[NSURL URLWithString:searchURL] options:@{} completionHandler:nil];
238 - (void)playSystemSound:(NSString*)soundType {
239 if ([soundType isEqualToString:
@"SystemSoundType.click"]) {
246 - (void)vibrateHapticFeedback:(NSString*)feedbackType {
248 AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
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];
263 - (void)setSystemChromePreferredOrientations:(NSArray*)orientations {
264 UIInterfaceOrientationMask mask = 0;
266 if (orientations.count == 0) {
267 mask |= UIInterfaceOrientationMaskAll;
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;
285 [[NSNotificationCenter defaultCenter]
286 postNotificationName:@(kOrientationUpdateNotificationName)
288 userInfo:@{@(kOrientationUpdateNotificationKey) : @(mask)}];
291 - (void)setSystemChromeApplicationSwitcherDescription:(NSDictionary*)object {
295 - (void)setSystemChromeEnabledSystemUIOverlays:(NSArray*)overlays {
296 BOOL statusBarShouldBeHidden = ![overlays containsObject:@"SystemUiOverlay.top"];
297 if ([overlays containsObject:
@"SystemUiOverlay.bottom"]) {
298 [[NSNotificationCenter defaultCenter]
299 postNotificationName:FlutterViewControllerShowHomeIndicator
302 [[NSNotificationCenter defaultCenter]
303 postNotificationName:FlutterViewControllerHideHomeIndicator
306 if (
self.enableViewControllerBasedStatusBarAppearance) {
307 [
self.engine viewController].prefersStatusBarHidden = statusBarShouldBeHidden;
319 - (void)setSystemChromeEnabledSystemUIMode:(NSString*)mode {
320 BOOL edgeToEdge = [mode isEqualToString:@"SystemUiMode.edgeToEdge"];
321 if (
self.enableViewControllerBasedStatusBarAppearance) {
322 [
self.engine viewController].prefersStatusBarHidden = !edgeToEdge;
332 [[NSNotificationCenter defaultCenter]
333 postNotificationName:edgeToEdge ? FlutterViewControllerShowHomeIndicator
334 : FlutterViewControllerHideHomeIndicator
338 - (void)restoreSystemChromeSystemUIOverlays {
342 - (void)setSystemChromeSystemUIOverlayStyle:(NSDictionary*)message {
343 NSString* brightness = message[@"statusBarBrightness"];
344 if (brightness == (
id)[NSNull
null]) {
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;
355 statusBarStyle = UIStatusBarStyleDefault;
361 if (
self.enableViewControllerBasedStatusBarAppearance) {
363 [[NSNotificationCenter defaultCenter]
364 postNotificationName:@(kOverlayStyleUpdateNotificationName)
366 userInfo:@{@(kOverlayStyleUpdateNotificationKey) : @(statusBarStyle)}];
372 - (void)popSystemNavigator:(BOOL)isAnimated {
380 UINavigationController* navigationController = [engineViewController navigationController];
381 if (navigationController) {
382 [navigationController popViewControllerAnimated:isAnimated];
384 UIViewController* rootViewController = nil;
386 if (flutterApplication) {
387 rootViewController = flutterApplication.keyWindow.rootViewController;
389 if (@available(iOS 15.0, *)) {
391 [engineViewController flutterWindowSceneIfViewLoaded].keyWindow.rootViewController;
394 <<
"rootViewController is not available in application extension prior to iOS 15.0.";
398 if (engineViewController != rootViewController) {
399 [engineViewController dismissViewControllerAnimated:isAnimated completion:nil];
404 - (NSDictionary*)getClipboardData:(NSString*)format {
405 UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
407 NSString* stringInPasteboard = pasteboard.string;
409 return stringInPasteboard == nil ? nil : @{
@"text" : stringInPasteboard};
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;
420 pasteboard.string =
@"null";
424 - (NSDictionary*)clipboardHasStrings {
425 return @{
@"value" : @([UIPasteboard generalPasteboard].hasStrings)};
428 - (BOOL)isLiveTextInputAvailable {
429 return [[
self textField] canPerformAction:@selector(captureTextFromCamera:) withSender:nil];
432 - (void)showLookUpViewController:(NSString*)term {
433 UIViewController* engineViewController = [
self.engine viewController];
434 UIReferenceLibraryViewController* referenceLibraryViewController =
435 [[UIReferenceLibraryViewController alloc] initWithTerm:term];
436 [engineViewController presentViewController:referenceLibraryViewController
441 - (UITextField*)textField {
442 if (_textField == nil) {
443 _textField = [[UITextField alloc] init];
void(^ FlutterResult)(id _Nullable result)
FLUTTER_DARWIN_EXPORT NSObject const * FlutterMethodNotImplemented
FlutterTextInputPlugin * textInputPlugin
UIApplication * application
UIView< UITextInput > * textInputView()
BOOL showEditMenu:(ios(16.0) API_AVAILABLE)
constexpr char kTextPlainFormat[]
const UInt32 kKeyPressClickSoundId
NSString *const kSearchURLPrefix
const char *const kOrientationUpdateNotificationKey
const char *const kOverlayStyleUpdateNotificationName
const char *const kOverlayStyleUpdateNotificationKey
const char *const kOrientationUpdateNotificationName