react-native/React/Modules/RCTAlertManager.m

274 lines
9.4 KiB
Mathematica
Raw Normal View History

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTAlertManager.h"
#import "RCTAssert.h"
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTUtils.h"
@implementation RCTConvert (UIAlertViewStyle)
RCT_ENUM_CONVERTER(UIAlertViewStyle, (@{
@"default": @(UIAlertViewStyleDefault),
@"secure-text": @(UIAlertViewStyleSecureTextInput),
@"plain-text": @(UIAlertViewStylePlainTextInput),
@"login-password": @(UIAlertViewStyleLoginAndPasswordInput),
}), UIAlertViewStyleDefault, integerValue)
@end
@interface RCTAlertManager() <UIAlertViewDelegate>
@end
@implementation RCTAlertManager
{
NSMutableArray<UIAlertView *> *_alerts;
NSMutableArray<UIAlertController *> *_alertControllers;
NSMutableArray<RCTResponseSenderBlock> *_alertCallbacks;
NSMutableArray<NSArray<NSString *> *> *_alertButtonKeys;
}
RCT_EXPORT_MODULE()
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
- (void)invalidate
{
for (UIAlertView *alert in _alerts) {
[alert dismissWithClickedButtonIndex:0 animated:YES];
}
for (UIAlertController *alertController in _alertControllers) {
[alertController.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
}
/**
* @param {NSDictionary} args Dictionary of the form
*
* @{
* @"message": @"<Alert message>",
* @"buttons": @[
* @{@"<key1>": @"<title1>"},
* @{@"<key2>": @"<title2>"},
* ],
* @"cancelButtonKey": @"<key2>",
* }
* The key from the `buttons` dictionary is passed back in the callback on click.
* Buttons are displayed in the order they are specified.
*/
2015-04-08 15:52:48 +00:00
RCT_EXPORT_METHOD(alertWithArgs:(NSDictionary *)args
callback:(RCTResponseSenderBlock)callback)
{
NSString *title = [RCTConvert NSString:args[@"title"]];
NSString *message = [RCTConvert NSString:args[@"message"]];
UIAlertViewStyle type = [RCTConvert UIAlertViewStyle:args[@"type"]];
NSArray<NSDictionary *> *buttons = [RCTConvert NSDictionaryArray:args[@"buttons"]];
NSString *cancelButtonKey = [RCTConvert NSString:args[@"cancelButtonKey"]];
NSString *destructiveButtonKey = [RCTConvert NSString:args[@"destructiveButtonKey"]];
if (!title && !message) {
RCTLogError(@"Must specify either an alert title, or message, or both");
return;
}
if (buttons.count == 0) {
if (type == UIAlertViewStyleDefault) {
buttons = @[@{@"0": RCTUIKitLocalizedString(@"OK")}];
cancelButtonKey = @"0";
} else {
buttons = @[
@{@"0": RCTUIKitLocalizedString(@"OK")},
@{@"1": RCTUIKitLocalizedString(@"Cancel")},
];
cancelButtonKey = @"1";
}
}
#if __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0
// TODO: we've encountered some bug when presenting alerts on top of a window
// that is subsequently dismissed. As a temporary solution to this, we'll use
// UIAlertView preferentially if it's available and supports our use case.
BOOL preferAlertView = (!RCTRunningInAppExtension() &&
!destructiveButtonKey &&
UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone);
if (preferAlertView || [UIAlertController class] == nil) {
UIAlertView *alertView = RCTAlertView(title, nil, self, nil, nil);
alertView.alertViewStyle = type;
alertView.message = message;
if (type != UIAlertViewStyleDefault) {
NSString *defaultValue = [RCTConvert NSString:args[@"defaultValue"]];
[alertView textFieldAtIndex:0].text = defaultValue;
}
NSMutableArray<NSString *> *buttonKeys =
[[NSMutableArray alloc] initWithCapacity:buttons.count];
NSInteger index = 0;
for (NSDictionary<NSString *, id> *button in buttons) {
if (button.count != 1) {
RCTLogError(@"Button definitions should have exactly one key.");
}
NSString *buttonKey = button.allKeys.firstObject;
NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]];
[alertView addButtonWithTitle:buttonTitle];
if ([buttonKey isEqualToString:cancelButtonKey]) {
alertView.cancelButtonIndex = buttonKeys.count;
}
[buttonKeys addObject:buttonKey];
index ++;
}
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
if (!_alerts) {
_alerts = [NSMutableArray new];
_alertCallbacks = [NSMutableArray new];
_alertButtonKeys = [NSMutableArray new];
}
[_alerts addObject:alertView];
[_alertCallbacks addObject:callback ?: ^(__unused id unused) {}];
[_alertButtonKeys addObject:buttonKeys];
[alertView show];
} else
#endif
{
UIViewController *presentingController = RCTKeyWindow().rootViewController;
if (presentingController == nil) {
RCTLogError(@"Tried to display alert view but there is no application window. args: %@", args);
return;
}
// Walk the chain up to get the topmost modal view controller. If modals are
// presented the root view controller's view might not be in the window
// hierarchy, and presenting from it will fail.
while (presentingController.presentedViewController) {
presentingController = presentingController.presentedViewController;
}
UIAlertController *alertController =
[UIAlertController alertControllerWithTitle:title
message:nil
preferredStyle:UIAlertControllerStyleAlert];
switch (type) {
case UIAlertViewStylePlainTextInput:
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.secureTextEntry = NO;
}];
break;
case UIAlertViewStyleSecureTextInput:
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = RCTUIKitLocalizedString(@"Password");
textField.secureTextEntry = YES;
}];
break;
case UIAlertViewStyleLoginAndPasswordInput:
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = RCTUIKitLocalizedString(@"Login");
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = RCTUIKitLocalizedString(@"Password");
textField.secureTextEntry = YES;
}];
case UIAlertViewStyleDefault:
break;
}
alertController.message = message;
for (NSDictionary<NSString *, id> *button in buttons) {
if (button.count != 1) {
RCTLogError(@"Button definitions should have exactly one key.");
}
NSString *buttonKey = button.allKeys.firstObject;
NSString *buttonTitle = [RCTConvert NSString:button[buttonKey]];
UIAlertActionStyle buttonStyle = UIAlertActionStyleDefault;
if ([buttonKey isEqualToString:cancelButtonKey]) {
buttonStyle = UIAlertActionStyleCancel;
} else if ([buttonKey isEqualToString:destructiveButtonKey]) {
buttonStyle = UIAlertActionStyleDestructive;
}
[alertController addAction:[UIAlertAction actionWithTitle:buttonTitle
style:buttonStyle
handler:^(__unused UIAlertAction *action) {
switch (type) {
case UIAlertViewStylePlainTextInput:
case UIAlertViewStyleSecureTextInput:
callback(@[buttonKey, [alertController.textFields.firstObject text]]);
break;
case UIAlertViewStyleLoginAndPasswordInput: {
NSDictionary<NSString *, NSString *> *loginCredentials = @{
@"login": [alertController.textFields.firstObject text],
@"password": [alertController.textFields.lastObject text]
};
callback(@[buttonKey, loginCredentials]);
break;
}
case UIAlertViewStyleDefault:
callback(@[buttonKey]);
break;
}
}]];
}
Refactored module access to allow for lazy loading Summary: public The `bridge.modules` dictionary provides access to all native modules, but this API requires that every module is initialized in advance so that any module can be accessed. This diff introduces a better API that will allow modules to be initialized lazily as they are needed, and deprecates `bridge.modules` (modules that use it will still work, but should be rewritten to use `bridge.moduleClasses` or `-[bridge moduleForName/Class:` instead. The rules are now as follows: * Any module that overrides `init` or `setBridge:` will be initialized on the main thread when the bridge is created * Any module that implements `constantsToExport:` will be initialized later when the config is exported (the module itself will be initialized on a background queue, but `constantsToExport:` will still be called on the main thread. * All other modules will be initialized lazily when a method is first called on them. These rules may seem slightly arcane, but they have the advantage of not violating any assumptions that may have been made by existing code - any module written under the original assumption that it would be initialized synchronously on the main thread when the bridge is created should still function exactly the same, but modules that avoid overriding `init` or `setBridge:` will now be loaded lazily. I've rewritten most of the standard modules to take advantage of this new lazy loading, with the following results: Out of the 65 modules included in UIExplorer: * 16 are initialized on the main thread when the bridge is created * A further 8 are initialized when the config is exported to JS * The remaining 41 will be initialized lazily on-demand Reviewed By: jspahrsummers Differential Revision: D2677695 fb-gh-sync-id: 507ae7e9fd6b563e89292c7371767c978e928f33
2015-11-25 11:09:00 +00:00
if (!_alertControllers) {
_alertControllers = [NSMutableArray new];
}
[_alertControllers addObject:alertController];
[presentingController presentViewController:alertController animated:YES completion:nil];
}
}
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
NSUInteger index = [_alerts indexOfObject:alertView];
RCTAssert(index != NSNotFound, @"Dismissed alert was not recognised");
RCTResponseSenderBlock callback = _alertCallbacks[index];
NSArray<NSString *> *buttonKeys = _alertButtonKeys[index];
switch (alertView.alertViewStyle) {
case UIAlertViewStylePlainTextInput:
case UIAlertViewStyleSecureTextInput:
callback(@[buttonKeys[buttonIndex], [alertView textFieldAtIndex:0].text]);
break;
case UIAlertViewStyleLoginAndPasswordInput: {
NSDictionary<NSString *, NSString *> *loginCredentials = @{
@"login": [alertView textFieldAtIndex:0].text,
@"password": [alertView textFieldAtIndex:1].text,
};
callback(@[buttonKeys[buttonIndex], loginCredentials]);
break;
}
case UIAlertViewStyleDefault:
callback(@[buttonKeys[buttonIndex]]);
break;
}
[_alerts removeObjectAtIndex:index];
[_alertCallbacks removeObjectAtIndex:index];
[_alertButtonKeys removeObjectAtIndex:index];
}
@end