mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
452 lines
20 KiB
Objective-C
452 lines
20 KiB
Objective-C
/*
|
|
* Copyright 2017 Google
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import <TargetConditionals.h>
|
|
#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
|
|
|
|
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
|
|
|
|
#import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
|
|
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMFetchResponseParser.h"
|
|
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentData.h"
|
|
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentDataWithImageURL.h"
|
|
#import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageDefinition.h"
|
|
#import "FirebaseInAppMessaging/Sources/Private/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h"
|
|
#import "FirebaseInAppMessaging/Sources/Private/Util/FIRIAMTimeFetcher.h"
|
|
#import "FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.h"
|
|
|
|
#import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
|
|
|
|
@interface FIRIAMFetchResponseParser ()
|
|
@property(nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
|
|
@end
|
|
|
|
// Returns `value` if it is an NSString, nil otherwise. Guards against server values of an
|
|
// unexpected type being stored as strings and crashing later outside of the parser.
|
|
static NSString *_Nullable FIRIAMStringOrNil(id _Nullable value) {
|
|
return [value isKindOfClass:[NSString class]] ? value : nil;
|
|
}
|
|
|
|
// Returns `value` if it is an NSDictionary, nil otherwise. Used to safely walk nested nodes so a
|
|
// wrong-typed intermediate node (e.g. NSNull or NSString) drops just that field rather than raising
|
|
// an unrecognized selector exception that discards the whole message.
|
|
static NSDictionary *_Nullable FIRIAMDictionaryOrNil(id _Nullable value) {
|
|
return [value isKindOfClass:[NSDictionary class]] ? value : nil;
|
|
}
|
|
|
|
@implementation FIRIAMFetchResponseParser
|
|
|
|
- (instancetype)initWithTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher {
|
|
if (self = [super init]) {
|
|
_timeFetcher = timeFetcher;
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (nullable NSArray<FIRIAMMessageDefinition *> *)
|
|
parseAPIResponseDictionary:(NSDictionary *)responseDict
|
|
discardedMsgCount:(NSInteger *)discardCount
|
|
fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime {
|
|
if (fetchWaitTime != nil) {
|
|
*fetchWaitTime = nil; // It would be set to non nil value if it's detected in responseDict
|
|
}
|
|
|
|
if (![responseDict isKindOfClass:[NSDictionary class]]) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900019", @"API response is not a dictionary: %@",
|
|
responseDict);
|
|
return nil;
|
|
}
|
|
|
|
if (fetchWaitTime != nil) {
|
|
if ([responseDict[@"expirationEpochTimestampMillis"] isKindOfClass:NSString.class]) {
|
|
NSTimeInterval nextFetchTimeInResponse =
|
|
[responseDict[@"expirationEpochTimestampMillis"] doubleValue] / 1000;
|
|
NSTimeInterval fetchWaitTimeInSeconds =
|
|
nextFetchTimeInResponse - [self.timeFetcher currentTimestampInSeconds];
|
|
|
|
FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900005",
|
|
@"Detected next fetch epoch time in API response as %f seconds and wait for %f "
|
|
"seconds before next fetch.",
|
|
nextFetchTimeInResponse, fetchWaitTimeInSeconds);
|
|
|
|
if (fetchWaitTimeInSeconds > 0.01) {
|
|
*fetchWaitTime = @(fetchWaitTimeInSeconds);
|
|
FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900018",
|
|
@"Fetch wait time calculated from server response is negative. Discard it.");
|
|
}
|
|
} else {
|
|
FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900014",
|
|
@"No fetch epoch time detected in API response.");
|
|
}
|
|
}
|
|
|
|
NSArray<NSDictionary *> *messageArray = responseDict[@"messages"];
|
|
if (messageArray && ![messageArray isKindOfClass:[NSArray class]]) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900020",
|
|
@"messages node in API response is not an array: %@", messageArray);
|
|
return nil;
|
|
}
|
|
NSInteger discarded = 0;
|
|
|
|
NSMutableArray<FIRIAMMessageDefinition *> *definitions = [[NSMutableArray alloc] init];
|
|
for (NSDictionary *nextMsg in messageArray) {
|
|
FIRIAMMessageDefinition *nextDefinition =
|
|
[nextMsg isKindOfClass:[NSDictionary class]]
|
|
? [self convertToMessageDefinitionWithMessageDict:nextMsg]
|
|
: nil;
|
|
if (nextDefinition) {
|
|
[definitions addObject:nextDefinition];
|
|
} else {
|
|
FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM900001",
|
|
@"No definition generated for message node %@", nextMsg);
|
|
discarded++;
|
|
}
|
|
}
|
|
FIRLogDebug(
|
|
kFIRLoggerInAppMessaging, @"I-IAM900002",
|
|
@"%lu message definitions were parsed out successfully and %lu messages are discarded",
|
|
(unsigned long)definitions.count, (unsigned long)discarded);
|
|
|
|
if (discardCount) {
|
|
*discardCount = discarded;
|
|
}
|
|
return [definitions copy];
|
|
}
|
|
|
|
// Return nil if no valid triggering condition can be detected
|
|
- (NSArray<FIRIAMDisplayTriggerDefinition *> *)parseTriggeringCondition:
|
|
(NSArray<NSDictionary *> *)triggerConditions {
|
|
if (![triggerConditions isKindOfClass:[NSArray class]] || triggerConditions.count == 0) {
|
|
return nil;
|
|
}
|
|
|
|
NSMutableArray<FIRIAMDisplayTriggerDefinition *> *triggers = [[NSMutableArray alloc] init];
|
|
|
|
for (NSDictionary *nextTriggerCondition in triggerConditions) {
|
|
if (![nextTriggerCondition isKindOfClass:[NSDictionary class]]) {
|
|
continue;
|
|
}
|
|
// Handle app_launch and on_foreground cases.
|
|
if (nextTriggerCondition[@"fiamTrigger"]) {
|
|
NSString *fiamTrigger = FIRIAMStringOrNil(nextTriggerCondition[@"fiamTrigger"]);
|
|
if ([fiamTrigger isEqualToString:@"ON_FOREGROUND"]) {
|
|
[triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]];
|
|
} else if ([fiamTrigger isEqualToString:@"APP_LAUNCH"]) {
|
|
[triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppLaunchTrigger]];
|
|
}
|
|
} else if ([nextTriggerCondition[@"event"] isKindOfClass:[NSDictionary class]]) {
|
|
NSDictionary *triggeringEvent = (NSDictionary *)nextTriggerCondition[@"event"];
|
|
NSString *eventName = FIRIAMStringOrNil(triggeringEvent[@"name"]);
|
|
if (eventName.length > 0) {
|
|
[triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc]
|
|
initWithFirebaseAnalyticEvent:eventName]];
|
|
}
|
|
}
|
|
}
|
|
|
|
return [triggers copy];
|
|
}
|
|
|
|
// For one element in the restful API response's messages array, convert into
|
|
// a FIRIAMMessageDefinition object. If the conversion fails, a nil is returned.
|
|
- (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictionary *)messageNode {
|
|
@try {
|
|
BOOL isTestMessage = NO;
|
|
|
|
id isTestCampaignNode = messageNode[@"isTestCampaign"];
|
|
if ([isTestCampaignNode isKindOfClass:[NSNumber class]]) {
|
|
isTestMessage = [isTestCampaignNode boolValue];
|
|
}
|
|
|
|
id payloadNode = messageNode[@"experimentalPayload"] ?: messageNode[@"vanillaPayload"];
|
|
|
|
if (![payloadNode isKindOfClass:[NSDictionary class]]) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900012",
|
|
@"Message payload does not exist or does not represent a dictionary in "
|
|
"message node %@",
|
|
messageNode);
|
|
return nil;
|
|
}
|
|
|
|
NSString *messageID = FIRIAMStringOrNil(payloadNode[@"campaignId"]);
|
|
if (messageID.length == 0) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900010",
|
|
@"message id is missing in message node %@", messageNode);
|
|
return nil;
|
|
}
|
|
|
|
NSString *messageName = FIRIAMStringOrNil(payloadNode[@"campaignName"]);
|
|
if (!messageName && !isTestMessage) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900011",
|
|
@"campaign name is missing in non-test message node %@", messageNode);
|
|
return nil;
|
|
}
|
|
|
|
ABTExperimentPayload *experimentPayload = nil;
|
|
NSDictionary *experimentPayloadDictionary = payloadNode[@"experimentPayload"];
|
|
|
|
if ([experimentPayloadDictionary isKindOfClass:[NSDictionary class]]) {
|
|
experimentPayload =
|
|
[[ABTExperimentPayload alloc] initWithDictionary:experimentPayloadDictionary];
|
|
}
|
|
|
|
NSTimeInterval startTimeInSeconds = 0;
|
|
NSTimeInterval endTimeInSeconds = 0;
|
|
if (!isTestMessage) {
|
|
// Parsing start/end times out of non-test messages. They are strings in the
|
|
// json response.
|
|
id startTimeNode = payloadNode[@"campaignStartTimeMillis"];
|
|
if ([startTimeNode isKindOfClass:[NSString class]]) {
|
|
startTimeInSeconds = [startTimeNode doubleValue] / 1000.0;
|
|
}
|
|
|
|
id endTimeNode = payloadNode[@"campaignEndTimeMillis"];
|
|
if ([endTimeNode isKindOfClass:[NSString class]]) {
|
|
endTimeInSeconds = [endTimeNode doubleValue] / 1000.0;
|
|
}
|
|
}
|
|
|
|
id contentNode = messageNode[@"content"];
|
|
if (![contentNode isKindOfClass:[NSDictionary class]]) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900013",
|
|
@"content node does not exist or does not represent a dictionary in "
|
|
"message node %@",
|
|
messageNode);
|
|
return nil;
|
|
}
|
|
|
|
NSDictionary *content = (NSDictionary *)contentNode;
|
|
FIRIAMRenderingMode mode;
|
|
UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *secondaryBtnTxtColor,
|
|
*titleTextColor;
|
|
viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil;
|
|
|
|
NSString *title, *body, *imageURLStr, *landscapeImageURLStr, *actionURLStr,
|
|
*secondaryActionURLStr, *actionButtonText, *secondaryActionButtonText;
|
|
title = body = imageURLStr = landscapeImageURLStr = actionButtonText =
|
|
secondaryActionButtonText = actionURLStr = secondaryActionURLStr = nil;
|
|
|
|
// TODO: Refactor this giant if-else block into separate parsing methods per message type.
|
|
if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) {
|
|
NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"];
|
|
mode = FIRIAMRenderAsBannerView;
|
|
|
|
NSDictionary *titleNode = FIRIAMDictionaryOrNil(bannerNode[@"title"]);
|
|
title = FIRIAMStringOrNil(titleNode[@"text"]);
|
|
titleTextColor = [UIColor firiam_colorWithHexString:titleNode[@"hexColor"]];
|
|
|
|
NSDictionary *bodyNode = FIRIAMDictionaryOrNil(bannerNode[@"body"]);
|
|
body = FIRIAMStringOrNil(bodyNode[@"text"]);
|
|
|
|
imageURLStr = FIRIAMStringOrNil(bannerNode[@"imageUrl"]);
|
|
NSDictionary *actionNode = FIRIAMDictionaryOrNil(bannerNode[@"action"]);
|
|
actionURLStr = FIRIAMStringOrNil(actionNode[@"actionUrl"]);
|
|
viewCardBackgroundColor =
|
|
[UIColor firiam_colorWithHexString:bannerNode[@"backgroundHexColor"]];
|
|
|
|
} else if ([content[@"modal"] isKindOfClass:[NSDictionary class]]) {
|
|
mode = FIRIAMRenderAsModalView;
|
|
|
|
NSDictionary *modalNode = (NSDictionary *)contentNode[@"modal"];
|
|
NSDictionary *titleNode = FIRIAMDictionaryOrNil(modalNode[@"title"]);
|
|
title = FIRIAMStringOrNil(titleNode[@"text"]);
|
|
titleTextColor = [UIColor firiam_colorWithHexString:titleNode[@"hexColor"]];
|
|
|
|
NSDictionary *bodyNode = FIRIAMDictionaryOrNil(modalNode[@"body"]);
|
|
body = FIRIAMStringOrNil(bodyNode[@"text"]);
|
|
|
|
imageURLStr = FIRIAMStringOrNil(modalNode[@"imageUrl"]);
|
|
NSDictionary *actionButtonNode = FIRIAMDictionaryOrNil(modalNode[@"actionButton"]);
|
|
NSDictionary *actionButtonTextNode = FIRIAMDictionaryOrNil(actionButtonNode[@"text"]);
|
|
actionButtonText = FIRIAMStringOrNil(actionButtonTextNode[@"text"]);
|
|
btnTxtColor = [UIColor firiam_colorWithHexString:actionButtonTextNode[@"hexColor"]];
|
|
btnBgColor = [UIColor firiam_colorWithHexString:actionButtonNode[@"buttonHexColor"]];
|
|
|
|
NSDictionary *actionNode = FIRIAMDictionaryOrNil(modalNode[@"action"]);
|
|
actionURLStr = FIRIAMStringOrNil(actionNode[@"actionUrl"]);
|
|
viewCardBackgroundColor =
|
|
[UIColor firiam_colorWithHexString:modalNode[@"backgroundHexColor"]];
|
|
} else if ([content[@"imageOnly"] isKindOfClass:[NSDictionary class]]) {
|
|
mode = FIRIAMRenderAsImageOnlyView;
|
|
NSDictionary *imageOnlyNode = (NSDictionary *)contentNode[@"imageOnly"];
|
|
|
|
imageURLStr = FIRIAMStringOrNil(imageOnlyNode[@"imageUrl"]);
|
|
|
|
if (!imageURLStr) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900007",
|
|
@"Image url is missing for image-only message %@", messageNode);
|
|
return nil;
|
|
}
|
|
NSDictionary *actionNode = FIRIAMDictionaryOrNil(imageOnlyNode[@"action"]);
|
|
actionURLStr = FIRIAMStringOrNil(actionNode[@"actionUrl"]);
|
|
} else if ([content[@"card"] isKindOfClass:[NSDictionary class]]) {
|
|
mode = FIRIAMRenderAsCardView;
|
|
NSDictionary *cardNode = (NSDictionary *)contentNode[@"card"];
|
|
NSDictionary *titleNode = FIRIAMDictionaryOrNil(cardNode[@"title"]);
|
|
title = FIRIAMStringOrNil(titleNode[@"text"]);
|
|
titleTextColor = [UIColor firiam_colorWithHexString:titleNode[@"hexColor"]];
|
|
|
|
NSDictionary *bodyNode = FIRIAMDictionaryOrNil(cardNode[@"body"]);
|
|
body = FIRIAMStringOrNil(bodyNode[@"text"]);
|
|
|
|
imageURLStr = FIRIAMStringOrNil(cardNode[@"portraitImageUrl"]);
|
|
landscapeImageURLStr = FIRIAMStringOrNil(cardNode[@"landscapeImageUrl"]);
|
|
|
|
viewCardBackgroundColor = [UIColor firiam_colorWithHexString:cardNode[@"backgroundHexColor"]];
|
|
|
|
NSDictionary *primaryButtonNode = FIRIAMDictionaryOrNil(cardNode[@"primaryActionButton"]);
|
|
NSDictionary *primaryButtonTextNode = FIRIAMDictionaryOrNil(primaryButtonNode[@"text"]);
|
|
actionButtonText = FIRIAMStringOrNil(primaryButtonTextNode[@"text"]);
|
|
btnTxtColor = [UIColor firiam_colorWithHexString:primaryButtonTextNode[@"hexColor"]];
|
|
|
|
NSDictionary *secondaryButtonNode = FIRIAMDictionaryOrNil(cardNode[@"secondaryActionButton"]);
|
|
NSDictionary *secondaryButtonTextNode = FIRIAMDictionaryOrNil(secondaryButtonNode[@"text"]);
|
|
secondaryActionButtonText = FIRIAMStringOrNil(secondaryButtonTextNode[@"text"]);
|
|
secondaryBtnTxtColor =
|
|
[UIColor firiam_colorWithHexString:secondaryButtonTextNode[@"hexColor"]];
|
|
|
|
NSDictionary *primaryActionNode = FIRIAMDictionaryOrNil(cardNode[@"primaryAction"]);
|
|
actionURLStr = FIRIAMStringOrNil(primaryActionNode[@"actionUrl"]);
|
|
NSDictionary *secondaryActionNode = FIRIAMDictionaryOrNil(cardNode[@"secondaryAction"]);
|
|
secondaryActionURLStr = FIRIAMStringOrNil(secondaryActionNode[@"actionUrl"]);
|
|
|
|
} else {
|
|
// Unknown message type
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003",
|
|
@"Unknown message type in message node %@", messageNode);
|
|
return nil;
|
|
}
|
|
|
|
if (title == nil && mode != FIRIAMRenderAsImageOnlyView) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900004",
|
|
@"Title text is missing in message node %@", messageNode);
|
|
return nil;
|
|
}
|
|
|
|
NSURL *imageURL = [self imageURLFromURLString:imageURLStr];
|
|
NSURL *landscapeImageURL = [self imageURLFromURLString:landscapeImageURLStr];
|
|
NSURL *actionURL = [self urlFromURLString:actionURLStr];
|
|
NSURL *secondaryActionURL = [self urlFromURLString:secondaryActionURLStr];
|
|
FIRIAMRenderingEffectSetting *renderEffect =
|
|
[FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
|
|
renderEffect.viewMode = mode;
|
|
|
|
if (viewCardBackgroundColor) {
|
|
renderEffect.displayBGColor = viewCardBackgroundColor;
|
|
}
|
|
|
|
if (btnBgColor) {
|
|
renderEffect.btnBGColor = btnBgColor;
|
|
}
|
|
|
|
if (btnTxtColor) {
|
|
renderEffect.btnTextColor = btnTxtColor;
|
|
}
|
|
|
|
if (secondaryBtnTxtColor) {
|
|
renderEffect.secondaryActionBtnTextColor = secondaryBtnTxtColor;
|
|
}
|
|
|
|
if (titleTextColor) {
|
|
renderEffect.textColor = titleTextColor;
|
|
}
|
|
|
|
NSArray<FIRIAMDisplayTriggerDefinition *> *triggersDefinition =
|
|
[self parseTriggeringCondition:messageNode[@"triggeringConditions"]];
|
|
|
|
if (isTestMessage) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900008",
|
|
@"A test message with id %@ was parsed successfully.", messageID);
|
|
renderEffect.isTestMessage = YES;
|
|
} else {
|
|
// Triggering definitions should always be present for a non-test message.
|
|
if (!triggersDefinition || triggersDefinition.count == 0) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900009",
|
|
@"No valid triggering condition is detected in message definition"
|
|
" with id %@",
|
|
messageID);
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
FIRIAMMessageContentDataWithImageURL *msgData =
|
|
[[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title
|
|
messageBody:body
|
|
actionButtonText:actionButtonText
|
|
secondaryActionButtonText:secondaryActionButtonText
|
|
actionURL:actionURL
|
|
secondaryActionURL:secondaryActionURL
|
|
imageURL:imageURL
|
|
landscapeImageURL:landscapeImageURL
|
|
usingURLSession:nil];
|
|
|
|
FIRIAMMessageRenderData *renderData =
|
|
[[FIRIAMMessageRenderData alloc] initWithMessageID:messageID
|
|
messageName:messageName
|
|
contentData:msgData
|
|
renderingEffect:renderEffect];
|
|
NSDictionary *dataBundle = nil;
|
|
id dataBundleNode = messageNode[@"dataBundle"];
|
|
if ([dataBundleNode isKindOfClass:[NSDictionary class]]) {
|
|
dataBundle = dataBundleNode;
|
|
}
|
|
if (isTestMessage) {
|
|
return [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:renderData
|
|
appData:dataBundle
|
|
experimentPayload:experimentPayload];
|
|
} else {
|
|
return [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData
|
|
startTime:startTimeInSeconds
|
|
endTime:endTimeInSeconds
|
|
triggerDefinition:triggersDefinition
|
|
appData:dataBundle
|
|
experimentPayload:experimentPayload
|
|
isTestMessage:NO];
|
|
}
|
|
} @catch (NSException *e) {
|
|
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900006",
|
|
@"Error in parsing message node %@ "
|
|
"with error %@",
|
|
messageNode, e);
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (nullable NSURL *)imageURLFromURLString:(NSString *)string {
|
|
NSURL *url = [self urlFromURLString:string];
|
|
|
|
// Image URLs must be valid HTTPS links, according to the Firebase Console.
|
|
if (![url.scheme.lowercaseString isEqualToString:@"https"]) return nil;
|
|
|
|
return url;
|
|
}
|
|
|
|
- (nullable NSURL *)urlFromURLString:(NSString *)string {
|
|
NSString *sanitizedString = [self sanitizedURLStringFromString:string];
|
|
|
|
if (sanitizedString.length == 0) return nil;
|
|
|
|
return [NSURL URLWithString:sanitizedString];
|
|
}
|
|
|
|
- (NSString *)sanitizedURLStringFromString:(NSString *)string {
|
|
return [string stringByReplacingOccurrencesOfString:@" " withString:@""];
|
|
}
|
|
|
|
@end
|
|
|
|
#endif // TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
|