-
Notifications
You must be signed in to change notification settings - Fork 60
/
Copy pathGettingStartedSearchViewController.swift
438 lines (361 loc) · 20.2 KB
/
GettingStartedSearchViewController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
//
// GettingStartedSearchViewController.swift
// DemoDirectory
//
// Created by Vladislav Fitc on 29/07/2020.
// Copyright © 2020 Algolia. All rights reserved.
//
// swiftlint:disable file_length line_length unused_optional_binding orphaned_doc_comment
import Foundation
import InstantSearch
import UIKit
enum GettingStartedGuide {
enum StepOne {}
enum StepTwo {}
enum StepThree {}
enum StepFour {}
enum StepFive {}
enum StepSix {}
enum StepSeven {}
}
/**
# Getting Started
## Welcome to InstantSearch iOS
In this guide, we will walk through the few steps needed to start a project with InstantSearch iOS. We will start from an empty iOS project, and create a full search experience from scratch!
## Before we start
To use InstantSearch iOS, you need an Algolia account. You can create a new account, or use the following credentials:
- APP ID: latency
- Search API Key: 1f6fd3a6fb973cb08419fe7d288fa4db
- Index name: bestbuy
These credentials give access to a preloaded dataset of products appropriate for this guide.
## Create a new project
Let’s get started! In Xcode, create a new Project:
On the Template screen, select Single View Application and click next
Specify your Product name, select Swift as the language and iPhone as the Device, and then create.
## Add InstantSearch to the project
To add InstantSearch package dependency to your Xcode project, you need a dependency manager.
### Swift Package Manager
- Select File > Swift Packages > Add Package Dependency and enter repository URL: https://github.com/algolia/instantsearch-ios
- You can also navigate to your target’s General pane, and in the “Frameworks, Libraries, and Embedded Content” section, click the + button, select Add Other, and choose Add Package Dependency.
- In the package products selection dialog, checkmark both `InstantSearch` and `InstantSearchCore` products
### Cocoapods
- If you don’t have CocoaPods installed on your machine, open your terminal and run sudo gem install cocoapods.
- Go to the root of your project then type pod init. A Podfile will be created for you.
- Open your Podfile and add pod 'InstantSearch', '~> 7' below your target.
- On your terminal, run pod update.
- Close your Xcode project, and then, at the root of your project, type open projectName.xcworkspace (replacing projectName with the actual name of your project).
## Let's start!
Open ViewController.swift which automatically generated by Xcode when you create a Single View Application.
Add `import InstantSearch` at the top.
## Define your record structure
First of all let's define a structure representing a record in your index. For the sake of simplicity, our structure will only provide the name of the product.
Structure must conform to `Codable` protocol to work properly with `InstantSearch`. Add the following declaration to ViewController.swift file.
*/
/**
struct Item: Codable {
let name: String
}
*/
/**
## Hits view controller
In this example we use `UITableViewController` which conforms to `HitsController` protocol.
This implementation defines how to bind the record's data to the `UITableViewCell` instance.
The following implementation binds the name of the fetched item to textLabel's text property of the cell.
*/
class SearchResultsViewController: UITableViewController, HitsController {
var hitsSource: HitsInteractor<Item>?
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
}
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
hitsSource?.numberOfHits() ?? 0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = hitsSource?.hit(atIndex: indexPath.row)?.name
return cell
}
override func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) {
if let _ = hitsSource?.hit(atIndex: indexPath.row) {
// Handle hit selection
}
}
}
/**
## Fill the ViewController
Let's complete the main view controller of our application. Declare a search controller which is a UIKit component managing the display of search results based on interactions with a search bar. It already contains a search bar inside and just requires a search results controller as a parameter. Let's add `hitsViewController` field to our view controller of type declared in the previous step. Set it as a initializer parameter of the search controller.
*/
extension GettingStartedGuide.StepOne {
class ViewController: UIViewController {
lazy var searchController = UISearchController(searchResultsController: hitsViewController)
let hitsViewController = SearchResultsViewController()
}
}
/**
## Initialize your searcher
We have the necessary view controllers, now it's time to add some search logic.
The central part of our search experience is the Searcher. The Searcher performs search requests and obtains search results. Almost all InstantSearch components are connected with the Searcher. In this tutorial we only target one index, so we will instantiate a HitsSearcher with the proper credentials.
Then add `searchConnector` property to view controller. Initialize it passing searcher, search controller and hits view controller. In the end, add activate the search connector by calling its `connect()` and then `searcher.search()` to launch the first empty search request immediately.
*/
extension GettingStartedGuide.StepTwo {
class ViewController: UIViewController {
lazy var searchController = UISearchController(searchResultsController: hitsViewController)
let hitsViewController = SearchResultsViewController()
let searcher = HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
lazy var searchConnector = SearchConnector<Item>(searcher: searcher,
searchController: searchController,
hitsInteractor: .init(),
hitsController: hitsViewController)
override func viewDidLoad() {
super.viewDidLoad()
searchConnector.connect()
searcher.search()
}
}
}
/**
The search logic is fully functional now, but it's not ready to use yet. We need a few more lines of code to setup the UI appearance. Add `setupUI` function to the view controller and call it from the `viewDidLoad` method. Finally, let's override `viewDidAppear` function and set search controller active so that the search controller present results immediately after the view controller appearance. We are all set now. Build and run your project and you will see the basic search experience with instant results!
*/
extension GettingStartedGuide.StepThree {
class ViewController: UIViewController {
lazy var searchController = UISearchController(searchResultsController: hitsViewController)
let hitsViewController = SearchResultsViewController()
let searcher = HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
lazy var searchConnector = SearchConnector<Item>(searcher: searcher,
searchController: searchController,
hitsInteractor: .init(),
hitsController: hitsViewController)
override func viewDidLoad() {
super.viewDidLoad()
searchConnector.connect()
searcher.search()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
}
func setupUI() {
view.backgroundColor = .white
navigationItem.searchController = searchController
searchController.hidesNavigationBarDuringPresentation = false
searchController.showsSearchResultsController = true
searchController.automaticallyShowsCancelButton = false
}
}
}
/**
## Add Stats
Now let's make our search experience more user-friendly by providing an additional feedback about search results. Along the way, you will discover how extend your search experience with different InstantSearch modules. Showing the hits count gives the user a complete undestanding about search results immediately without need of additional interaction. Let's add a `Stats` components. Stats interactor extracts search meta-data from the response and provides an interface to present it to the user.
- Add stats interactor to view controller
- Connect stats interactor to searcher using `connectSearcher` method
*/
extension GettingStartedGuide.StepFour {
class ViewController: UIViewController {
lazy var searchController = UISearchController(searchResultsController: hitsViewController)
let hitsViewController = SearchResultsViewController()
let searcher = HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
lazy var searchConnector = SearchConnector<Item>(searcher: searcher,
searchController: searchController,
hitsInteractor: .init(),
hitsController: hitsViewController)
let statsInteractor = StatsInteractor()
override func viewDidLoad() {
super.viewDidLoad()
searchConnector.connect()
statsInteractor.connectSearcher(searcher)
searcher.search()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
}
func setupUI() {
view.backgroundColor = .white
navigationItem.searchController = searchController
searchController.hidesNavigationBarDuringPresentation = false
searchController.showsSearchResultsController = true
searchController.automaticallyShowsCancelButton = false
}
}
}
/**
Well done, now stats interactor receives the search statistics data. However it shoes them nowhere. Let's fix that!
To make it simple, we will present hits count as the title of the view controller. Perhaps it's not the best place to show it in the interface, but this prevents putting to much layout-related code in this tutorial.
Stats interactor present its data in the component implementing `StatsTextController` protocol.
Make your view controller conform to this protocol by adding an extension. Now the view controller can be connected to the stats interactor with the corresponding method. Add this connection in the `viewDidLoad` method.
*/
extension GettingStartedGuide.StepFive {
class ViewController: UIViewController {
lazy var searchController = UISearchController(searchResultsController: hitsTableViewController)
let hitsTableViewController = SearchResultsViewController()
let searcher = HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
lazy var searchConnector = SearchConnector<Item>(searcher: searcher,
searchController: searchController,
hitsInteractor: .init(),
hitsController: hitsTableViewController)
let statsInteractor = StatsInteractor()
override func viewDidLoad() {
super.viewDidLoad()
searchConnector.connect()
statsInteractor.connectSearcher(searcher)
statsInteractor.connectController(self)
searcher.search()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
}
func setupUI() {
view.backgroundColor = .white
navigationItem.searchController = searchController
searchController.hidesNavigationBarDuringPresentation = false
searchController.showsSearchResultsController = true
searchController.automaticallyShowsCancelButton = false
}
}
}
extension GettingStartedGuide.StepFive.ViewController: StatsTextController {
func setItem(_ item: String?) {
title = item
}
}
/**
Build and run your application: on each keystroke the updated search results count is shown. Now you have an idea of how InstantSearch modules are organized.
- Each module has an `Interactor` containing a business-logic of the module.
- Each `Interactor` has a corresponding `Controller` protocol defining the interaction with a UI component.
`InstantSearch` provides a few basic implementations of `Controller` protocol for `UIKit` components such as `HitsTableViewController`, `TextFieldController`, `ActivityIndicatorController`. Feel free to use them to discover the abilities of `InstantSearch` with minimal effort. In your own project you might want implement more custom UI and behaviour. So, it's up to you to create an implementations of `Controller` protocol and to connect them to a corresponding interactors.
*/
/** ## Filter your results: RefinementList
With your app, you can search more than 10000 products. However, you don’t want to scroll to the bottom of the list to find the exact product you’re looking for. We can more accurately filter our results by making use of the RefinementList components. We’ll build a filter that allows us to filter products by their category.
First of all, add a FilterState. This component provide a convenient way to manage the state of your filters. In our case, we will add one refinement attribute: `category`. Finally we have to add the `RefinementList` components to other components in our search experience, such as `FacetListConnector`, `FacetListTableController` and `UITableViewController`. The `UITableViewController` will actually present a facet list. As a result, property definitions of your `ViewController` must look like this:
*/
extension GettingStartedGuide.StepSix {
class ViewController: UIViewController {
lazy var searchController = UISearchController(searchResultsController: hitsViewController)
let hitsViewController = SearchResultsViewController()
let searcher = HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
lazy var searchConnector = SearchConnector<Item>(searcher: searcher,
searchController: searchController,
hitsInteractor: .init(),
hitsController: hitsViewController,
filterState: filterState)
let statsInteractor = StatsInteractor()
let filterState = FilterState()
lazy var categoryConnector = FacetListConnector(searcher: searcher,
filterState: filterState,
attribute: "category",
operator: .and,
controller: categoryListController)
lazy var categoryListController = FacetListTableController(tableView: categoryTableViewController.tableView)
let categoryTableViewController = UITableViewController()
override func viewDidLoad() {
super.viewDidLoad()
searchConnector.connect()
categoryConnector.connect()
statsInteractor.connectSearcher(searcher)
statsInteractor.connectController(self)
searcher.search()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
}
func setupUI() {
view.backgroundColor = .white
navigationItem.searchController = searchController
searchController.hidesNavigationBarDuringPresentation = false
searchController.showsSearchResultsController = true
searchController.automaticallyShowsCancelButton = false
}
}
}
extension GettingStartedGuide.StepSix.ViewController: StatsTextController {
func setItem(_ item: String?) {
title = item
}
}
/**
Finally in the `setupUI()` method setup a navigation bar button item which will trigger the presentation of facet list and set the title of this list.
Add `showFilters` and `dismissFilters` functions responsible for the presentation and dismiss logic of the facet list.
*/
extension GettingStartedGuide.StepSeven {
class ViewController: UIViewController {
lazy var searchController = UISearchController(searchResultsController: hitsViewController)
let hitsViewController = SearchResultsViewController()
let searcher = HitsSearcher(appID: "latency",
apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db",
indexName: "bestbuy")
lazy var searchConnector = SearchConnector<Item>(searcher: searcher,
searchController: searchController,
hitsInteractor: .init(),
hitsController: hitsViewController,
filterState: filterState)
let statsInteractor = StatsInteractor()
let filterState = FilterState()
lazy var categoryConnector = FacetListConnector(searcher: searcher,
filterState: filterState,
attribute: "category",
operator: .and,
controller: categoryListController)
lazy var categoryListController = FacetListTableController(tableView: categoryTableViewController.tableView)
let categoryTableViewController = UITableViewController()
override func viewDidLoad() {
super.viewDidLoad()
searchConnector.connect()
categoryConnector.connect()
statsInteractor.connectSearcher(searcher)
statsInteractor.connectController(self)
searcher.search()
setupUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
searchController.isActive = true
}
func setupUI() {
view.backgroundColor = .white
navigationItem.searchController = searchController
navigationItem.rightBarButtonItem = .init(title: "Category", style: .plain, target: self, action: #selector(showFilters))
searchController.hidesNavigationBarDuringPresentation = false
searchController.showsSearchResultsController = true
searchController.automaticallyShowsCancelButton = false
categoryTableViewController.title = "Category"
}
@objc func showFilters() {
let navigationController = UINavigationController(rootViewController: categoryTableViewController)
categoryTableViewController.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(dismissFilters))
present(navigationController, animated: true, completion: .none)
}
@objc func dismissFilters() {
categoryTableViewController.navigationController?.dismiss(animated: true, completion: .none)
}
}
}
extension GettingStartedGuide.StepSeven.ViewController: StatsTextController {
func setItem(_ item: String?) {
title = item
}
}
/**
You can now build and run your application: you now have a search experience with filtering using the `RefinementList`!
## Going further
Your users can enter a query, and your application shows them results as they type. It also provides a possibility to filter the results even further using `RefinementList`. That is pretty nice already! However, we can go further and improve on that.
You can have a look at our examples to see more complex examples of applications built with `InstantSearch`.
You can head to our components page to see other components that you could use.
*/
// swiftlint:enable file_length line_length unused_optional_binding orphaned_doc_comment