2015-03-23 20:28:42 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2015-02-20 04:10:52 +00:00
|
|
|
|
|
|
|
#import "RCTUtils.h"
|
|
|
|
|
|
|
|
#import <mach/mach_time.h>
|
2015-07-16 16:34:28 +00:00
|
|
|
#import <objc/message.h>
|
2015-03-09 23:58:39 +00:00
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
#import <UIKit/UIKit.h>
|
|
|
|
|
2015-03-09 23:58:39 +00:00
|
|
|
#import <CommonCrypto/CommonCrypto.h>
|
|
|
|
|
2015-07-16 16:34:28 +00:00
|
|
|
#import <zlib.h>
|
|
|
|
#import <dlfcn.h>
|
|
|
|
|
2015-03-09 23:58:39 +00:00
|
|
|
#import "RCTLog.h"
|
|
|
|
|
2016-01-19 20:19:47 +00:00
|
|
|
NSString *const RCTErrorUnspecified = @"EUNSPECIFIED";
|
|
|
|
|
2016-01-26 14:12:48 +00:00
|
|
|
static NSString *__nullable _RCTJSONStringifyNoRetry(id __nullable jsonObject, NSError **error)
|
2015-02-20 04:10:52 +00:00
|
|
|
{
|
2016-01-26 14:12:48 +00:00
|
|
|
if (!jsonObject) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2015-07-20 16:20:06 +00:00
|
|
|
static SEL JSONKitSelector = NULL;
|
2015-11-03 22:45:46 +00:00
|
|
|
static NSSet<Class> *collectionTypes;
|
2015-07-20 16:20:06 +00:00
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
SEL selector = NSSelectorFromString(@"JSONStringWithOptions:error:");
|
|
|
|
if ([NSDictionary instancesRespondToSelector:selector]) {
|
|
|
|
JSONKitSelector = selector;
|
|
|
|
collectionTypes = [NSSet setWithObjects:
|
|
|
|
[NSArray class], [NSMutableArray class],
|
|
|
|
[NSDictionary class], [NSMutableDictionary class], nil];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2016-01-26 14:12:48 +00:00
|
|
|
@try {
|
|
|
|
|
|
|
|
// Use JSONKit if available and object is not a fragment
|
|
|
|
if (JSONKitSelector && [collectionTypes containsObject:[jsonObject classForCoder]]) {
|
|
|
|
return ((NSString *(*)(id, SEL, int, NSError **))objc_msgSend)(jsonObject, JSONKitSelector, 0, error);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use Foundation JSON method
|
|
|
|
NSData *jsonData = [NSJSONSerialization
|
|
|
|
dataWithJSONObject:jsonObject options:(NSJSONWritingOptions)NSJSONReadingAllowFragments
|
|
|
|
error:error];
|
|
|
|
|
|
|
|
return jsonData ? [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding] : nil;
|
2015-07-20 16:20:06 +00:00
|
|
|
}
|
2016-01-26 14:12:48 +00:00
|
|
|
@catch (NSException *exception) {
|
2015-07-20 16:20:06 +00:00
|
|
|
|
2016-01-26 14:12:48 +00:00
|
|
|
// Convert exception to error
|
|
|
|
if (error) {
|
|
|
|
*error = [NSError errorWithDomain:RCTErrorDomain code:0 userInfo:@{
|
|
|
|
NSLocalizedDescriptionKey: exception.description ?: @""
|
|
|
|
}];
|
|
|
|
}
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
NSString *__nullable RCTJSONStringify(id __nullable jsonObject, NSError **error)
|
|
|
|
{
|
|
|
|
if (error) {
|
|
|
|
return _RCTJSONStringifyNoRetry(jsonObject, error);
|
|
|
|
} else {
|
|
|
|
NSError *localError;
|
|
|
|
NSString *json = _RCTJSONStringifyNoRetry(jsonObject, &localError);
|
|
|
|
if (localError) {
|
|
|
|
RCTLogError(@"RCTJSONStringify() encountered the following error: %@",
|
|
|
|
localError.localizedDescription);
|
|
|
|
// Sanitize the data, then retry. This is slow, but it prevents uncaught
|
|
|
|
// data issues from crashing in production
|
|
|
|
return _RCTJSONStringifyNoRetry(RCTJSONClean(jsonObject), NULL);
|
|
|
|
}
|
|
|
|
return json;
|
|
|
|
}
|
2015-02-20 04:10:52 +00:00
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
static id __nullable _RCTJSONParse(NSString *__nullable jsonString, BOOL mutable, NSError **error)
|
2015-02-20 04:10:52 +00:00
|
|
|
{
|
2015-07-20 16:20:06 +00:00
|
|
|
static SEL JSONKitSelector = NULL;
|
|
|
|
static SEL JSONKitMutableSelector = NULL;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
SEL selector = NSSelectorFromString(@"objectFromJSONStringWithParseOptions:error:");
|
|
|
|
if ([NSString instancesRespondToSelector:selector]) {
|
|
|
|
JSONKitSelector = selector;
|
|
|
|
JSONKitMutableSelector = NSSelectorFromString(@"mutableObjectFromJSONStringWithParseOptions:error:");
|
2015-03-25 00:47:59 +00:00
|
|
|
}
|
2015-07-20 16:20:06 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
if (jsonString) {
|
|
|
|
|
|
|
|
// Use JSONKit if available and string is not a fragment
|
|
|
|
if (JSONKitSelector) {
|
|
|
|
NSInteger length = jsonString.length;
|
|
|
|
for (NSInteger i = 0; i < length; i++) {
|
|
|
|
unichar c = [jsonString characterAtIndex:i];
|
|
|
|
if (strchr("{[", c)) {
|
|
|
|
static const int options = (1 << 2); // loose unicode
|
|
|
|
SEL selector = mutable ? JSONKitMutableSelector : JSONKitSelector;
|
|
|
|
return ((id (*)(id, SEL, int, NSError **))objc_msgSend)(jsonString, selector, options, error);
|
|
|
|
}
|
|
|
|
if (!strchr(" \r\n\t", c)) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Use Foundation JSON method
|
|
|
|
NSData *jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding];
|
|
|
|
if (!jsonData) {
|
|
|
|
jsonData = [jsonString dataUsingEncoding:NSUTF8StringEncoding allowLossyConversion:YES];
|
|
|
|
if (jsonData) {
|
|
|
|
RCTLogWarn(@"RCTJSONParse received the following string, which could "
|
|
|
|
"not be losslessly converted to UTF8 data: '%@'", jsonString);
|
|
|
|
} else {
|
|
|
|
NSString *errorMessage = @"RCTJSONParse received invalid UTF8 data";
|
|
|
|
if (error) {
|
|
|
|
*error = RCTErrorWithMessage(errorMessage);
|
|
|
|
} else {
|
|
|
|
RCTLogError(@"%@", errorMessage);
|
|
|
|
}
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
NSJSONReadingOptions options = NSJSONReadingAllowFragments;
|
|
|
|
if (mutable) {
|
|
|
|
options |= NSJSONReadingMutableContainers;
|
|
|
|
}
|
|
|
|
return [NSJSONSerialization JSONObjectWithData:jsonData
|
|
|
|
options:options
|
|
|
|
error:error];
|
2015-03-09 23:58:39 +00:00
|
|
|
}
|
2015-07-20 16:20:06 +00:00
|
|
|
return nil;
|
2015-06-03 23:57:08 +00:00
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
id __nullable RCTJSONParse(NSString *__nullable jsonString, NSError **error)
|
2015-07-20 16:20:06 +00:00
|
|
|
{
|
|
|
|
return _RCTJSONParse(jsonString, NO, error);
|
2015-06-03 23:57:08 +00:00
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
id __nullable RCTJSONParseMutable(NSString *__nullable jsonString, NSError **error)
|
2015-07-20 16:20:06 +00:00
|
|
|
{
|
|
|
|
return _RCTJSONParse(jsonString, YES, error);
|
2015-02-20 04:10:52 +00:00
|
|
|
}
|
|
|
|
|
2015-05-06 07:11:31 +00:00
|
|
|
id RCTJSONClean(id object)
|
|
|
|
{
|
|
|
|
static dispatch_once_t onceToken;
|
2015-11-03 22:45:46 +00:00
|
|
|
static NSSet<Class> *validLeafTypes;
|
2015-05-06 07:11:31 +00:00
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
validLeafTypes = [[NSSet alloc] initWithArray:@[
|
|
|
|
[NSString class],
|
|
|
|
[NSMutableString class],
|
|
|
|
[NSNumber class],
|
|
|
|
[NSNull class],
|
|
|
|
]];
|
|
|
|
});
|
|
|
|
|
|
|
|
if ([validLeafTypes containsObject:[object classForCoder]]) {
|
2016-01-26 14:12:48 +00:00
|
|
|
if ([object isKindOfClass:[NSNumber class]]) {
|
|
|
|
return @(RCTZeroIfNaN([object doubleValue]));
|
|
|
|
}
|
|
|
|
if ([object isKindOfClass:[NSString class]]) {
|
|
|
|
if ([object UTF8String] == NULL) {
|
|
|
|
return (id)kCFNull;
|
|
|
|
}
|
|
|
|
}
|
2015-05-06 07:11:31 +00:00
|
|
|
return object;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ([object isKindOfClass:[NSDictionary class]]) {
|
|
|
|
__block BOOL copy = NO;
|
2015-11-14 18:25:00 +00:00
|
|
|
NSMutableDictionary<NSString *, id> *values = [[NSMutableDictionary alloc] initWithCapacity:[object count]];
|
2015-06-15 14:53:45 +00:00
|
|
|
[object enumerateKeysAndObjectsUsingBlock:^(NSString *key, id item, __unused BOOL *stop) {
|
2015-05-06 07:11:31 +00:00
|
|
|
id value = RCTJSONClean(item);
|
|
|
|
values[key] = value;
|
|
|
|
copy |= value != item;
|
|
|
|
}];
|
|
|
|
return copy ? values : object;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ([object isKindOfClass:[NSArray class]]) {
|
|
|
|
__block BOOL copy = NO;
|
|
|
|
__block NSArray *values = object;
|
2015-06-15 14:53:45 +00:00
|
|
|
[object enumerateObjectsUsingBlock:^(id item, NSUInteger idx, __unused BOOL *stop) {
|
2015-05-06 07:11:31 +00:00
|
|
|
id value = RCTJSONClean(item);
|
|
|
|
if (copy) {
|
|
|
|
[(NSMutableArray *)values addObject:value];
|
|
|
|
} else if (value != item) {
|
|
|
|
// Converted value is different, so we'll need to copy the array
|
|
|
|
values = [[NSMutableArray alloc] initWithCapacity:values.count];
|
2015-06-15 14:53:45 +00:00
|
|
|
for (NSUInteger i = 0; i < idx; i++) {
|
2015-05-06 07:11:31 +00:00
|
|
|
[(NSMutableArray *)values addObject:object[i]];
|
|
|
|
}
|
|
|
|
[(NSMutableArray *)values addObject:value];
|
|
|
|
copy = YES;
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
return values;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (id)kCFNull;
|
|
|
|
}
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
NSString *RCTMD5Hash(NSString *string)
|
|
|
|
{
|
2015-08-24 10:14:33 +00:00
|
|
|
const char *str = string.UTF8String;
|
2015-02-20 04:10:52 +00:00
|
|
|
unsigned char result[CC_MD5_DIGEST_LENGTH];
|
|
|
|
CC_MD5(str, (CC_LONG)strlen(str), result);
|
|
|
|
|
|
|
|
return [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
|
2015-03-25 23:22:59 +00:00
|
|
|
result[0], result[1], result[2], result[3],
|
|
|
|
result[4], result[5], result[6], result[7],
|
|
|
|
result[8], result[9], result[10], result[11],
|
|
|
|
result[12], result[13], result[14], result[15]
|
|
|
|
];
|
2015-02-20 04:10:52 +00:00
|
|
|
}
|
|
|
|
|
2015-11-25 11:09:00 +00:00
|
|
|
void RCTExecuteOnMainThread(dispatch_block_t block, BOOL sync)
|
|
|
|
{
|
|
|
|
if ([NSThread isMainThread]) {
|
|
|
|
block();
|
|
|
|
} else if (sync) {
|
|
|
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
|
|
block();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
block();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
CGFloat RCTScreenScale()
|
|
|
|
{
|
|
|
|
static CGFloat scale;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
2015-11-25 11:09:00 +00:00
|
|
|
RCTExecuteOnMainThread(^{
|
2015-02-20 04:10:52 +00:00
|
|
|
scale = [UIScreen mainScreen].scale;
|
2015-11-25 11:09:00 +00:00
|
|
|
}, YES);
|
2015-02-20 04:10:52 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return scale;
|
|
|
|
}
|
|
|
|
|
|
|
|
CGSize RCTScreenSize()
|
|
|
|
{
|
|
|
|
static CGSize size;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
2015-11-25 11:09:00 +00:00
|
|
|
RCTExecuteOnMainThread(^{
|
2016-01-22 16:18:15 +00:00
|
|
|
size = [UIScreen mainScreen].bounds.size;
|
2015-11-25 11:09:00 +00:00
|
|
|
}, YES);
|
2015-02-20 04:10:52 +00:00
|
|
|
});
|
2015-03-09 23:58:39 +00:00
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
return size;
|
|
|
|
}
|
|
|
|
|
|
|
|
CGFloat RCTRoundPixelValue(CGFloat value)
|
|
|
|
{
|
|
|
|
CGFloat scale = RCTScreenScale();
|
|
|
|
return round(value * scale) / scale;
|
|
|
|
}
|
|
|
|
|
|
|
|
CGFloat RCTCeilPixelValue(CGFloat value)
|
|
|
|
{
|
|
|
|
CGFloat scale = RCTScreenScale();
|
|
|
|
return ceil(value * scale) / scale;
|
|
|
|
}
|
|
|
|
|
|
|
|
CGFloat RCTFloorPixelValue(CGFloat value)
|
|
|
|
{
|
|
|
|
CGFloat scale = RCTScreenScale();
|
|
|
|
return floor(value * scale) / scale;
|
|
|
|
}
|
|
|
|
|
2016-01-20 19:03:22 +00:00
|
|
|
CGSize RCTSizeInPixels(CGSize pointSize, CGFloat scale)
|
|
|
|
{
|
|
|
|
return (CGSize){
|
|
|
|
ceil(pointSize.width * scale),
|
|
|
|
ceil(pointSize.height * scale),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
void RCTSwapClassMethods(Class cls, SEL original, SEL replacement)
|
|
|
|
{
|
|
|
|
Method originalMethod = class_getClassMethod(cls, original);
|
|
|
|
IMP originalImplementation = method_getImplementation(originalMethod);
|
|
|
|
const char *originalArgTypes = method_getTypeEncoding(originalMethod);
|
2015-03-09 23:58:39 +00:00
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
Method replacementMethod = class_getClassMethod(cls, replacement);
|
|
|
|
IMP replacementImplementation = method_getImplementation(replacementMethod);
|
|
|
|
const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
|
2015-03-09 23:58:39 +00:00
|
|
|
|
2015-07-07 19:14:13 +00:00
|
|
|
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) {
|
2015-02-20 04:10:52 +00:00
|
|
|
class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes);
|
2015-07-07 19:14:13 +00:00
|
|
|
} else {
|
2015-02-20 04:10:52 +00:00
|
|
|
method_exchangeImplementations(originalMethod, replacementMethod);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void RCTSwapInstanceMethods(Class cls, SEL original, SEL replacement)
|
|
|
|
{
|
|
|
|
Method originalMethod = class_getInstanceMethod(cls, original);
|
|
|
|
IMP originalImplementation = method_getImplementation(originalMethod);
|
|
|
|
const char *originalArgTypes = method_getTypeEncoding(originalMethod);
|
2015-03-09 23:58:39 +00:00
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
Method replacementMethod = class_getInstanceMethod(cls, replacement);
|
|
|
|
IMP replacementImplementation = method_getImplementation(replacementMethod);
|
|
|
|
const char *replacementArgTypes = method_getTypeEncoding(replacementMethod);
|
2015-03-09 23:58:39 +00:00
|
|
|
|
2015-07-07 19:14:13 +00:00
|
|
|
if (class_addMethod(cls, original, replacementImplementation, replacementArgTypes)) {
|
2015-02-20 04:10:52 +00:00
|
|
|
class_replaceMethod(cls, replacement, originalImplementation, originalArgTypes);
|
2015-07-07 19:14:13 +00:00
|
|
|
} else {
|
2015-02-20 04:10:52 +00:00
|
|
|
method_exchangeImplementations(originalMethod, replacementMethod);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL RCTClassOverridesClassMethod(Class cls, SEL selector)
|
|
|
|
{
|
|
|
|
return RCTClassOverridesInstanceMethod(object_getClass(cls), selector);
|
|
|
|
}
|
|
|
|
|
|
|
|
BOOL RCTClassOverridesInstanceMethod(Class cls, SEL selector)
|
|
|
|
{
|
|
|
|
unsigned int numberOfMethods;
|
|
|
|
Method *methods = class_copyMethodList(cls, &numberOfMethods);
|
2015-07-07 19:14:13 +00:00
|
|
|
for (unsigned int i = 0; i < numberOfMethods; i++) {
|
|
|
|
if (method_getName(methods[i]) == selector) {
|
2015-02-20 04:10:52 +00:00
|
|
|
free(methods);
|
|
|
|
return YES;
|
|
|
|
}
|
|
|
|
}
|
2015-03-01 23:33:55 +00:00
|
|
|
free(methods);
|
2015-02-20 04:10:52 +00:00
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
NSDictionary<NSString *, id> *RCTMakeError(NSString *message,
|
|
|
|
id __nullable toStringify,
|
|
|
|
NSDictionary<NSString *, id> *__nullable extraData)
|
2015-03-11 19:06:03 +00:00
|
|
|
{
|
|
|
|
if (toStringify) {
|
2015-07-07 15:47:23 +00:00
|
|
|
message = [message stringByAppendingString:[toStringify description]];
|
2015-03-11 19:06:03 +00:00
|
|
|
}
|
2015-07-07 15:47:23 +00:00
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
NSMutableDictionary<NSString *, id> *error = [extraData mutableCopy] ?: [NSMutableDictionary new];
|
2015-07-07 15:47:23 +00:00
|
|
|
error[@"message"] = message;
|
2015-03-11 19:06:03 +00:00
|
|
|
return error;
|
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
NSDictionary<NSString *, id> *RCTMakeAndLogError(NSString *message,
|
|
|
|
id __nullable toStringify,
|
|
|
|
NSDictionary<NSString *, id> *__nullable extraData)
|
2015-03-11 19:06:03 +00:00
|
|
|
{
|
2015-11-14 18:25:00 +00:00
|
|
|
NSDictionary<NSString *, id> *error = RCTMakeError(message, toStringify, extraData);
|
2015-03-11 19:06:03 +00:00
|
|
|
RCTLogError(@"\nError: %@", error);
|
|
|
|
return error;
|
|
|
|
}
|
2015-04-27 10:50:07 +00:00
|
|
|
|
2015-11-14 18:25:00 +00:00
|
|
|
NSDictionary<NSString *, id> *RCTJSErrorFromNSError(NSError *error)
|
2016-01-19 20:19:47 +00:00
|
|
|
{
|
2016-03-11 10:54:15 +00:00
|
|
|
NSString *codeWithDomain = [NSString stringWithFormat:@"E%@%zd", error.domain.uppercaseString, error.code];
|
|
|
|
return RCTJSErrorFromCodeMessageAndNSError(codeWithDomain,
|
2016-01-21 15:49:45 +00:00
|
|
|
error.localizedDescription,
|
|
|
|
error);
|
2016-01-19 20:19:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: Can we just replace RCTMakeError with this function instead?
|
2016-01-21 15:49:45 +00:00
|
|
|
NSDictionary<NSString *, id> *RCTJSErrorFromCodeMessageAndNSError(NSString *code,
|
|
|
|
NSString *message,
|
|
|
|
NSError *__nullable error)
|
2015-07-13 17:33:39 +00:00
|
|
|
{
|
|
|
|
NSString *errorMessage;
|
2015-11-03 22:45:46 +00:00
|
|
|
NSArray<NSString *> *stackTrace = [NSThread callStackSymbols];
|
2015-11-14 18:25:00 +00:00
|
|
|
NSMutableDictionary<NSString *, id> *errorInfo =
|
2016-01-19 20:19:47 +00:00
|
|
|
[NSMutableDictionary dictionaryWithObject:stackTrace forKey:@"nativeStackIOS"];
|
2015-07-13 17:33:39 +00:00
|
|
|
|
|
|
|
if (error) {
|
|
|
|
errorMessage = error.localizedDescription ?: @"Unknown error from a native module";
|
|
|
|
errorInfo[@"domain"] = error.domain ?: RCTErrorDomain;
|
|
|
|
} else {
|
|
|
|
errorMessage = @"Unknown error from a native module";
|
|
|
|
errorInfo[@"domain"] = RCTErrorDomain;
|
|
|
|
}
|
2016-01-19 20:19:47 +00:00
|
|
|
errorInfo[@"code"] = code ?: RCTErrorUnspecified;
|
|
|
|
// Allow for explicit overriding of the error message
|
|
|
|
errorMessage = message ?: errorMessage;
|
2015-07-13 17:33:39 +00:00
|
|
|
|
|
|
|
return RCTMakeError(errorMessage, nil, errorInfo);
|
|
|
|
}
|
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
BOOL RCTRunningInTestEnvironment(void)
|
|
|
|
{
|
2015-07-06 18:19:02 +00:00
|
|
|
static BOOL isTestEnvironment = NO;
|
2015-04-27 10:50:07 +00:00
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
2015-07-06 18:19:02 +00:00
|
|
|
isTestEnvironment = NSClassFromString(@"SenTestCase") || NSClassFromString(@"XCTest");
|
2015-04-27 10:50:07 +00:00
|
|
|
});
|
2015-07-06 18:19:02 +00:00
|
|
|
return isTestEnvironment;
|
2015-04-27 10:50:07 +00:00
|
|
|
}
|
2015-05-22 14:17:08 +00:00
|
|
|
|
2015-09-22 17:43:56 +00:00
|
|
|
BOOL RCTRunningInAppExtension(void)
|
|
|
|
{
|
|
|
|
return [[[[NSBundle mainBundle] bundlePath] pathExtension] isEqualToString:@"appex"];
|
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
UIApplication *__nullable RCTSharedApplication(void)
|
2015-09-22 17:43:56 +00:00
|
|
|
{
|
|
|
|
if (RCTRunningInAppExtension()) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
return [[UIApplication class] performSelector:@selector(sharedApplication)];
|
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
UIWindow *__nullable RCTKeyWindow(void)
|
2015-11-05 11:49:41 +00:00
|
|
|
{
|
|
|
|
if (RCTRunningInAppExtension()) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: replace with a more robust solution
|
|
|
|
return RCTSharedApplication().keyWindow;
|
|
|
|
}
|
|
|
|
|
2016-01-27 17:04:14 +00:00
|
|
|
BOOL RCTForceTouchAvailable(void)
|
|
|
|
{
|
|
|
|
static BOOL forceSupported;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
forceSupported = [UITraitCollection class] &&
|
|
|
|
[UITraitCollection instancesRespondToSelector:@selector(forceTouchCapability)];
|
|
|
|
});
|
|
|
|
|
|
|
|
return forceSupported &&
|
|
|
|
(RCTKeyWindow() ?: [UIView new]).traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable;
|
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
UIAlertView *__nullable RCTAlertView(NSString *title,
|
|
|
|
NSString *__nullable message,
|
|
|
|
id __nullable delegate,
|
|
|
|
NSString *__nullable cancelButtonTitle,
|
|
|
|
NSArray<NSString *> *__nullable otherButtonTitles)
|
2015-09-22 17:43:56 +00:00
|
|
|
{
|
|
|
|
if (RCTRunningInAppExtension()) {
|
|
|
|
RCTLogError(@"RCTAlertView is unavailable when running in an app extension");
|
|
|
|
return nil;
|
|
|
|
}
|
2015-11-05 11:49:41 +00:00
|
|
|
|
2015-11-02 15:58:47 +00:00
|
|
|
UIAlertView *alertView = [UIAlertView new];
|
2015-09-22 17:43:56 +00:00
|
|
|
alertView.title = title;
|
|
|
|
alertView.message = message;
|
|
|
|
alertView.delegate = delegate;
|
|
|
|
if (cancelButtonTitle != nil) {
|
|
|
|
[alertView addButtonWithTitle:cancelButtonTitle];
|
|
|
|
alertView.cancelButtonIndex = 0;
|
|
|
|
}
|
2015-11-03 22:45:46 +00:00
|
|
|
for (NSString *buttonTitle in otherButtonTitles) {
|
2015-09-22 17:43:56 +00:00
|
|
|
[alertView addButtonWithTitle:buttonTitle];
|
|
|
|
}
|
|
|
|
return alertView;
|
|
|
|
}
|
|
|
|
|
2015-06-09 19:25:24 +00:00
|
|
|
NSError *RCTErrorWithMessage(NSString *message)
|
|
|
|
{
|
2015-11-14 18:25:00 +00:00
|
|
|
NSDictionary<NSString *, id> *errorInfo = @{NSLocalizedDescriptionKey: message};
|
2015-11-05 20:19:56 +00:00
|
|
|
return [[NSError alloc] initWithDomain:RCTErrorDomain code:0 userInfo:errorInfo];
|
2015-06-09 19:25:24 +00:00
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
id RCTNullIfNil(id __nullable value)
|
2015-06-09 13:46:49 +00:00
|
|
|
{
|
|
|
|
return value ?: (id)kCFNull;
|
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
id __nullable RCTNilIfNull(id __nullable value)
|
2015-06-09 13:46:49 +00:00
|
|
|
{
|
|
|
|
return value == (id)kCFNull ? nil : value;
|
|
|
|
}
|
[ReactNative] Move module info from bridge to RCTModuleData
Summary:
@public
The info about bridge modules (such as id, name, queue, methods...) was spread
across arrays & dictionaries on the bridge, move it into a specific class.
It also removes a lot of information that was statically cached, and now have
the same lifecycle of the bridge.
Also moved RCTModuleMethod, RCTFrameUpdate and RCTBatchedBridge into it's own
files, for organization sake.
NOTE: This diff seems huge, but most of it was just moving code :)
Test Plan:
Tested UIExplorer & UIExplorer tests, Catalyst, MAdMan and Groups. Everything
looks fine.
2015-06-24 23:34:56 +00:00
|
|
|
|
2016-01-27 17:04:14 +00:00
|
|
|
double RCTZeroIfNaN(double value)
|
2015-11-26 11:04:33 +00:00
|
|
|
{
|
|
|
|
return isnan(value) || isinf(value) ? 0 : value;
|
|
|
|
}
|
|
|
|
|
2015-07-13 17:33:39 +00:00
|
|
|
NSURL *RCTDataURL(NSString *mimeType, NSData *data)
|
[ReactNative] Move module info from bridge to RCTModuleData
Summary:
@public
The info about bridge modules (such as id, name, queue, methods...) was spread
across arrays & dictionaries on the bridge, move it into a specific class.
It also removes a lot of information that was statically cached, and now have
the same lifecycle of the bridge.
Also moved RCTModuleMethod, RCTFrameUpdate and RCTBatchedBridge into it's own
files, for organization sake.
NOTE: This diff seems huge, but most of it was just moving code :)
Test Plan:
Tested UIExplorer & UIExplorer tests, Catalyst, MAdMan and Groups. Everything
looks fine.
2015-06-24 23:34:56 +00:00
|
|
|
{
|
2015-07-13 17:33:39 +00:00
|
|
|
return [NSURL URLWithString:
|
|
|
|
[NSString stringWithFormat:@"data:%@;base64,%@", mimeType,
|
|
|
|
[data base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0]]];
|
[ReactNative] Move module info from bridge to RCTModuleData
Summary:
@public
The info about bridge modules (such as id, name, queue, methods...) was spread
across arrays & dictionaries on the bridge, move it into a specific class.
It also removes a lot of information that was statically cached, and now have
the same lifecycle of the bridge.
Also moved RCTModuleMethod, RCTFrameUpdate and RCTBatchedBridge into it's own
files, for organization sake.
NOTE: This diff seems huge, but most of it was just moving code :)
Test Plan:
Tested UIExplorer & UIExplorer tests, Catalyst, MAdMan and Groups. Everything
looks fine.
2015-06-24 23:34:56 +00:00
|
|
|
}
|
2015-07-13 17:33:39 +00:00
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
BOOL RCTIsGzippedData(NSData *__nullable); // exposed for unit testing purposes
|
|
|
|
BOOL RCTIsGzippedData(NSData *__nullable data)
|
2015-07-16 16:34:28 +00:00
|
|
|
{
|
|
|
|
UInt8 *bytes = (UInt8 *)data.bytes;
|
|
|
|
return (data.length >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b);
|
|
|
|
}
|
|
|
|
|
2016-01-22 16:39:44 +00:00
|
|
|
NSData *__nullable RCTGzipData(NSData *__nullable input, float level)
|
2015-07-16 16:34:28 +00:00
|
|
|
{
|
|
|
|
if (input.length == 0 || RCTIsGzippedData(input)) {
|
|
|
|
return input;
|
|
|
|
}
|
|
|
|
|
2015-07-27 08:15:53 +00:00
|
|
|
void *libz = dlopen("/usr/lib/libz.dylib", RTLD_LAZY);
|
2015-07-16 16:34:28 +00:00
|
|
|
int (*deflateInit2_)(z_streamp, int, int, int, int, int, const char *, int) = dlsym(libz, "deflateInit2_");
|
|
|
|
int (*deflate)(z_streamp, int) = dlsym(libz, "deflate");
|
|
|
|
int (*deflateEnd)(z_streamp) = dlsym(libz, "deflateEnd");
|
|
|
|
|
|
|
|
z_stream stream;
|
|
|
|
stream.zalloc = Z_NULL;
|
|
|
|
stream.zfree = Z_NULL;
|
|
|
|
stream.opaque = Z_NULL;
|
|
|
|
stream.avail_in = (uint)input.length;
|
|
|
|
stream.next_in = (Bytef *)input.bytes;
|
|
|
|
stream.total_out = 0;
|
|
|
|
stream.avail_out = 0;
|
|
|
|
|
|
|
|
static const NSUInteger RCTGZipChunkSize = 16384;
|
|
|
|
|
|
|
|
NSMutableData *output = nil;
|
|
|
|
int compression = (level < 0.0f)? Z_DEFAULT_COMPRESSION: (int)(roundf(level * 9));
|
|
|
|
if (deflateInit2(&stream, compression, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK) {
|
|
|
|
output = [NSMutableData dataWithLength:RCTGZipChunkSize];
|
|
|
|
while (stream.avail_out == 0) {
|
|
|
|
if (stream.total_out >= output.length) {
|
|
|
|
output.length += RCTGZipChunkSize;
|
|
|
|
}
|
|
|
|
stream.next_out = (uint8_t *)output.mutableBytes + stream.total_out;
|
|
|
|
stream.avail_out = (uInt)(output.length - stream.total_out);
|
|
|
|
deflate(&stream, Z_FINISH);
|
|
|
|
}
|
|
|
|
deflateEnd(&stream);
|
|
|
|
output.length = stream.total_out;
|
|
|
|
}
|
|
|
|
|
|
|
|
dlclose(libz);
|
|
|
|
|
|
|
|
return output;
|
|
|
|
}
|
2015-10-12 11:14:21 +00:00
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
NSString *__nullable RCTBundlePathForURL(NSURL *__nullable URL)
|
2015-10-12 11:14:21 +00:00
|
|
|
{
|
|
|
|
if (!URL.fileURL) {
|
|
|
|
// Not a file path
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
NSString *path = URL.path;
|
|
|
|
NSString *bundlePath = [[NSBundle mainBundle] resourcePath];
|
|
|
|
if (![path hasPrefix:bundlePath]) {
|
|
|
|
// Not a bundle-relative file
|
|
|
|
return nil;
|
|
|
|
}
|
2016-01-07 12:00:15 +00:00
|
|
|
path = [path substringFromIndex:bundlePath.length];
|
|
|
|
if ([path hasPrefix:@"/"]) {
|
|
|
|
path = [path substringFromIndex:1];
|
|
|
|
}
|
|
|
|
return path;
|
2015-10-12 11:14:21 +00:00
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
BOOL RCTIsXCAssetURL(NSURL *__nullable imageURL)
|
2015-10-12 11:14:21 +00:00
|
|
|
{
|
|
|
|
NSString *name = RCTBundlePathForURL(imageURL);
|
|
|
|
if (name.pathComponents.count != 1) {
|
|
|
|
// URL is invalid, or is a file path, not an XCAsset identifier
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
NSString *extension = [name pathExtension];
|
|
|
|
if (extension.length && ![extension isEqualToString:@"png"]) {
|
|
|
|
// Not a png
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
extension = extension.length ? nil : @"png";
|
|
|
|
if ([[NSBundle mainBundle] pathForResource:name ofType:extension]) {
|
|
|
|
// File actually exists in bundle, so is not an XCAsset
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
return YES;
|
|
|
|
}
|
2015-11-26 11:04:33 +00:00
|
|
|
|
2016-02-23 10:26:11 +00:00
|
|
|
RCT_EXTERN NSString *__nullable RCTTempFilePath(NSString *extension, NSError **error)
|
|
|
|
{
|
|
|
|
static NSError *setupError = nil;
|
|
|
|
static NSString *directory;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
directory = [NSTemporaryDirectory() stringByAppendingPathComponent:@"ReactNative"];
|
|
|
|
// If the temporary directory already exists, we'll delete it to ensure
|
|
|
|
// that temp files from the previous run have all been deleted. This is not
|
|
|
|
// a security measure, it simply prevents the temp directory from using too
|
|
|
|
// much space, as the circumstances under which iOS clears it automatically
|
|
|
|
// are not well-defined.
|
|
|
|
NSFileManager *fileManager = [NSFileManager new];
|
|
|
|
if ([fileManager fileExistsAtPath:directory]) {
|
|
|
|
[fileManager removeItemAtPath:directory error:NULL];
|
|
|
|
}
|
|
|
|
if (![fileManager fileExistsAtPath:directory]) {
|
|
|
|
NSError *localError = nil;
|
|
|
|
if (![fileManager createDirectoryAtPath:directory
|
|
|
|
withIntermediateDirectories:YES
|
|
|
|
attributes:nil
|
|
|
|
error:&localError]) {
|
|
|
|
// This is bad
|
|
|
|
RCTLogError(@"Failed to create temporary directory: %@", localError);
|
|
|
|
setupError = localError;
|
|
|
|
directory = nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!directory || setupError) {
|
|
|
|
if (error) {
|
|
|
|
*error = setupError;
|
|
|
|
}
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Append a unique filename
|
|
|
|
NSString *filename = [NSUUID new].UUIDString;
|
|
|
|
if (extension) {
|
|
|
|
filename = [filename stringByAppendingPathExtension:extension];
|
|
|
|
}
|
|
|
|
return [directory stringByAppendingPathComponent:filename];
|
|
|
|
}
|
|
|
|
|
2015-11-26 11:04:33 +00:00
|
|
|
static void RCTGetRGBAColorComponents(CGColorRef color, CGFloat rgba[4])
|
|
|
|
{
|
|
|
|
CGColorSpaceModel model = CGColorSpaceGetModel(CGColorGetColorSpace(color));
|
|
|
|
const CGFloat *components = CGColorGetComponents(color);
|
|
|
|
switch (model)
|
|
|
|
{
|
|
|
|
case kCGColorSpaceModelMonochrome:
|
|
|
|
{
|
|
|
|
rgba[0] = components[0];
|
|
|
|
rgba[1] = components[0];
|
|
|
|
rgba[2] = components[0];
|
|
|
|
rgba[3] = components[1];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case kCGColorSpaceModelRGB:
|
|
|
|
{
|
|
|
|
rgba[0] = components[0];
|
|
|
|
rgba[1] = components[1];
|
|
|
|
rgba[2] = components[2];
|
|
|
|
rgba[3] = components[3];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case kCGColorSpaceModelCMYK:
|
|
|
|
case kCGColorSpaceModelDeviceN:
|
|
|
|
case kCGColorSpaceModelIndexed:
|
|
|
|
case kCGColorSpaceModelLab:
|
|
|
|
case kCGColorSpaceModelPattern:
|
|
|
|
case kCGColorSpaceModelUnknown:
|
|
|
|
{
|
|
|
|
|
|
|
|
#ifdef RCT_DEBUG
|
|
|
|
//unsupported format
|
|
|
|
RCTLogError(@"Unsupported color model: %i", model);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
rgba[0] = 0.0;
|
|
|
|
rgba[1] = 0.0;
|
|
|
|
rgba[2] = 0.0;
|
|
|
|
rgba[3] = 1.0;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
NSString *RCTColorToHexString(CGColorRef color)
|
|
|
|
{
|
|
|
|
CGFloat rgba[4];
|
|
|
|
RCTGetRGBAColorComponents(color, rgba);
|
|
|
|
uint8_t r = rgba[0]*255;
|
|
|
|
uint8_t g = rgba[1]*255;
|
|
|
|
uint8_t b = rgba[2]*255;
|
|
|
|
uint8_t a = rgba[3]*255;
|
|
|
|
if (a < 255) {
|
|
|
|
return [NSString stringWithFormat:@"#%02x%02x%02x%02x", r, g, b, a];
|
|
|
|
} else {
|
|
|
|
return [NSString stringWithFormat:@"#%02x%02x%02x", r, g, b];
|
|
|
|
}
|
|
|
|
}
|
2015-12-01 02:44:06 +00:00
|
|
|
|
|
|
|
// (https://github.com/0xced/XCDFormInputAccessoryView/blob/master/XCDFormInputAccessoryView/XCDFormInputAccessoryView.m#L10-L14)
|
2016-01-04 18:39:07 +00:00
|
|
|
NSString *RCTUIKitLocalizedString(NSString *string)
|
2015-12-01 02:44:06 +00:00
|
|
|
{
|
|
|
|
NSBundle *UIKitBundle = [NSBundle bundleForClass:[UIApplication class]];
|
|
|
|
return UIKitBundle ? [UIKitBundle localizedStringForKey:string value:string table:nil] : string;
|
|
|
|
}
|
2016-01-04 18:39:07 +00:00
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
NSString *__nullable RCTGetURLQueryParam(NSURL *__nullable URL, NSString *param)
|
2016-01-04 18:39:07 +00:00
|
|
|
{
|
|
|
|
RCTAssertParam(param);
|
|
|
|
if (!URL) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
NSURLComponents *components = [NSURLComponents componentsWithURL:URL
|
|
|
|
resolvingAgainstBaseURL:YES];
|
2016-01-05 20:50:25 +00:00
|
|
|
|
|
|
|
// TODO: use NSURLComponents.queryItems once we drop support for iOS 7
|
|
|
|
for (NSString *item in [components.percentEncodedQuery componentsSeparatedByString:@"&"].reverseObjectEnumerator) {
|
|
|
|
NSArray *keyValue = [item componentsSeparatedByString:@"="];
|
|
|
|
NSString *key = [keyValue.firstObject stringByRemovingPercentEncoding];
|
|
|
|
if ([key isEqualToString:param]) {
|
|
|
|
return [keyValue.lastObject stringByRemovingPercentEncoding];
|
2016-01-04 18:39:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
|
2016-01-21 15:49:45 +00:00
|
|
|
NSURL *__nullable RCTURLByReplacingQueryParam(NSURL *__nullable URL, NSString *param, NSString *__nullable value)
|
2016-01-04 18:39:07 +00:00
|
|
|
{
|
|
|
|
RCTAssertParam(param);
|
|
|
|
if (!URL) {
|
|
|
|
return nil;
|
|
|
|
}
|
|
|
|
NSURLComponents *components = [NSURLComponents componentsWithURL:URL
|
|
|
|
resolvingAgainstBaseURL:YES];
|
2016-01-05 20:50:25 +00:00
|
|
|
|
|
|
|
// TODO: use NSURLComponents.queryItems once we drop support for iOS 7
|
|
|
|
|
|
|
|
// Unhelpfully, iOS doesn't provide this set as a constant
|
|
|
|
static NSCharacterSet *URLParamCharacterSet;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
|
|
NSMutableCharacterSet *characterSet = [NSMutableCharacterSet new];
|
|
|
|
[characterSet formUnionWithCharacterSet:[NSCharacterSet URLQueryAllowedCharacterSet]];
|
|
|
|
[characterSet removeCharactersInString:@"&=?"];
|
|
|
|
URLParamCharacterSet = [characterSet copy];
|
|
|
|
});
|
|
|
|
|
|
|
|
NSString *encodedParam =
|
|
|
|
[param stringByAddingPercentEncodingWithAllowedCharacters:URLParamCharacterSet];
|
|
|
|
|
2016-01-04 18:39:07 +00:00
|
|
|
__block NSInteger paramIndex = NSNotFound;
|
2016-01-05 20:50:25 +00:00
|
|
|
NSMutableArray *queryItems = [[components.percentEncodedQuery componentsSeparatedByString:@"&"] mutableCopy];
|
2016-01-04 18:39:07 +00:00
|
|
|
[queryItems enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:
|
2016-01-05 20:50:25 +00:00
|
|
|
^(NSString *item, NSUInteger i, BOOL *stop) {
|
|
|
|
NSArray *keyValue = [item componentsSeparatedByString:@"="];
|
|
|
|
if ([keyValue.firstObject isEqualToString:encodedParam]) {
|
2016-01-04 18:39:07 +00:00
|
|
|
paramIndex = i;
|
|
|
|
*stop = YES;
|
|
|
|
}
|
|
|
|
}];
|
|
|
|
|
2016-01-06 19:54:25 +00:00
|
|
|
if (!value) {
|
|
|
|
if (paramIndex != NSNotFound) {
|
|
|
|
[queryItems removeObjectAtIndex:paramIndex];
|
|
|
|
}
|
2016-01-04 18:39:07 +00:00
|
|
|
} else {
|
2016-01-06 19:54:25 +00:00
|
|
|
NSString *encodedValue =
|
|
|
|
[value stringByAddingPercentEncodingWithAllowedCharacters:URLParamCharacterSet];
|
|
|
|
|
|
|
|
NSString *newItem = [encodedParam stringByAppendingFormat:@"=%@", encodedValue];
|
|
|
|
if (paramIndex == NSNotFound) {
|
|
|
|
[queryItems addObject:newItem];
|
|
|
|
} else {
|
|
|
|
[queryItems replaceObjectAtIndex:paramIndex withObject:newItem];
|
|
|
|
}
|
2016-01-04 18:39:07 +00:00
|
|
|
}
|
2016-01-05 20:50:25 +00:00
|
|
|
components.percentEncodedQuery = [queryItems componentsJoinedByString:@"&"];
|
2016-01-04 18:39:07 +00:00
|
|
|
return components.URL;
|
|
|
|
}
|