Build an InstantSearch iOS page from start to finish using SwiftUI.
This guide describes how to start a SwiftUI project with InstantSearch iOS and create a full search experience from scratch.This search experience includes:
In Xcode, create a new Project:Open Xcode, and select File -> New -> Project in the menu bar.Select iOS -> App template and click Next.Give your application a name. Make sure that you have selected SwiftUI option in the Interface field and click Next.You should see the ContentView.swift file opened with a Hello World project and the live preview canvas.
This tutorial uses Swift Package Manager to integrate the InstantSearch library.
If you prefer to use another dependency manager (Cocoapods, Carthage) please checkout the corresponding installation guides for InstantSearch.In the menu bar select File -> Swift Packages -> Add Package Dependency.Paste the GitHub link for the InstantSearch library: https://github.com/algolia/instantsearch-iosPick the latest library version on the next screen, and select the InstantSearchSwiftUI product from the following list:The InstantSearch dependency is installed and you’re all set to work on your application.
Start by creating a classic search interface with search bar and results list. In your Xcode project, open the ContentView.swift file and import the InstantSearch library.
Define a structure that represent a record in your index. For simplicity’s sake, the structure only provides the name of the product.
It must conform to the Codable protocol to work with InstantSearch. Add the following structure definition to the ContentView.swift file:
Add the AlgoliaController class containing the InstantSearch business logic components to the ContentView.swift file.You need three components coupled with the corresponding UI controllers for the basic search experience:
HitsSearcher performs search requests and obtains search results.
SearchBoxInteractor handles a textual query input and triggers search requests when needed.
HitsInteractor stores hits and manages the pagination logic.
The setupConnections method establishes the connections between these components and their UI controllers to make them work together seamlessly.
Swift
Report incorrect code
Copy
class AlgoliaController { let searcher: HitsSearcher let searchBoxInteractor: SearchBoxInteractor let searchBoxController: SearchBoxObservableController let hitsInteractor: HitsInteractor<StockItem> let hitsController: HitsObservableController<StockItem> init() { self.searcher = HitsSearcher(appID: "latency", apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db", indexName: "bestbuy") self.searchBoxInteractor = .init() self.searchBoxController = .init() self.hitsInteractor = .init() self.hitsController = .init() setupConnections() } func setupConnections() { searchBoxInteractor.connectSearcher(searcher) searchBoxInteractor.connectController(searchBoxController) hitsInteractor.connectSearcher(searcher) hitsInteractor.connectController(hitsController) }}
The business logic is all set. It’s time to work on the UI. Focus on the ContentView structure declaration.
Add SearchBoxObservableController and HitsObservableController properties to the ContentView structure with an @ObservedObject property wrapper, so the view is automatically notified when the state of the search text or the hits list changed.
Swift
Report incorrect code
Copy
struct ContentView: View { @ObservedObject var searchBoxController: SearchBoxObservableController @ObservedObject var hitsController: HitsObservableController<StockItem> var body: some View { Text("Hello, world!") .padding() }}
Add the isEditing property that binds the editing state of the search bar.
Swift
Report incorrect code
Copy
struct ContentView: View { @ObservedObject var searchBoxController: SearchBoxObservableController @ObservedObject var hitsController: HitsObservableController<StockItem> @State private var isEditing = false var body: some View { Text("Hello, world!") .padding() }}
Then alter the body declaration.
Replace the “Hello, world!” text view with a vertical stack containing the SearchBar configured with SearchBoxObservableController properties and isEditing state binding.
Swift
Report incorrect code
Copy
var body: some View { VStack(spacing: 7) { SearchBar(text: $searchBoxController.query, isEditing: $isEditing, onSubmit: searchBoxController.submit) }}
Starting from iOS 15 you can use the searchable modifier to add a system search field.
You should embed your search view into the NavigationView to ensure the search input field appears.
The body of your view will look as follows:
Swift
Report incorrect code
Copy
var body: some View { VStack(spacing: 7) { }.searchable(text: $searchBoxController.query)}
The remainder of this guide relies on an explicit SearchBar view (as a more versatile approach).Insert the HitsList component configured with HitsObservableController and a closure constructing the hit row.
The hit row is represented by a vertical stack with a text block presenting the name of the item and a Divider.
Complete your search experience with a noResults trailing closure in the HitsList, that constructs the view presented in case of an empty result set, and add the navigationBarTitle string to show the navigation header on top of you search screen.
The business logic and view are ready. Connect them and try out the search experience in the live preview.
Add a static instance of the AlgoliaController in the PreviewProvider.
Then, in the previews declaration, instantiate the ContentView with the UI controller references in the AlgoliaController class, and embed it in the NavigationView.
Launch the initial search inside the onAppear closure of the NavigationView.
The resulting ContentView_Previews structure content should look as follows:
Swift
Report incorrect code
Copy
struct ContentView_Previews: PreviewProvider { static let algoliaController = AlgoliaController() static var previews: some View { NavigationView { ContentView(searchBoxController: algoliaController.searchBoxController, hitsController: algoliaController.hitsController) }.onAppear { algoliaController.searcher.search() } }}
Launch your preview to see the basic search experience in action.
You should see that the results are changing on each key stroke.
To make the search experience more user-friendly, you can give more context about the search results to your users.
You can do this with different InstantSearch modules.
First, add a statistics component.
This component shows the hit count and the request processing time.
This gives users a complete understanding of their search, without the need for extra interaction.
Then create a StatsInteractor, which extracts the metadata from the search response, and provides an interface to present it to users.
Add StatsInteractor and StatsTextObservableController to AlgoliaController and connect the StatsInteractor to the Searcher in the setupConnections method.
Complete the setupConnections method of the PreviewProvider with the connection between the StatsInteractor and the StatsTextObservableController.
Swift
Report incorrect code
Copy
class AlgoliaController { let searcher: HitsSearcher let searchBoxInteractor: SearchBoxInteractor let searchBoxController: SearchBoxObservableController let hitsInteractor: HitsInteractor<StockItem> let hitsController: HitsObservableController<StockItem> let statsInteractor: StatsInteractor let statsController: StatsTextObservableController init() { self.searcher = HitsSearcher(appID: "latency", apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db", indexName: "bestbuy") self.searchBoxInteractor = .init() self.hitsInteractor = .init() self.statsInteractor = .init() self.searchBoxController = .init() self.hitsController = .init() self.statsController = .init() setupConnections() } func setupConnections() { searchBoxInteractor.connectSearcher(searcher) searchBoxInteractor.connectController(searchBoxController) hitsInteractor.connectSearcher(searcher) hitsInteractor.connectController(hitsController) statsInteractor.connectSearcher(searcher) statsInteractor.connectController(statsController) }}
The StatsInteractor receives the search statistics now, but doesn’t display it yet.
Add StatsTextObservableController property to the ContentView and add the Text with its stats property into the stack in the middle of the SearchBar and the HitsList.
Swift
Report incorrect code
Copy
struct ContentView: View { @ObservedObject var searchBoxController: SearchBoxObservableController @ObservedObject var hitsController: HitsObservableController<StockItem> @ObservedObject var statsController: StatsTextObservableController @State private var isEditing = false var body: some View { VStack(spacing: 7) { SearchBar(text: $searchBoxController.query, isEditing: $isEditing, onSubmit: searchBoxController.submit) Text(statsController.stats) .fontWeight(.medium) HitsList(hitsController) { (hit, _) in VStack(alignment: .leading, spacing: 10) { Text(hit?.name ?? "") .padding(.all, /*@START_MENU_TOKEN@*/10/*@END_MENU_TOKEN@*/) Divider() } } noResults: { Text("No Results") .frame(maxWidth: .infinity, maxHeight: .infinity) } } .navigationBarTitle("Algolia & SwiftUI") }}
Alter the PreviewProvider structure by adding a StatsTextObservableController in the ContentView initializer.
Swift
Report incorrect code
Copy
struct ContentView_Previews: PreviewProvider { static let algoliaController = AlgoliaController() static var previews: some View { NavigationView { ContentView(searchBoxController: algoliaController.searchBoxController, hitsController: algoliaController.hitsController, statsController: algoliaController.statsController) }.onAppear { algoliaController.searcher.search() } }}
Update your live preview.
You should now see updated results and an updated hit count on each keystroke.
With your app, you can search more than 10,000 products. However, you don’t want to scroll to the bottom of the list to find the exact product you’re looking for. One can more accurately filter the results by making use of the RefinementList components. This section explains how to build a filter that allows to filter products by their category.
Add a FilterState component to the AlgoliaController. This component provides a convenient way to manage the state of your filters. Add the manufacturer refinement attribute.
Add the FacetListInteractor, which stores the list of facets retrieved with search results.
Add the connections between HitsSearcher, FilterState and FacetListInteractor in the setupConnections method.
Complete the setupConnections method of the PreviewProvider with the connection between the FacetListInteractor and the FacetListObservableController. To improve the user experience, the connection includes the FacetListPresenter parameter that pins the selected facets to the top of the list and uses facet count value as the second ranking criteria, so that the facets with the most hits show up higher.
Swift
Report incorrect code
Copy
class AlgoliaController { let searcher: HitsSearcher let searchBoxInteractor: SearchBoxInteractor let searchBoxController: SearchBoxObservableController let hitsInteractor: HitsInteractor<StockItem> let hitsController: HitsObservableController<StockItem> let statsInteractor: StatsInteractor let statsController: StatsTextObservableController let filterState: FilterState let facetListInteractor: FacetListInteractor let facetListController: FacetListObservableController init() { self.searcher = HitsSearcher(appID: "latency", apiKey: "1f6fd3a6fb973cb08419fe7d288fa4db", indexName: "bestbuy") self.searchBoxInteractor = .init() self.searchBoxController = .init() self.hitsInteractor = .init() self.hitsController = .init() self.statsInteractor = .init() self.statsController = .init() self.filterState = .init() self.facetListInteractor = .init() self.facetListController = .init() setupConnections() } func setupConnections() { searchBoxInteractor.connectSearcher(searcher) searchBoxInteractor.connectController(searchBoxController) hitsInteractor.connectSearcher(searcher) hitsInteractor.connectController(hitsController) statsInteractor.connectSearcher(searcher) statsInteractor.connectController(statsController) searcher.connectFilterState(filterState) facetListInteractor.connectSearcher(searcher, with: "manufacturer") facetListInteractor.connectFilterState(filterState, with: "manufacturer", operator: .or) facetListInteractor.connectController(facetListController, with: FacetListPresenter(sortBy: [.isRefined, .count(order: .descending)])) }}
In the ContentView add a new state flag isPresentingFacets, which defines if the facet list is presented.
Swift
Report incorrect code
Copy
@State private var isPresentingFacets = false
Then declare a function that constructs the button which triggers the appearance of the facet list by toggling the isPresentingFacets flag.
Complete the initializer of the ContentView with the FacetListObservableController.
Swift
Report incorrect code
Copy
struct ContentView_Previews: PreviewProvider { static let algoliaController = AlgoliaController() static var previews: some View { NavigationView { ContentView(searchBoxController: algoliaController.searchBoxController, hitsController: algoliaController.hitsController, statsController: algoliaController.statsController, facetListController: algoliaController.facetListController) }.onAppear { algoliaController.searcher.search() } }}
Update the live preview. Now you see a filter button on top right of your screen.
Click it to present the refinements list, select one or more refinements,
and then dismiss the refinements list by swiping it down to see the filtered result.
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. This is a great start, but you can go even further and improve on that.