NSUserActivity results

In my viewController, I'm creating an NSUserActivity, attaching an CSSearchableItemAttributesSet, assigning the activity to the viewController's userActivity property, and then calling becomeCurrent on the activity as below:


NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:activity.uniqueIdentifier];
CSSearchableItemAttributeSet *attribs = [self attributesForActivity:activity]; //sets title, contentDescription
userActivity.contentAttributeSet = attribs;
userActivity.eligibleForSearch = YES;
userActivity.eligibleForHandoff = YES;
userActivity.eligibleForPublicIndexing = YES;
self.userActivity = userActivity;
[self.userActivity becomeCurrent];


But nothing is showing up. I've tried running this on the simulator and on an iPhone 5 device, but nothing is happening. Can't see any log files to indicate anything is amiss.


If I use Core Spotlight and add the same item to the index, it shows up immediately on both device and simulator. Is the NSUserActivity integration not working yet?

Replies

NSUserActivity isn't working for me either on iOS 9.0 beta 5 on an iPad Mini 2 (Xcode 7 beta 6). I've tried all combinations of:

  • setting/not setting the .contentAttributeSet property
  • setting/not setting .userInfo (though I don't need it for this case) with and without .requiredUserInfoKeys
  • also setting .needsSave = YES
  • setting/not setting the keywords property
  • setting/not setting the thumbnailURL (which incidentally works just fine when using CSSearchableItem).
  • setting eligibleForHandoff = YES (and NO)


CSSearchableItem works just fine on this device for other data we're indexing.


Code is below:


NSUserActivity* shortcut = [[NSUserActivity alloc] initWithActivityType:@"com.example.myapp.show_settings"];
shortcut.eligibleForHandoff = NO;
shortcut.eligibleForSearch = YES;
shortcut.eligibleForPublicIndexing = YES;


shortcut.title = @"Some Title";


CSSearchableItemAttributeSet* attrs = [[CSSearchableItemAttributeSet alloc]
                                       initWithItemContentType:(__bridge NSString*)kUTTypeItem];
attrs.contentDescription = @"description";


shortcut.keywords = [NSSet setWithArray:@[@"keyword1",@"keyword2"]];


NSURLComponents* rsrc_path_parts = [NSURLComponents componentsWithURL:[[NSBundle mainBundle] resourceURL]
                                              resolvingAgainstBaseURL:YES];
rsrc_path_parts.path = [rsrc_path_parts.path stringByAppendingString:@"activitytype.png"];
attrs.thumbnailURL = rsrc_path_parts.URL;
shortcut.contentAttributeSet = attrs;
[shortcut becomeCurrent];


//Retain a reference so it makes into Spotlight
[_shortcut_activities addObject:shortcut];



At this point I have to conclude NSUserActivity for Spotlight just doesn't work.

Same here. I feel like I've "tried everything" without success.

This is all working fine for me now.


The missing part of the puzzle was that if you set a relatedUniqueIdentifier on an NSUserActivity, it doesn't get indexed. Seems like Apple didn't envisage this usage pattern so we've been advised that if you want to do this (i.e to be able to de-duplicate between CoreSpotlight and NSUserActivity) you should create a CoreSpotlight entry first with the relevant uniqueIdentifier.


Any updates from attributes in the NSUserActivity make it into Spotlight.


This works fine on the simulator and devices for me, both with b5 and tested with Xcode b6.


Other things I found important ( but it looks like you're already doing them)

Retaining the activity

Not using the userActivity property of viewController as this seems to be at the mercy of iOS so can lose its userInfo.


It may be worth trying with a different contentType. I remember back in the early betas some content types didn't seem to get indexed.

If you are setting a relatedUniqueIdentifier on a NSUserActivity, that identifier must already have been indexed with CoreSpotlight. Would that explain the behavior you're seeing?

Hi pdm, it doesn't. The exact code I'm using (santized parameters of course) is what I included above. These NSUserActivity instances are shortcuts into particular sections of the app so there's no need to relate them to anything else.


Since they're global shortcuts, I'm creating them in didFinishLaunchingWithOptions and adding them to an NSMutableArray that's strongly held. I also tried creating them in my root view controller instead with no better luck.

I copy/pasted the code you included into a new Single View project into the viewDidLoad method (just since it was there in the template), added a shortcut_activities property on the VC, initialized it to a mutable array (if it hadn't been initialized already), built, ran, and searched for "keyword1", "keyword2", and "some". In all cases I got a result in Spotlight. So in the build that I'm using it's working (granted my build is slightly newer than beta 5). But I'm fairly confident that it'd be working in beta 5 as well.


Can you check in the debugger that your array is actually non-nil? Could you also check the value of +[CSSearchableIndex isIndexingAvailable]? I believe iPad mini 2 is one of the supported devices, but it would be good to include that check regardless.


If you're still not able to get it to work, I'd suggest filing a bug report and attaching a sample app.

Hey pdm (thanks for your help btw). I feel really dumb...we were checking isIndexing, but the NSMutableArray wasn't getting alloc/init'd before use. So the addObject had no effect of retaining a reference.


Correcting that, we get what 1 out of 5 activities indexed. The code above is actually run inside a "for in" block that creates 5 "shortcut" activities and calls becomeCurrent on each one inside the loop before adding them to the array. It looks like whatever activity is in the last iteration of the loop is the one that gets created. I'm guessing that's because the becomeCurrent calls are too close together timewise?

If I had a $1 for every time I felt dumb for the same reason.... 🙂


NSUserActivity is intended to be just that, based on actual user activity. The fact that you're doing something in a loop indicates to me you're likely using the wrong API. I think you should be using CoreSpotlight instead of NSUserActivity. I would recommend switching to those APIs (which shouldn't be a large change) and see how that works out.


Then as the user actually activates the activities, then generate a single NSUserActivity (using the relatedUniqueIdentitifer to link it to your previous CoreSpotlight item that you indexed). This will give you the best results becase we'll see that you have indexed activity (CoreSpotlight) that the user has actually engaged with (NSUserActivity) and this gives us a hint that the CS items are valuable to the user, which may lead to your search results being promoted higher in search results.

Hello pdm,


I have a similiar problem like jasonjwwilliams. I have a list of Restaurants, i wish to index them for my users. Actually I started with CoreSpotlight indexing because it feels like more powerful. It correctly indexed every restaurant for me and when i searched, for instance, "restarunt a", it did show me all the information about that restaurant, which is good.


But the problem i had is when user clicks on the search result, indexed by "CoreSpolight", such as "restaurant a". I wish to bring them to the restaurant detail page. However, the only "userInfo" i can get from method


- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler


of that userActivity is the unique idenfitier, which is not enough for me to do that. So i tried to user NSUserActivity for indexing that list. Then i ended up exactly like Jason, it only indexed the last item in my array.


From the WWDC Search Api video, seems like they recommending us to not using any loader and show the result directly, but it feels like CoreSpotlight indexing can't carry enough information for doing that.


Please let me know if there is anything i did wrong, or I could have done. I have been struggling on this feature for a coupe of days. Please advise.


Thanks.

Hi pdm. Well not really...the docs on using Spotlight all urge you to use NSUserActivity to make shortcuts into your app available.


But there are numerous parts of the app the user won't know are there if they never use them (and hence haven't visited them yet so they would be indexed)...which obviates the recommendation to put shortcuts into Spotlight using NSUserActivity. For example, we have a Forgot Password mechanism in the app that the user may have trouble finding. It would be natural for them to type "<app> reset password" to figure out how to do it. So we'd like to preindex that activity for them, since the reason they're searching is because they haven't been there and can't figure out how to. Does that make sense?

Hey OblivioN. Actually your use is a bit different than why we're trying to use NSUserActivity in a loop (we just want shortcuts into major areas of the app).


For another very similar use to the one you described, we are using CSSearchableItem + CSSearchableIndex. We're using the following format for our Spotlight item IDs (since domainIdentifier on the CSSearchableItem isn't provided to willContinueActivity...which would be really helpful):


"<type_prefix>|<item_id>"


That lets us differentiate between different types of incoming items since they all come in with activity type CSSearchableItemActionType and no real further information. So if we were indexing recipes an item ID for us would look like:


"recipe|b948e24a-d33f-4d39-b97b-13c08cd2f414"


Then we look up b948e24a-d33f-4d39-b97b-13c08cd2f414 in the recipe database to pull out the rest of the information we need.


I imagine you could use a similar delimited encoding of your own devising to put the basic info you need at retrieval time in the ID.

Hello Jason,


Thank you very much for your suggestion. I still have some questions. When you mentioned "pull out from Database", you are talking about the local database in the client or your backend server? We were planning to call our backend to get all the information we need, but we saw in WWDC 2015 Search Api Seesion video, they suggested not to using any loading when showing the destination page when press search result. So that I was trying to find a way to avoid extra loading from our backend. I would except Apple provide some kinda of solution if they suggested that way.


Don't know if you guys are using backend server for that or not, Please let me know. If there is no other way to avoid the loader on resume, i would let my team know, so we can move foward with the easier solution. Wish pdm can give a more official answers on this part.


Thank you once again.

We're using a local SQLite DB for our Spotlight data, but I would do the same thing for the the data from our REST APIs we need to index.


I watched the same video and I interpreted their comments as meaning you don't want to pop-up a "Loading ..." interstitial while you grab the content. So my approach would be to:


1.) Store 3 or 4 of the crucial data fields in the index ID itself using a delimiter. That would allow me to present the detail view immediately and fill it with enough info to get the user started, while I loaded the rest of the data in the background and updated the detail view as it came in.

2.) Also, when you pull the content originally from the server, I would take the result and store it in a private NSURLCache instance (you could also use a SQLite database instead) used only for caching indexed records and set them with a very long expiration stored under a special URL like recipe://uuid. THEN index it with Spotlight. On retrieval I would immediately try to fill the detail view with content from the custom NSURLCache instance (recipe://uuid), and in the background fire off a web request to the API to pull the current data...then update it when it arrives (reindex it, update the cache too).


Combined you'd always have at least the minimum amount of data to show a user, and optimally all of the data when the cache has it, and then any staleness would be updated within a few seconds.


-J

So I FINALLY got it working. Here are a few comments


- setup your .plist as follows

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
  <string>com.stockswipe.stocksSwiped</string>
  <string>com.stockswipe.stocksViewed</string>
</array>
</plist>

- setup your function as follows

@available(iOS 9.0, *)
func createNSUserActivity(title: String, contentDescription: String, image: NSData?, uniqueIdentifier: String, domainIdentifier: String) {

    let attributeSet:CSSearchableItemAttributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeImage as String)
    attributeSet.contentDescription = contentDescription
    attributeSet.thumbnailData = image

    let activity: NSUserActivity = NSUserActivity(activityType: domainIdentifier)
    activity.title = title
    activity.keywords = NSSet(array: [title, contentDescription]) as! Set<String>
    activity.userInfo = ["symbol": title]
    activity.contentAttributeSet = attributeSet

    activity.eligibleForSearch = true
    activity.eligibleForPublicIndexing = true
    activity.requiredUserInfoKeys = NSSet(array: ["title", "userInfo", "contentAttributeSet", "eligibleForSearch", "eligibleForPublicIndexing"]) as! Set<String>

    userActivityTest = activity
    activity.becomeCurrent()
}

important was to create var userActivityTest = activity which stores that activity.