├── ios ├── RCTSearchApi │ ├── RCTSearchApiManager.h │ ├── NSDictionary+RCTSearchApi.h │ ├── NSDictionary+RCTSearchApi.m │ └── RCTSearchApiManager.m └── RCTSearchApi.xcodeproj │ └── project.pbxproj ├── .gitignore ├── package.json ├── LICENSE ├── README.md └── index.js /ios/RCTSearchApi/RCTSearchApiManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // RCTSearchApiManager.h 3 | // RCTSearchApi 4 | // 5 | // Created by Daniil Konoplev on 17/11/2016. 6 | // Copyright © 2016 Ombori AB. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface RCTSearchApiManager : RCTEventEmitter 13 | 14 | + (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | *.iml 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | 37 | # BUCK 38 | buck-out/ 39 | \.buckd/ 40 | android/app/libs 41 | android/keystores/debug.keystore 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-search-api", 3 | "repository": { 4 | "type": "git", 5 | "url": "https://github.com/ombori/react-native-search-api" 6 | }, 7 | "version": "1.2.0", 8 | "description": "A React Native module that allows to perform the Spotlight integration on iOS", 9 | "author": "Daniil Konoplev ", 10 | "nativePackage": true, 11 | "license": "MIT", 12 | "homepage": "https://github.com/ombori/react-native-search-api", 13 | "keywords": [ 14 | "react-native", 15 | "react", 16 | "native", 17 | "search", 18 | "indexing", 19 | "ios", 20 | "spotlight" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /ios/RCTSearchApi/NSDictionary+RCTSearchApi.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+RCTSearchApi.h 3 | // RCTSearchApi 4 | // 5 | // Created by Daniil Konoplev on 17/11/2016. 6 | // Copyright © 2016 Ombori AB. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface NSDictionary (RCTSearchApi) 13 | 14 | @property (nonatomic, readonly) NSString *rctsa_title; 15 | @property (nonatomic, readonly) NSString *rctsa_contentDescription; 16 | @property (nonatomic, readonly) NSArray *rctsa_keywords; 17 | @property (nonatomic, readonly) RCTImageSource *rctsa_thumbnail; 18 | @property (nonatomic, readonly) NSString *rctsa_uniqueIdentifier; 19 | @property (nonatomic, readonly) NSString *rctsa_domain; 20 | @property (nonatomic, readonly) NSDictionary *rctsa_userInfo; 21 | @property (nonatomic, readonly) BOOL rctsa_eligibleForPublicIndexing; 22 | @property (nonatomic, readonly) NSDate *rctsa_expirationDate; 23 | @property (nonatomic, readonly) NSURL *rctsa_webpageURL; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Ombori Group AB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ios/RCTSearchApi/NSDictionary+RCTSearchApi.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSDictionary+RCTSearchApi.m 3 | // RCTSearchApi 4 | // 5 | // Created by Daniil Konoplev on 17/11/2016. 6 | // Copyright © 2016 Ombori AB. All rights reserved. 7 | // 8 | 9 | #import "NSDictionary+RCTSearchApi.h" 10 | 11 | @implementation NSDictionary (RCTSearchApi) 12 | 13 | - (NSString *)rctsa_title { 14 | return self[@"title"]; 15 | } 16 | 17 | - (NSString *)rctsa_contentDescription { 18 | return self[@"contentDescription"]; 19 | } 20 | 21 | - (NSArray *)rctsa_keywords { 22 | return self[@"keywords"]; 23 | } 24 | 25 | - (RCTImageSource *)rctsa_thumbnail { 26 | return [RCTConvert RCTImageSource:self[@"thumbnail"]]; 27 | } 28 | 29 | - (NSString *)rctsa_uniqueIdentifier { 30 | return self[@"uniqueIdentifier"]; 31 | } 32 | 33 | - (NSString *)rctsa_domain { 34 | return self[@"domain"]; 35 | } 36 | 37 | - (NSDictionary *)rctsa_userInfo { 38 | return self[@"userInfo"]; 39 | } 40 | 41 | - (BOOL)rctsa_eligibleForPublicIndexing { 42 | return [self[@"eligibleForPublicIndexing"] boolValue]; 43 | } 44 | 45 | - (NSDate *)rctsa_expirationDate { 46 | return self[@"expirationDate"]; 47 | } 48 | 49 | - (NSURL *)rctsa_webpageURL { 50 | NSString *uri = self[@"webpageURL"]; 51 | if (!uri) 52 | return nil; 53 | return [NSURL URLWithString:uri]; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Native Search Api module 2 | 3 | The `SearchApi` module gives you a general React Native interface to interact with the iOS Search API, Core Spotlight. 4 | 5 | For more information about iOS Search APIs, see [https://developer.apple.com/ios/search/](https://developer.apple.com/ios/search/). 6 | 7 | ## Installation 8 | 9 | ### Automatic part 10 | 11 | 1. `npm install react-native-search-api --save` 12 | 1. `react-native link` 13 | 14 | ### Manual part 15 | 16 | To the top of your `AppDelegate.m` add the following line: 17 | ```objc 18 | #import "RCTSearchApiManager.h" 19 | ``` 20 | 21 | In your AppDelegate implementation add the following: 22 | ```objc 23 | - (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler { 24 | return [RCTSearchApiManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler]; 25 | } 26 | ``` 27 | 28 | ## Usage 29 | 30 | Subscribe to the search item open events in your components like this: 31 | ```js 32 | componentDidMount() { 33 | <...> 34 | SearchApi.addOnSpotlightItemOpenEventListener(this.handleOnSpotlightItemOpenEventListener); 35 | SearchApi.addOnAppHistoryItemOpenEventListener(this.handleOnAppHistoryItemOpenEventListener); 36 | } 37 | ``` 38 | 39 | To prevent memory leaks don't forget to unsubscribe: 40 | ```js 41 | componentWillUnmount() { 42 | <...> 43 | SearchApi.removeOnSpotlightItemOpenEventListener(this.handleOnSpotlightItemOpenEventListener); 44 | SearchApi.removeOnAppHistoryItemOpenEventListener(this.handleOnAppHistoryItemOpenEventListener) 45 | } 46 | ``` 47 | 48 | Generally you should be interested whether the app was started using the search, therefore consider using 49 | the following two methods: 50 | ```js 51 | // For the spotlight item: 52 | SearchApi.getInitialSpotlightItem().then(result => { 53 | if (result) { 54 | console.log('Started with a spotlight item!') 55 | } 56 | }) 57 | // For the app history item: 58 | SearchApi.getInitialAppHistoryItem().then(result => { 59 | if (result) { 60 | console.log('Started with an app history item!') 61 | } 62 | }) 63 | ``` 64 | 65 | In order to create a new spotlight item, use `indexSpotlightItem` or `indexSpotlightItems`: 66 | ```js 67 | SearchApi.indexSpotlightItem(item).then(result => { 68 | console.log('Success'); 69 | }).catch(err => { 70 | console.log('Error: ' + err); 71 | }); 72 | ``` 73 | 74 | To add new items to the app history, use `createUserActivity`: 75 | ```js 76 | SearchApi.indexAppHistoryItem(item).then(result => { 77 | console.log('Success'); 78 | that.setState({labelText: 'Success'}); 79 | }).catch(err => { 80 | console.log('Error: ' + err); 81 | that.setState({labelText: ('Error: ' + err)}); 82 | }); 83 | ``` 84 | 85 | The parameters, that items may specify are listed below: 86 | 87 | ## Search item keys 88 | 89 | Dictionaries, passed to create spotlight and app history items have some common 90 | and some specific keys, here is the list of all possible keys. 91 | 92 | ### Common keys 93 | 94 | ##### `title`: string 95 | Title of the item. Required for both item types. 96 | 97 | ##### `contentDescription`: string 98 | Description of the item. Optional. 99 | 100 | ##### `keywords`: Array 101 | An array of keywords, assigned to the search item. Optional. 102 | 103 | ##### `thumbnail`: string|int|object 104 | Thumbnail to be presented in the search results. The same format as `source` in 105 | the `Image` component. Optional. 106 | 107 | Examples: 108 | ```js 109 | var localItem = { 110 | <...>, 111 | thumbnail: require('/react-native/img/favicon.png') 112 | }; 113 | var remoteItem = { 114 | <...>, 115 | thumbnail: {uri: 'https://facebook.github.io/react-native/docs/assets/favicon.png'} 116 | }; 117 | ``` 118 | 119 | Please refer to [documentation](https://facebook.github.io/react-native/docs/image.html) for more details. 120 | 121 | ### Spotlight-specific keys 122 | 123 | ##### `uniqueIdentifier`: string 124 | The unique identifier of the spotlight item, passed later on during 125 | the item opening event. Required. 126 | 127 | ##### `domain`: string 128 | The domain for the spotlight item. Optional. 129 | 130 | ### App history-specific keys 131 | 132 | ##### `userInfo`: Object 133 | A dictionary, passed later on during the item opening event. Required. 134 | 135 | ##### `eligibleForPublicIndexing`: boolean 136 | A flag, that when set to `true` allows to add the item to the public index. 137 | Optional. 138 | 139 | ##### `expirationDate`: Date 140 | Expiration date of the user activity item. Optional. 141 | 142 | ##### `webpageURL`: string 143 | URL of the page, representing the same content on the app's website. 144 | 145 | ## Credits 146 | [© 2017 PresenceKit by Ombori AB](https://ombori.com/) 147 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | import { NativeModules, NativeEventEmitter } from 'react-native'; 3 | import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; 4 | 5 | const SearchApiManager = NativeModules.SearchApiManager; 6 | 7 | const SPOTLIGHT_SEARCH_ITEM_TAPPED_EVENT = "spotlightSearchItemTapped"; 8 | const APP_HISTORY_SEARCH_ITEM_TAPPED = "appHistorySearchItemTapped"; 9 | 10 | /** 11 | * `SearchApi` gives you a general interface to interact with the iOS Search API. 12 | * 13 | * ## Search item keys 14 | * 15 | * Dictionaries, passed to create spotlight and app history items have some common 16 | * and some specific keys, here is the list of all possible keys. 17 | * 18 | * ### Common keys 19 | * 20 | * #### title: string 21 | * Title of the item. Required for both item types. 22 | * 23 | * #### contentDescription: string 24 | * Description of the item. Optional. 25 | * 26 | * #### keywords: Array 27 | * An array of keywords, assigned to the search item. Optional. 28 | * 29 | * #### thumbnail: string|object 30 | * Image to be used as the thumbnail. Same as the `source` value of the `Image` 31 | * view. Optional. 32 | * 33 | * ### Spotlight-specific keys 34 | * 35 | * #### uniqueIdentifier: string 36 | * The unique identifier of the spotlight item, passed later on during 37 | * the item opening event. Required. 38 | * 39 | * #### domain: string 40 | * The domain for the spotlight item. Optional. 41 | * 42 | * ### App history-specific keys 43 | * 44 | * #### userInfo: Object 45 | * A dictionary, passed later on during the item opening event. Required. 46 | * 47 | * #### eligibleForPublicIndexing: boolean 48 | * A flag, that when set to `true` allows to add the item to the public index. 49 | * Optional. 50 | * 51 | * #### expirationDate: Date 52 | * Expiration date of the search item. Optional. 53 | * 54 | * #### webpageURL: string 55 | * URL of the page, representing the same content on the app's website. 56 | */ 57 | class SearchApi extends NativeEventEmitter { 58 | 59 | constructor() { 60 | super(SearchApiManager); 61 | } 62 | 63 | /** 64 | * Gets the initial spotlight item's identifier. Resoves to null 65 | * in case the app was started otherwise. 66 | * 67 | * @NOTE A good place for calling this method is the component's 68 | * `componentDidMount` override. 69 | */ 70 | getInitialSpotlightItem(): Promise { 71 | return SearchApiManager.getInitialSpotlightItem(); 72 | } 73 | 74 | /** 75 | * Gets the initial app history item's user info dictionary. Resolves to null 76 | * in case the app was started otherwise. 77 | * 78 | * @NOTE A good place for calling this method is the component's 79 | * `componentDidMount` override. 80 | */ 81 | getInitialAppHistoryItem(): Promise { 82 | return SearchApiManager.getInitialAppHistoryItem(); 83 | } 84 | 85 | /** 86 | * Registers for the spotlight item opening event. 87 | * 88 | * @NOTE A good place for calling this method is the component's 89 | * `componentDidMount` override. 90 | * 91 | * @param listener A function that takes a single parameter 92 | * of type `string`, containing the unique identifier of the 93 | * spotlight item. 94 | */ 95 | addOnSpotlightItemOpenEventListener(listener: Function) { 96 | this.addListener(SPOTLIGHT_SEARCH_ITEM_TAPPED_EVENT, listener); 97 | } 98 | 99 | /** 100 | * Removes the spotlight item opening event listener. 101 | * 102 | * @NOTE A good place for calling this method is the component's 103 | * `componentWillUnmount` override. 104 | * 105 | * @param listener The function, previously passed to 106 | * `addOnSpotlightItemOpenEventListener`. 107 | */ 108 | removeOnSpotlightItemOpenEventListener(listener: Function) { 109 | this.removeListener(SPOTLIGHT_SEARCH_ITEM_TAPPED_EVENT, listener); 110 | } 111 | 112 | /** 113 | * Registers for the app history item opening event. 114 | * 115 | * @NOTE A good place for calling this method is the component's 116 | * `componentDidMount` override. 117 | * 118 | * @param listener A function that takes a single parameter 119 | * of type `Object`, containing the user info, passed when 120 | * creating the search item. 121 | */ 122 | addOnAppHistoryItemOpenEventListener(listener: Function) { 123 | this.addListener(APP_HISTORY_SEARCH_ITEM_TAPPED, listener); 124 | } 125 | 126 | /** 127 | * Removes the app history item opening event listener. 128 | * 129 | * @NOTE A good place for calling this method is the component's 130 | * `componentWillUnmount` override. 131 | * 132 | * @param listener The function, previously passed to 133 | * `addOnAppHistoryItemOpenEventListener`. 134 | */ 135 | removeOnAppHistoryItemOpenEventListener(listener: Function) { 136 | this.removeListener(APP_HISTORY_SEARCH_ITEM_TAPPED, listener); 137 | } 138 | 139 | /** 140 | * Adds a new item to the spotlight index. 141 | * 142 | * @param item A dictionary with the item's parameters. 143 | * See the comment above this class for more info. 144 | */ 145 | indexSpotlightItem(item: Object): Promise { 146 | return this.indexSpotlightItems([item]); 147 | } 148 | 149 | /** 150 | * Adds an array of new items to the spotlight index. 151 | * 152 | * @param items An array with new items to be added. 153 | * See the comment above this class for more info. 154 | */ 155 | indexSpotlightItems(items: Array): Promise { 156 | var copies = items.map(item => resolveItemThumbnail(item)); 157 | return SearchApiManager.indexItems(copies); 158 | } 159 | 160 | /** 161 | * Deletes all items with specified identifiers from the 162 | * spotlight index. 163 | * 164 | * @param identifiers An array of unique item identifiers. 165 | */ 166 | deleteSpotlightItemsWithIdentifiers(identifiers: Array): Promise { 167 | return SearchApiManager.deleteItemsWithIdentifiers(identifiers); 168 | } 169 | 170 | /** 171 | * Deletes all items in specified domains from the spotlight index. 172 | * 173 | * @param domains An array of spotlight item domains. 174 | */ 175 | deleteSpotlightItemsInDomains(domains: Array): Promise { 176 | return SearchApiManager.deleteItemsInDomains(domains); 177 | } 178 | 179 | /** 180 | * Clears up the spotlight index. 181 | */ 182 | deleteAllSpotlightItems(): Promise { 183 | return SearchApiManager.deleteAllItems(); 184 | } 185 | 186 | /** 187 | * Creates a new search item, added to the app history. 188 | * 189 | * @param item A dictionary with the item's parameters. 190 | * See the comment above this class for more info. 191 | */ 192 | indexAppHistoryItem(item: Object): Promise { 193 | var itemCopy = resolveItemThumbnail(item); 194 | return SearchApiManager.createUserActivity(itemCopy); 195 | } 196 | 197 | } 198 | 199 | function resolveItemThumbnail(item: Object): Object { 200 | var itemCopy = JSON.parse(JSON.stringify(item)); 201 | itemCopy.thumbnail = resolveAssetSource(item.thumbnail); 202 | return itemCopy; 203 | } 204 | 205 | export default new SearchApi(); 206 | -------------------------------------------------------------------------------- /ios/RCTSearchApi.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CCA11CD51DDDCEAB0083321D /* NSDictionary+RCTSearchApi.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA11CD21DDDCEA70083321D /* NSDictionary+RCTSearchApi.m */; }; 11 | CCA11CD61DDDCEAB0083321D /* RCTSearchApiManager.m in Sources */ = {isa = PBXBuildFile; fileRef = CCA11CD41DDDCEA70083321D /* RCTSearchApiManager.m */; }; 12 | /* End PBXBuildFile section */ 13 | 14 | /* Begin PBXCopyFilesBuildPhase section */ 15 | F35C8FBC1D19DABC00E97D91 /* CopyFiles */ = { 16 | isa = PBXCopyFilesBuildPhase; 17 | buildActionMask = 2147483647; 18 | dstPath = "include/$(PRODUCT_NAME)"; 19 | dstSubfolderSpec = 16; 20 | files = ( 21 | ); 22 | runOnlyForDeploymentPostprocessing = 0; 23 | }; 24 | /* End PBXCopyFilesBuildPhase section */ 25 | 26 | /* Begin PBXFileReference section */ 27 | CCA11CD11DDDCEA70083321D /* NSDictionary+RCTSearchApi.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+RCTSearchApi.h"; sourceTree = ""; }; 28 | CCA11CD21DDDCEA70083321D /* NSDictionary+RCTSearchApi.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+RCTSearchApi.m"; sourceTree = ""; }; 29 | CCA11CD31DDDCEA70083321D /* RCTSearchApiManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTSearchApiManager.h; sourceTree = ""; }; 30 | CCA11CD41DDDCEA70083321D /* RCTSearchApiManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCTSearchApiManager.m; sourceTree = ""; }; 31 | F35C8FBE1D19DABC00E97D91 /* libRCTSearchApi.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRCTSearchApi.a; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | F35C8FBB1D19DABC00E97D91 /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXFrameworksBuildPhase section */ 43 | 44 | /* Begin PBXGroup section */ 45 | CCA11CD01DDDCEA70083321D /* RCTSearchApi */ = { 46 | isa = PBXGroup; 47 | children = ( 48 | CCA11CD11DDDCEA70083321D /* NSDictionary+RCTSearchApi.h */, 49 | CCA11CD21DDDCEA70083321D /* NSDictionary+RCTSearchApi.m */, 50 | CCA11CD31DDDCEA70083321D /* RCTSearchApiManager.h */, 51 | CCA11CD41DDDCEA70083321D /* RCTSearchApiManager.m */, 52 | ); 53 | path = RCTSearchApi; 54 | sourceTree = ""; 55 | }; 56 | F35C8FB51D19DABC00E97D91 = { 57 | isa = PBXGroup; 58 | children = ( 59 | CCA11CD01DDDCEA70083321D /* RCTSearchApi */, 60 | F35C8FBF1D19DABC00E97D91 /* Products */, 61 | ); 62 | sourceTree = ""; 63 | }; 64 | F35C8FBF1D19DABC00E97D91 /* Products */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | F35C8FBE1D19DABC00E97D91 /* libRCTSearchApi.a */, 68 | ); 69 | name = Products; 70 | sourceTree = ""; 71 | }; 72 | /* End PBXGroup section */ 73 | 74 | /* Begin PBXNativeTarget section */ 75 | F35C8FBD1D19DABC00E97D91 /* RCTSearchApi */ = { 76 | isa = PBXNativeTarget; 77 | buildConfigurationList = F35C8FC71D19DABC00E97D91 /* Build configuration list for PBXNativeTarget "RCTSearchApi" */; 78 | buildPhases = ( 79 | F35C8FBA1D19DABC00E97D91 /* Sources */, 80 | F35C8FBB1D19DABC00E97D91 /* Frameworks */, 81 | F35C8FBC1D19DABC00E97D91 /* CopyFiles */, 82 | ); 83 | buildRules = ( 84 | ); 85 | dependencies = ( 86 | ); 87 | name = RCTSearchApi; 88 | productName = RCTSpotlightSearch; 89 | productReference = F35C8FBE1D19DABC00E97D91 /* libRCTSearchApi.a */; 90 | productType = "com.apple.product-type.library.static"; 91 | }; 92 | /* End PBXNativeTarget section */ 93 | 94 | /* Begin PBXProject section */ 95 | F35C8FB61D19DABC00E97D91 /* Project object */ = { 96 | isa = PBXProject; 97 | attributes = { 98 | LastUpgradeCheck = 0810; 99 | ORGANIZATIONNAME = "Ombori AB"; 100 | TargetAttributes = { 101 | F35C8FBD1D19DABC00E97D91 = { 102 | CreatedOnToolsVersion = 7.3; 103 | }; 104 | }; 105 | }; 106 | buildConfigurationList = F35C8FB91D19DABC00E97D91 /* Build configuration list for PBXProject "RCTSearchApi" */; 107 | compatibilityVersion = "Xcode 3.2"; 108 | developmentRegion = English; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | ); 113 | mainGroup = F35C8FB51D19DABC00E97D91; 114 | productRefGroup = F35C8FBF1D19DABC00E97D91 /* Products */; 115 | projectDirPath = ""; 116 | projectRoot = ""; 117 | targets = ( 118 | F35C8FBD1D19DABC00E97D91 /* RCTSearchApi */, 119 | ); 120 | }; 121 | /* End PBXProject section */ 122 | 123 | /* Begin PBXSourcesBuildPhase section */ 124 | F35C8FBA1D19DABC00E97D91 /* Sources */ = { 125 | isa = PBXSourcesBuildPhase; 126 | buildActionMask = 2147483647; 127 | files = ( 128 | CCA11CD61DDDCEAB0083321D /* RCTSearchApiManager.m in Sources */, 129 | CCA11CD51DDDCEAB0083321D /* NSDictionary+RCTSearchApi.m in Sources */, 130 | ); 131 | runOnlyForDeploymentPostprocessing = 0; 132 | }; 133 | /* End PBXSourcesBuildPhase section */ 134 | 135 | /* Begin XCBuildConfiguration section */ 136 | F35C8FC51D19DABC00E97D91 /* Debug */ = { 137 | isa = XCBuildConfiguration; 138 | buildSettings = { 139 | ALWAYS_SEARCH_USER_PATHS = NO; 140 | CLANG_ANALYZER_NONNULL = YES; 141 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 142 | CLANG_CXX_LIBRARY = "libc++"; 143 | CLANG_ENABLE_MODULES = YES; 144 | CLANG_ENABLE_OBJC_ARC = YES; 145 | CLANG_WARN_BOOL_CONVERSION = YES; 146 | CLANG_WARN_CONSTANT_CONVERSION = YES; 147 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 148 | CLANG_WARN_EMPTY_BODY = YES; 149 | CLANG_WARN_ENUM_CONVERSION = YES; 150 | CLANG_WARN_INFINITE_RECURSION = YES; 151 | CLANG_WARN_INT_CONVERSION = YES; 152 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 153 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 154 | CLANG_WARN_UNREACHABLE_CODE = YES; 155 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 156 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 157 | COPY_PHASE_STRIP = NO; 158 | DEBUG_INFORMATION_FORMAT = dwarf; 159 | ENABLE_STRICT_OBJC_MSGSEND = YES; 160 | ENABLE_TESTABILITY = YES; 161 | GCC_C_LANGUAGE_STANDARD = gnu99; 162 | GCC_DYNAMIC_NO_PIC = NO; 163 | GCC_NO_COMMON_BLOCKS = YES; 164 | GCC_OPTIMIZATION_LEVEL = 0; 165 | GCC_PREPROCESSOR_DEFINITIONS = ( 166 | "DEBUG=1", 167 | "$(inherited)", 168 | ); 169 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 170 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 171 | GCC_WARN_UNDECLARED_SELECTOR = YES; 172 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 173 | GCC_WARN_UNUSED_FUNCTION = YES; 174 | GCC_WARN_UNUSED_VARIABLE = YES; 175 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 176 | MTL_ENABLE_DEBUG_INFO = YES; 177 | ONLY_ACTIVE_ARCH = YES; 178 | SDKROOT = iphoneos; 179 | }; 180 | name = Debug; 181 | }; 182 | F35C8FC61D19DABC00E97D91 /* Release */ = { 183 | isa = XCBuildConfiguration; 184 | buildSettings = { 185 | ALWAYS_SEARCH_USER_PATHS = NO; 186 | CLANG_ANALYZER_NONNULL = YES; 187 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 188 | CLANG_CXX_LIBRARY = "libc++"; 189 | CLANG_ENABLE_MODULES = YES; 190 | CLANG_ENABLE_OBJC_ARC = YES; 191 | CLANG_WARN_BOOL_CONVERSION = YES; 192 | CLANG_WARN_CONSTANT_CONVERSION = YES; 193 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 194 | CLANG_WARN_EMPTY_BODY = YES; 195 | CLANG_WARN_ENUM_CONVERSION = YES; 196 | CLANG_WARN_INFINITE_RECURSION = YES; 197 | CLANG_WARN_INT_CONVERSION = YES; 198 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 199 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 200 | CLANG_WARN_UNREACHABLE_CODE = YES; 201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 202 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 203 | COPY_PHASE_STRIP = NO; 204 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 205 | ENABLE_NS_ASSERTIONS = NO; 206 | ENABLE_STRICT_OBJC_MSGSEND = YES; 207 | GCC_C_LANGUAGE_STANDARD = gnu99; 208 | GCC_NO_COMMON_BLOCKS = YES; 209 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 210 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 211 | GCC_WARN_UNDECLARED_SELECTOR = YES; 212 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 213 | GCC_WARN_UNUSED_FUNCTION = YES; 214 | GCC_WARN_UNUSED_VARIABLE = YES; 215 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 216 | MTL_ENABLE_DEBUG_INFO = NO; 217 | SDKROOT = iphoneos; 218 | VALIDATE_PRODUCT = YES; 219 | }; 220 | name = Release; 221 | }; 222 | F35C8FC81D19DABC00E97D91 /* Debug */ = { 223 | isa = XCBuildConfiguration; 224 | buildSettings = { 225 | HEADER_SEARCH_PATHS = "$(inherited)"; 226 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 227 | OTHER_LDFLAGS = "-ObjC"; 228 | PRODUCT_NAME = "$(TARGET_NAME)"; 229 | SKIP_INSTALL = YES; 230 | }; 231 | name = Debug; 232 | }; 233 | F35C8FC91D19DABC00E97D91 /* Release */ = { 234 | isa = XCBuildConfiguration; 235 | buildSettings = { 236 | HEADER_SEARCH_PATHS = "$(inherited)"; 237 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 238 | OTHER_LDFLAGS = "-ObjC"; 239 | PRODUCT_NAME = "$(TARGET_NAME)"; 240 | SKIP_INSTALL = YES; 241 | }; 242 | name = Release; 243 | }; 244 | /* End XCBuildConfiguration section */ 245 | 246 | /* Begin XCConfigurationList section */ 247 | F35C8FB91D19DABC00E97D91 /* Build configuration list for PBXProject "RCTSearchApi" */ = { 248 | isa = XCConfigurationList; 249 | buildConfigurations = ( 250 | F35C8FC51D19DABC00E97D91 /* Debug */, 251 | F35C8FC61D19DABC00E97D91 /* Release */, 252 | ); 253 | defaultConfigurationIsVisible = 0; 254 | defaultConfigurationName = Release; 255 | }; 256 | F35C8FC71D19DABC00E97D91 /* Build configuration list for PBXNativeTarget "RCTSearchApi" */ = { 257 | isa = XCConfigurationList; 258 | buildConfigurations = ( 259 | F35C8FC81D19DABC00E97D91 /* Debug */, 260 | F35C8FC91D19DABC00E97D91 /* Release */, 261 | ); 262 | defaultConfigurationIsVisible = 0; 263 | defaultConfigurationName = Release; 264 | }; 265 | /* End XCConfigurationList section */ 266 | }; 267 | rootObject = F35C8FB61D19DABC00E97D91 /* Project object */; 268 | } 269 | -------------------------------------------------------------------------------- /ios/RCTSearchApi/RCTSearchApiManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // RCTSearchApiManager.m 3 | // RCTSearchApi 4 | // 5 | // Created by Daniil Konoplev on 17/11/2016. 6 | // Copyright © 2016 Ombori AB. All rights reserved. 7 | // 8 | 9 | @import CoreSpotlight; 10 | @import MobileCoreServices; 11 | 12 | #import "RCTSearchApiManager.h" 13 | #import "NSDictionary+RCTSearchApi.h" 14 | #import 15 | #import 16 | 17 | static NSString *const kHandleContinueUserActivityNotification = @"handleContinueUserActivity"; 18 | static NSString *const kUserActivityKey = @"userActivity"; 19 | static NSString *const kSpotlightSearchItemTapped = @"spotlightSearchItemTapped"; 20 | static NSString *const kAppHistorySearchItemTapped = @"appHistorySearchItemTapped"; 21 | static NSString *const kApplicationLaunchOptionsUserActivityKey = @"UIApplicationLaunchOptionsUserActivityKey"; 22 | 23 | typedef void (^ContentAttributeSetCreationCompletion)(CSSearchableItemAttributeSet *set, NSError *error); 24 | 25 | @interface RCTSearchApiManager () 26 | 27 | @property (nonatomic, strong) NSMutableArray *userActivities; 28 | 29 | @end 30 | 31 | @implementation RCTSearchApiManager 32 | 33 | RCT_EXPORT_MODULE(); 34 | 35 | #pragma mark - Initialization 36 | 37 | - (instancetype)init { 38 | if ((self = [super init])) { 39 | _userActivities = [NSMutableArray array]; 40 | } 41 | return self; 42 | } 43 | 44 | #pragma mark - Properties 45 | 46 | - (dispatch_queue_t)methodQueue { 47 | return dispatch_get_main_queue(); 48 | } 49 | 50 | - (NSArray *)supportedEvents { 51 | return @[kSpotlightSearchItemTapped, kAppHistorySearchItemTapped]; 52 | } 53 | 54 | #pragma mark - Public API 55 | 56 | + (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray *))restorationHandler { 57 | if (![userActivity.activityType isEqualToString:CSSearchableItemActionType] && 58 | ![userActivity.activityType containsString:[NSBundle mainBundle].bundleIdentifier]) 59 | return NO; 60 | [[NSNotificationCenter defaultCenter] postNotificationName:kHandleContinueUserActivityNotification 61 | object:nil 62 | userInfo:@{kUserActivityKey: userActivity}]; 63 | return YES; 64 | } 65 | 66 | - (void)startObserving { 67 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleContinueUserActivity:) name:kHandleContinueUserActivityNotification object:nil]; 68 | } 69 | 70 | - (void)stopObserving { 71 | [[NSNotificationCenter defaultCenter] removeObserver:self]; 72 | } 73 | 74 | #pragma mark - Exported API 75 | 76 | RCT_REMAP_METHOD(getInitialSpotlightItem, retrieveInitialSpotlightItemWithResolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { 77 | [self retrieveInitialSearchItemOfType:CSSearchableItemActionType bodyBlock:^id(NSUserActivity *activity) { 78 | return activity.userInfo[CSSearchableItemActivityIdentifier]; 79 | } resolve:resolve]; 80 | } 81 | 82 | RCT_REMAP_METHOD(getInitialAppHistoryItem, retrieveInitialAppHistoryItemWithResolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { 83 | [self retrieveInitialSearchItemOfType:[NSBundle mainBundle].bundleIdentifier bodyBlock:^id(NSUserActivity *activity) { 84 | return activity.userInfo; 85 | } resolve:resolve]; 86 | } 87 | 88 | RCT_EXPORT_METHOD(indexItems:(NSArray *)items resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { 89 | if (items.count == 0) 90 | return resolve(nil); 91 | dispatch_group_t group = dispatch_group_create(); 92 | NSMutableArray *itemsToIndex = [NSMutableArray array]; 93 | [items enumerateObjectsUsingBlock:^(NSDictionary *item, NSUInteger idx, BOOL * _Nonnull stop) { 94 | dispatch_group_enter(group); 95 | [self createContentAttributeSetFromItem:item withCompletion:^(CSSearchableItemAttributeSet *set, NSError *error) { 96 | if (set && !error) { 97 | CSSearchableItem *searchableItem = [[CSSearchableItem alloc] initWithUniqueIdentifier:item.rctsa_uniqueIdentifier 98 | domainIdentifier:item.rctsa_domain 99 | attributeSet:set]; 100 | [itemsToIndex addObject:searchableItem]; 101 | } 102 | dispatch_group_leave(group); 103 | }]; 104 | }]; 105 | dispatch_group_notify(group, self.methodQueue, ^{ 106 | if (itemsToIndex.count == items.count) { 107 | return [[CSSearchableIndex defaultSearchableIndex] indexSearchableItems:itemsToIndex completionHandler:[self completionBlockWithResolve:resolve reject:reject]]; 108 | } else { 109 | NSError *e = RCTErrorWithMessage(@"Failed to create one or more content attribute sets"); 110 | reject(RCTErrorUnspecified, e.localizedDescription, e); 111 | } 112 | }); 113 | } 114 | 115 | RCT_EXPORT_METHOD(deleteItemsWithIdentifiers:(NSArray *)identifiers resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { 116 | [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithIdentifiers:identifiers completionHandler:[self completionBlockWithResolve:resolve reject:reject]]; 117 | } 118 | 119 | RCT_EXPORT_METHOD(deleteItemsInDomains:(NSArray *)identifiers resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { 120 | [[CSSearchableIndex defaultSearchableIndex] deleteSearchableItemsWithDomainIdentifiers:identifiers completionHandler:[self completionBlockWithResolve:resolve reject:reject]]; 121 | } 122 | 123 | RCT_REMAP_METHOD(deleteAllItems, resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { 124 | [[CSSearchableIndex defaultSearchableIndex] deleteAllSearchableItemsWithCompletionHandler:[self completionBlockWithResolve:resolve reject:reject]]; 125 | } 126 | 127 | RCT_EXPORT_METHOD(createUserActivity:(NSDictionary *)item resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) { 128 | [self createContentAttributeSetFromItem:item withCompletion:^(CSSearchableItemAttributeSet *set, NSError *error) { 129 | dispatch_async(self.methodQueue, ^{ 130 | if (error || !set) { 131 | NSError *e = error ?: RCTErrorWithMessage(@"Could not create a content attribute set"); 132 | reject(RCTErrorUnspecified, e.localizedDescription, e); 133 | } else { 134 | NSUserActivity *userActivity = [[NSUserActivity alloc] initWithActivityType:[NSBundle mainBundle].bundleIdentifier]; 135 | userActivity.contentAttributeSet = set; 136 | userActivity.title = item.rctsa_title; 137 | userActivity.userInfo = item.rctsa_userInfo; 138 | userActivity.eligibleForPublicIndexing = item.rctsa_eligibleForPublicIndexing; 139 | userActivity.expirationDate = item.rctsa_expirationDate; 140 | userActivity.webpageURL = item.rctsa_webpageURL; 141 | userActivity.eligibleForSearch = YES; 142 | userActivity.eligibleForHandoff = NO; 143 | [userActivity becomeCurrent]; 144 | [self.userActivities addObject:userActivity]; 145 | resolve(nil); 146 | } 147 | }); 148 | }]; 149 | } 150 | 151 | #pragma mark - Private API 152 | 153 | - (void)handleContinueUserActivity:(NSNotification *)notification { 154 | NSUserActivity *userActivity = notification.userInfo[kUserActivityKey]; 155 | if ([userActivity.activityType isEqualToString:CSSearchableItemActionType]) { 156 | NSString *uniqueItemIdentifier = userActivity.userInfo[CSSearchableItemActivityIdentifier]; 157 | if (!uniqueItemIdentifier) 158 | return; 159 | [self sendEventWithName:kSpotlightSearchItemTapped body:uniqueItemIdentifier]; 160 | } else { 161 | [self sendEventWithName:kAppHistorySearchItemTapped body:userActivity.userInfo]; 162 | } 163 | } 164 | 165 | - (void (^)(NSError * _Nullable error))completionBlockWithResolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject { 166 | return ^(NSError * _Nullable error) { 167 | if (error) { 168 | reject(RCTErrorUnspecified, error.localizedDescription, error); 169 | } else { 170 | resolve(nil); 171 | } 172 | }; 173 | } 174 | 175 | - (void)retrieveInitialSearchItemOfType:(NSString *)type bodyBlock:(id (^)(NSUserActivity *))block resolve:(RCTPromiseResolveBlock)resolve { 176 | NSDictionary *userActivityDictionary = self.bridge.launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; 177 | if (!userActivityDictionary) 178 | return resolve([NSNull null]); 179 | NSString *userActivityType = userActivityDictionary[UIApplicationLaunchOptionsUserActivityTypeKey]; 180 | if (![userActivityType isEqualToString:type]) 181 | return resolve([NSNull null]); 182 | NSUserActivity *userActivity = userActivityDictionary[kApplicationLaunchOptionsUserActivityKey]; 183 | resolve(RCTNullIfNil(block(userActivity))); 184 | } 185 | 186 | - (void)createContentAttributeSetFromItem:(NSDictionary *)item withCompletion:(ContentAttributeSetCreationCompletion)completionBlock { 187 | CSSearchableItemAttributeSet *attributeSet = [[CSSearchableItemAttributeSet alloc] initWithItemContentType:(NSString *)kUTTypeJSON]; 188 | attributeSet.title = item.rctsa_title; 189 | attributeSet.contentDescription = item.rctsa_contentDescription; 190 | attributeSet.keywords = item.rctsa_keywords; 191 | if (item.rctsa_thumbnail) { 192 | [self loadImageFromSource:item.rctsa_thumbnail withCompletion:^(NSError *error, UIImage *image) { 193 | if (error || !image) { 194 | return completionBlock(nil, error ?: RCTErrorWithMessage(@"Could not load an image")); 195 | } 196 | attributeSet.thumbnailData = UIImagePNGRepresentation(image); 197 | completionBlock(attributeSet, nil); 198 | }]; 199 | } else { 200 | completionBlock(attributeSet, nil); 201 | } 202 | } 203 | 204 | - (void)loadImageFromSource:(RCTImageSource *)source withCompletion:(RCTImageLoaderCompletionBlock)completionBlock { 205 | [self.bridge.imageLoader loadImageWithURLRequest:source.request 206 | size:source.size 207 | scale:source.scale 208 | clipped:YES 209 | resizeMode:RCTResizeModeStretch 210 | progressBlock:NULL 211 | partialLoadBlock:NULL 212 | completionBlock:completionBlock]; 213 | } 214 | 215 | @end 216 | 217 | --------------------------------------------------------------------------------