66// Copyright © 2015 Branch Metrics. All rights reserved.
77//
88
9+ #import " BNCPreferenceHelper.h"
910#import " BNCContentDiscoveryManager.h"
1011#import " BNCSystemObserver.h"
1112#import " BNCError.h"
2930
3031@interface BNCContentDiscoveryManager ()
3132
32- @property (strong , nonatomic ) NSUserActivity *currentUserActivity ;
33+ @property (strong , nonatomic ) NSMutableDictionary *userInfo ;
3334
3435@end
3536
@@ -39,18 +40,14 @@ @implementation BNCContentDiscoveryManager
3940
4041- (NSString *)spotlightIdentifierFromActivity : (NSUserActivity *)userActivity {
4142#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000
42- if ([userActivity.activityType hasPrefix: BRANCH_SPOTLIGHT_PREFIX]) {
43- return userActivity.activityType ;
44- }
43+ // If it has our prefix, then the link identifier is just the last piece of the identifier.
44+ NSString *activityIdentifier = userActivity.userInfo [CSSearchableItemActivityIdentifier] ;
45+ BOOL isBranchIdentifier = [activityIdentifier hasPrefix: BRANCH_SPOTLIGHT_PREFIX];
4546
46- // CoreSpotlight version. Matched if it has our prefix, then the link identifier is just the last piece of the identifier.
47- if ([userActivity.activityType isEqualToString: CSSearchableItemActionType]) {
48- NSString *activityIdentifier = userActivity.userInfo [CSSearchableItemActivityIdentifier];
49- BOOL isBranchIdentifier = [activityIdentifier hasPrefix: BRANCH_SPOTLIGHT_PREFIX];
50-
51- if (isBranchIdentifier) {
52- return activityIdentifier;
53- }
47+ // Checking for CSSearchableItemActionType in the activity for legacy spotlight indexing (pre 0.12.7)
48+ // Now we index NSUserActivies with type set to io.branch. + bundleId for better SEO
49+ if ([userActivity.activityType isEqualToString: CSSearchableItemActionType] || isBranchIdentifier) {
50+ return activityIdentifier;
5451 }
5552#endif
5653
@@ -59,16 +56,14 @@ - (NSString *)spotlightIdentifierFromActivity:(NSUserActivity *)userActivity {
5956
6057- (NSString *)standardSpotlightIdentifierFromActivity : (NSUserActivity *)userActivity {
6158#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 90000
62- // CoreSpotlight version. Matched if it has our prefix, then the link identifier is just the last piece of the identifier.
63- if ([userActivity.activityType isEqualToString: CSSearchableItemActionType] && userActivity.userInfo [CSSearchableItemActivityIdentifier]) {
59+ if (userActivity.userInfo [CSSearchableItemActivityIdentifier]) {
6460 return userActivity.userInfo [CSSearchableItemActivityIdentifier];
6561 }
6662#endif
6763
6864 return nil ;
6965}
7066
71-
7267#pragma mark - Content Indexing
7368
7469- (void )indexContentWithTitle : (NSString *)title
@@ -309,7 +304,7 @@ - (void)indexContentWithTitle:(NSString *)title
309304 if ([BNCSystemObserver getOSVersion ].integerValue < 9 ) {
310305 NSError *error = [NSError errorWithDomain: BNCErrorDomain code: BNCVersionError userInfo: @{ NSLocalizedDescriptionKey : @" Cannot use CoreSpotlight indexing service prior to iOS 9" }];
311306 if (callback) {
312- callback (nil , error);
307+ callback ([BNCPreferenceHelper preferenceHelper ]. userUrl , error);
313308 }
314309 else if (spotlightCallback) {
315310 spotlightCallback (nil , nil , error);
@@ -319,7 +314,7 @@ - (void)indexContentWithTitle:(NSString *)title
319314#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED < 90000
320315 NSError *error = [NSError errorWithDomain: BNCErrorDomain code: BNCBadRequestError userInfo: @{ NSLocalizedDescriptionKey : @" CoreSpotlight is not available because the base SDK for this project is less than 9.0" }];
321316 if (callback) {
322- callback (nil , error);
317+ callback ([BNCPreferenceHelper preferenceHelper ]. userUrl , error);
323318 }
324319 else if (spotlightCallback) {
325320 spotlightCallback (nil , nil , error);
@@ -334,7 +329,7 @@ - (void)indexContentWithTitle:(NSString *)title
334329 if (!isIndexingAvailable) {
335330 NSError *error = [NSError errorWithDomain: BNCErrorDomain code: BNCVersionError userInfo: @{ NSLocalizedDescriptionKey : @" Cannot use CoreSpotlight indexing service on this device/OS" }];
336331 if (callback) {
337- callback (nil , error);
332+ callback ([BNCPreferenceHelper preferenceHelper ]. userUrl , error);
338333 }
339334 else if (spotlightCallback) {
340335 spotlightCallback (nil , nil , error);
@@ -345,7 +340,7 @@ - (void)indexContentWithTitle:(NSString *)title
345340 if (!title) {
346341 NSError *error = [NSError errorWithDomain: BNCErrorDomain code: BNCBadRequestError userInfo: @{ NSLocalizedDescriptionKey : @" Spotlight Indexing requires a title" }];
347342 if (callback) {
348- callback (nil , error);
343+ callback ([BNCPreferenceHelper preferenceHelper ]. userUrl , error);
349344 }
350345 else if (spotlightCallback) {
351346 spotlightCallback (nil , nil , error);
@@ -397,7 +392,7 @@ - (void)indexContentWithTitle:(NSString *)title
397392 [[Branch getInstance ] getSpotlightUrlWithParams: spotlightLinkData callback: ^(NSDictionary *data, NSError *urlError) {
398393 if (urlError) {
399394 if (callback) {
400- callback (nil , urlError);
395+ callback ([BNCPreferenceHelper preferenceHelper ]. userUrl , urlError);
401396 }
402397 else if (spotlightCallback) {
403398 spotlightCallback (nil , nil , urlError);
@@ -430,8 +425,6 @@ - (void)indexContentWithUrl:(NSString *)url spotlightIdentifier:(NSString *)spot
430425 attributes = ((id (*)(id , SEL , NSString *))[attributes methodForSelector: initAttributesSelector])(attributes, initAttributesSelector, type);
431426 SEL setIdentifierSelector = NSSelectorFromString (@" setIdentifier:" );
432427 ((void (*)(id , SEL , NSString *))[attributes methodForSelector: setIdentifierSelector])(attributes, setIdentifierSelector, spotlightIdentifier);
433- SEL setRelatedUniqueIdentifierSelector = NSSelectorFromString (@" setRelatedUniqueIdentifier:" );
434- ((void (*)(id , SEL , NSString *))[attributes methodForSelector: setRelatedUniqueIdentifierSelector])(attributes, setRelatedUniqueIdentifierSelector, spotlightIdentifier);
435428 SEL setTitleSelector = NSSelectorFromString (@" setTitle:" );
436429 ((void (*)(id , SEL , NSString *))[attributes methodForSelector: setTitleSelector])(attributes, setTitleSelector, title);
437430 SEL setContentDescriptionSelector = NSSelectorFromString (@" setContentDescription:" );
@@ -440,65 +433,82 @@ - (void)indexContentWithUrl:(NSString *)url spotlightIdentifier:(NSString *)spot
440433 ((void (*)(id , SEL , NSURL *))[attributes methodForSelector: setThumbnailURLSelector])(attributes, setThumbnailURLSelector, thumbnailUrl);
441434 SEL setThumbnailDataSelector = NSSelectorFromString (@" setThumbnailData:" );
442435 ((void (*)(id , SEL , NSData *))[attributes methodForSelector: setThumbnailDataSelector])(attributes, setThumbnailDataSelector, thumbnailData);
436+ // NSUserActivity.CSSearchableItemAttributeSet.contentURL
443437 SEL setContentURLSelector = NSSelectorFromString (@" setContentURL:" );
444438 ((void (*)(id , SEL , NSURL *))[attributes methodForSelector: setContentURLSelector])(attributes, setContentURLSelector, [NSURL URLWithString: url]);
445439
446- // Index via the NSUserActivity strategy
447- // Currently (iOS 9 Beta 4) we need a strong reference to this, or it isn't indexed
448- self.currentUserActivity = [[NSUserActivity alloc ] initWithActivityType: spotlightIdentifier];
449- self.currentUserActivity .title = title;
450- self.currentUserActivity .webpageURL = [NSURL URLWithString: url]; // This should allow indexed content to fall back to the web if user doesn't have the app installed. Unable to test as of iOS 9 Beta 4
451- self.currentUserActivity .eligibleForSearch = YES ;
452- self.currentUserActivity .eligibleForPublicIndexing = publiclyIndexable;
453- SEL setContentAttributeSetSelector = NSSelectorFromString (@" setContentAttributeSet:" );
454- ((void (*)(id , SEL , id ))[self .currentUserActivity methodForSelector: setContentAttributeSetSelector])(self.currentUserActivity , setContentAttributeSetSelector, attributes);
455- self.currentUserActivity .userInfo = userInfo; // As of iOS 9 Beta 4, this gets lost and never makes it through to application:continueActivity:restorationHandler:
456- self.currentUserActivity .requiredUserInfoKeys = [NSSet setWithArray: userInfo.allKeys]; // This, however, seems to force the userInfo to come through.
457- self.currentUserActivity .keywords = keywords;
458- [self .currentUserActivity becomeCurrent ];
440+ NSDictionary *userActivityIndexingParams = @{@" title" : title,
441+ @" url" : url,
442+ @" spotlightId" : spotlightIdentifier,
443+ @" userInfo" : [userInfo mutableCopy ],
444+ @" keywords" : keywords,
445+ @" publiclyIndexable" : [NSNumber numberWithBool: publiclyIndexable],
446+ @" attributeSet" : attributes
447+ };
448+ [self indexUsingNSUserActivity: userActivityIndexingParams];
459449
460- // Index via the CoreSpotlight strategy
461- // get the CSSearchableItem Class object
462- id CSSearchableItemClass = NSClassFromString (@" CSSearchableItem" );
463- // alloc an empty instance
464- id searchableItem = [CSSearchableItemClass alloc ];
465- // create-by-name a selector fot the init method we want
466- SEL initItemSelector = NSSelectorFromString (@" initWithUniqueIdentifier:domainIdentifier:attributeSet:" );
467- // call the selector on the searchableItem with appropriate arguments
468- searchableItem = ((id (*)(id , SEL , NSString *, NSString *, id ))[searchableItem methodForSelector: initItemSelector])(searchableItem, initItemSelector, spotlightIdentifier, BRANCH_SPOTLIGHT_PREFIX, attributes);
469-
470- // create an assignment method to set the expiration date on the searchableItem
471- SEL expirationSelector = NSSelectorFromString (@" setExpirationDate:" );
472- // now invoke it on the searchableItem, providing the expirationdate
473- ((void (*)(id , SEL , NSDate *))[searchableItem methodForSelector: expirationSelector])(searchableItem, expirationSelector, expirationDate);
474-
475-
476- Class CSSearchableIndexClass = NSClassFromString (@" CSSearchableIndex" );
477- SEL defaultSearchableIndexSelector = NSSelectorFromString (@" defaultSearchableIndex" );
478- id defaultSearchableIndex = ((id (*)(id , SEL ))[CSSearchableIndexClass methodForSelector: defaultSearchableIndexSelector])(CSSearchableIndexClass, defaultSearchableIndexSelector);
479- SEL indexSearchableItemsSelector = NSSelectorFromString (@" indexSearchableItems:completionHandler:" );
480- void (^__nullable completionBlock)(NSError *indexError) = ^void (NSError *__nullable indexError) {
481- if (callback || spotlightCallback) {
482- if (indexError) {
483- if (callback) {
484- callback (nil , indexError);
485- }
486- else if (spotlightCallback) {
487- spotlightCallback (nil , nil , indexError);
488- }
489- }
490- else {
491- if (callback) {
492- callback (url, nil );
493- }
494- else if (spotlightCallback) {
495- spotlightCallback (url, spotlightIdentifier, nil );
496- }
497- }
450+ // Not handling error scenarios because they are already handled upstream by the caller
451+ if (url) {
452+ if (callback) {
453+ callback (url, nil );
454+ } else if (spotlightCallback) {
455+ spotlightCallback (url, spotlightIdentifier, nil );
498456 }
499- };
500- ((void (*)(id , SEL , NSArray *, void (^ __nullable)(NSError * __nullable error)))[defaultSearchableIndex methodForSelector: indexSearchableItemsSelector])(defaultSearchableIndex, indexSearchableItemsSelector, @[searchableItem], completionBlock);
457+ }
501458#endif
502459}
503460
461+ #pragma mark Delegate Methods
462+
463+ - (void )userActivityWillSave : (NSUserActivity *)userActivity {
464+ [userActivity addUserInfoEntriesFromDictionary: self .userInfo];
465+ }
466+
467+ #pragma mark Helper Methods
468+
469+ - (UIViewController *)getActiveViewController {
470+ Class UIApplicationClass = NSClassFromString (@" UIApplication" );
471+ UIViewController *rootViewController = [UIApplicationClass sharedApplication ].keyWindow .rootViewController ;
472+ return [self getActiveViewController: rootViewController];
473+ }
474+
475+ - (UIViewController *)getActiveViewController : (UIViewController *)rootViewController {
476+ UIViewController *activeController;
477+ if ([rootViewController isKindOfClass: [UINavigationController class ]]) {
478+ activeController = ((UINavigationController *)rootViewController).topViewController ;
479+ } else if ([rootViewController isKindOfClass: [UITabBarController class ]]) {
480+ activeController = ((UITabBarController *)rootViewController).selectedViewController ;
481+ } else {
482+ activeController = rootViewController;
483+ }
484+ return activeController;
485+ }
486+
487+ - (void )indexUsingNSUserActivity : (NSDictionary *)params {
488+ self.userInfo = params[@" userInfo" ];
489+ self.userInfo [CSSearchableItemActivityIdentifier] = params[@" spotlightId" ];
490+
491+ UIViewController *activeViewController = [self getActiveViewController ];
492+
493+ if (!activeViewController) {
494+ // if no view controller, don't index. Current use case: iMessage extensions
495+ return ;
496+ }
497+ NSString *uniqueIdentifier = [NSString stringWithFormat: @" io.branch.%@ " , [[NSBundle mainBundle ] bundleIdentifier ]];
498+ // Can't create any weak references here to the userActivity, otherwise it will not index.
499+ activeViewController.userActivity = [[NSUserActivity alloc ] initWithActivityType: uniqueIdentifier];
500+ activeViewController.userActivity .delegate = self;
501+ activeViewController.userActivity .title = params[@" title" ];
502+ activeViewController.userActivity .webpageURL = [NSURL URLWithString: params[@" url" ]];
503+ activeViewController.userActivity .eligibleForSearch = YES ;
504+ activeViewController.userActivity .eligibleForPublicIndexing = [params[@" publiclyIndexable" ] boolValue ];
505+ activeViewController.userActivity .userInfo = self.userInfo ; // This alone doesn't pass userInfo through
506+ activeViewController.userActivity .requiredUserInfoKeys = [NSSet setWithArray: self .userInfo.allKeys]; // This along with the delegate method userActivityWillSave, however, seem to force the userInfo to come through.
507+ activeViewController.userActivity .keywords = params[@" keywords" ];
508+ SEL setContentAttributeSetSelector = NSSelectorFromString (@" setContentAttributeSet:" );
509+ ((void (*)(id , SEL , id ))[activeViewController.userActivity methodForSelector: setContentAttributeSetSelector])(activeViewController.userActivity , setContentAttributeSetSelector, params[@" attributeSet" ]);
510+
511+ [activeViewController.userActivity becomeCurrent ];
512+ }
513+
504514@end
0 commit comments