Search bar Swift UI with history – SQLite

SQLite is used to store the data locally inside the app in SQL structure. It is a relational local database. You can use this database to store data inside the app using Swift and Swift UI. We will be creating a simple search bar in an iOS app in Swift UI to search animals from an array and save the user’s searched strings in the SQLite database.

We will be using a library called SQLite by Stephen Celis. To install this library, you must have Cocoapods installed in your system.

Installation

You can install Cocoapods in your system by simply running the following command in your terminal:

sudo gem install cocoapods

First, you need to open a command prompt (Terminal) at the root of your XCode project and run the following command in it:

pod init

Now you will see a new file created at the root of your project named Podfile. Open that file in your text editor and add the line to install the library. Following will be the content of your Podfile:

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
 
target 'SQLite_Database' do
  # Comment the next line if you don't want to use dynamic frameworks
  use_frameworks!
 
  # Pods for SQLite_Database
  pod 'SQLite.swift', '~> 0.12.0'
 
end

After that, run the following command in your Terminal to install this library:

pod update

Once installed, close your XCode and re-open but this time double-click on the file which has an extension “.xcworkspace“. This file will be created only after the pod has been installed/updated.

Create a search bar

First, we will create a search bar using TextField. Then, when the user hits submit, we will send the searched value in the next view.

In the second view, we will get the value from the text field of the first view and search that value in an array. Then, display all the records that matched the searched string and display them in the list view.

Create a state variable in your content view:

// variable to goto search view
@State var gotoSearchPage: Bool = false

// value that is searched from the text field
@State var searchedText: String = ""

Following will be the content of your content view’s body:

// live tracking of input field value
let binding = Binding<String>(get: {
    self.searchedText
}, set: { (value) in
    self.searchedText = value
})

// navigation view to goto search view
return NavigationView {

    VStack (alignment: .leading, spacing: 10) {
    
        // navigation link to goto search view
        NavigationLink (destination: SearchView(searchedText: self.$searchedText), isActive: self.$gotoSearchPage) {
            EmptyView()
        }
    
        // search field
        TextField("Search ...", text: binding, onCommit: {
        
            // goto search view when search icon is clicked
            self.gotoSearchPage = true
        
        })
            .padding(7)
            .padding(.horizontal, 5)
            .background(Color(.systemGray6))
            .cornerRadius(8)
            .disableAutocorrection(true)
            .keyboardType(.webSearch)
        
    }
    .padding(20)
    .navigationBarTitle("Search - SQLite")
}

This will create a text field where you can enter your query, on hitting the “search icon” it should take you to the search view which we will create in the next step.

Create a search view

Now, we need to create a search view where we will perform the searching and display the filtered records in a list view.

Create a modal class for Animal:

//
//  Animal.swift
//  Search bar with history
//
//  Created by Adnan Afzal on 02/12/2020.
//  Copyright © 2020 Adnan Afzal. All rights reserved.
//

import Foundation

class Animal: Identifiable {
    var id: Int64 = 0
    var name: String = ""
    
    init() {
        //
    }
    
    init(name: String) {
        self.name = name
    }
}

Create a new file named SearchView.swift and paste the following code in it:

//
//  SearchView.swift
//  Search bar with history
//
//  Created by Adnan Afzal on 02/12/2020.
//  Copyright © 2020 Adnan Afzal. All rights reserved.
//

import SwiftUI

struct SearchView: View {
    
    // get searched value from the previous view
    @Binding var searchedText: String
    
    // a list of all items (change this variable as per your need)
    @State var animals: [Animal] = []
    
    // list of items that matched the searched text
    @State var searchedArray: [Animal] = []
    
    var body: some View {
        
        VStack {
            
            // show searched text
            Text("\(searchedText)").bold()
            
            // show items that matched the searched text in list view
            List (self.searchedArray) { (item) in
                Button(action: {
                    //
                }, label: {
                    Text(item.name)
                })
            }
        }.onAppear(perform: {
        
            // make the items empty when the page loads
            self.animals.removeAll()
            
            // add the data that needs to be searched (you can put your own array items here)
            self.animals.append(Animal(name: "Lion"))
            self.animals.append(Animal(name: "Tiger"))
            self.animals.append(Animal(name: "Rhino"))
            self.animals.append(Animal(name: "Elephant"))
            self.animals.append(Animal(name: "Cheetah"))
            self.animals.append(Animal(name: "Polar bear"))
            self.animals.append(Animal(name: "Leopard"))
            self.animals.append(Animal(name: "Wolf"))
            
            // empty the searched array
            self.searchedArray.removeAll()
            
            // find all the elements that matched the searched string
            for animal in self.animals {
                if (animal.name.lowercased().contains(self.searchedText.lowercased())) {
                    self.searchedArray.append(animal)
                }
            }
        })
        
    }
}

struct SearchView_Previews: PreviewProvider {

    // when using @Binding, use this in preview provider
    @State static var searchedText: String = ""

    static var previews: some View {
        SearchView(searchedText: $searchedText)
    }
}

Comments have been added on each line to explain each line individually. Now, at this point, you will be able to perform the search and see the animals which contain the name field that matches with the text field value you entered in the first view.

Show search history

Now, we need to show the search history in the content view. For that, first, we have to save each searched string in the SQLite database.

Create a separate class named “DB_Manager” that will hold all the functions for the SQLite database.

//
//  DB_Manager.swift
//  Search bar with history
//
//  Created by Adnan Afzal on 02/12/2020.
//  Copyright © 2020 Adnan Afzal. All rights reserved.
//

import Foundation

// import library
import SQLite

class DB_Manager {

    // sqlite instance
    private var db: Connection!
     
    // table instance
    private var animals: Table!
 
    // columns instances of table
    private var id: Expression<Int64>!
    private var name: Expression<String>!
    
    // constructor of this class
    init () {
         
        // exception handling
        do {
             
            // path of document directory
            let path: String = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first ?? ""
 
            // creating database connection
            db = try Connection("\(path)/my_animals.sqlite3")
             
            // creating table object
            animals = Table("animals")
             
            // create instances of each column
            id = Expression<Int64>("id")
            name = Expression<String>("name")
             
            // check if the animal's table is already created
            if (!UserDefaults.standard.bool(forKey: "is_db_created")) {
 
                // if not, then create the table
                try db.run(animals.create { (t) in
                    t.column(id, primaryKey: true)
                    t.column(name)
                })
                 
                // set the value to true, so it will not attempt to create the table again
                UserDefaults.standard.set(true, forKey: "is_db_created")
            }
             
        } catch {
            // show error message if any
            print(error.localizedDescription)
        }
         
    }

    // check if record already exists in SQLite
    public func isExists(searchedText: String) -> Bool {
     
        var isExists: Bool = false
         
        // exception handling
        do {
     
            // get animal using ID
            let animal: AnySequence<Row> = try db.prepare(animals.filter(name.lowercaseString == searchedText.lowercased()))
     
            // get row
            animal.forEach({ (rowValue) in
                isExists = true
            })
            
        } catch {
            print(error.localizedDescription)
        }
     
        return isExists
    }
    
    // add a new row in SQLite
    public func addAnimal(nameValue: String) {
        do {
            try db.run(animals.insert(name <- nameValue))
        } catch {
            print(error.localizedDescription)
        }
    }
    
}

Comments have been added with each line for an explanation. Now, in your search view inside the onAppear() function, check if the searched string already exists in the SQLite database. If NOT, then add the searched text in the SQLite database.

// add the searched text in SQLite database if NOT exists
let isExists: Bool = DB_Manager().isExists(searchedText: self.searchedText)
if (!isExists) {
    DB_Manager().addAnimal(nameValue: self.searchedText)
}

Now, we need to show the search history in the main content view when the user clicks on the search bar text field. So, create 2 state wrapper properties in your content view:

// variable to show search history view
@State var showSearchHistoryView: Bool = false

// array of history array
@State var history: [Animal] = []

And inside the body, we need to track the live text change of the input field. We have already created a binding variable inside the body of the content view. Change the binding variable inside the content view body to the following:

// live tracking of input field value
let binding = Binding<String>(get: {
    self.searchedText
}, set: { (value) in
    self.searchedText = value
    
    // show history if the text field is not empty
    self.showSearchHistoryView = !self.searchedText.isEmpty
})

Now, when the showSearchHistoryView variable is true, we need to display a list of all previously searched strings. So, create a list view under an if condition below the search bar text field:

// show history view only when a variable is true
if (self.showSearchHistoryView) {
    
    // create list view to show all history items
    List (self.history) { (model) in
    
        // show history text
        HStack {
        
            Image(systemName: "arrow.counterclockwise")
        
            Button(action: {
                self.searchedText = model.name
                self.gotoSearchPage = true
            }, label: {
                Text(model.name)
            })

        }
    }
}

This will display a counter clock icon that represents that it is data from history, a button that shows the previously searched string, and which when clicked will move to the search view.

Now, we need to load the data in our history array. So, attach an onAppear() event to your VStack and fetch the data from the SQLite database and save it in the history array.

// load data in history models array
.onAppear(perform: {
    self.history = DB_Manager().getAnimals()
})

Now, we need to create a function named getAnimals() in our DB_Manager class that will fetch the records from the SQLite database.

// return array of animal models
public func getAnimals() -> [Animal] {
     
    // create empty array
    var animalModels: [Animal] = []
 
    // get all animals in descending order
    animals = animals.order(id.desc)
 
    // exception handling
    do {
 
        // loop through all animals
        for animal in try db.prepare(animals) {
 
            // create new model in each loop iteration
            let animalModel: Animal = Animal()
 
            // set values in model from database
            animalModel.id = animal[id]
            animalModel.name = animal[name]
 
            // append in new array
            animalModels.append(animalModel)
        }
    } catch {
        print(error.localizedDescription)
    }
 
    // return array
    return animalModels
}

Comments have been added with each line for an explanation. If you run the code now, you will be able to view all the searched strings in a list view, upon click it will take you to the search view screen.

At this point, the search bar in Swift UI is completed. But you can go further and add the delete functionality. So user can remove his searched queries.

Delete history

Now, we need a function to delete the string from search history. First, create a button inside your history list after the counter-clock icon and button in your content view:

... counter-clock icon and button

// show delete button
Spacer()
Button(action: {
    
    // delete history item from SQLite
    DB_Manager().deleteAnimal(idValue: model.id)
    
    // refresh the list view
    self.history = DB_Manager().getAnimals()
    
}, label: {
    Text("Delete")
        .foregroundColor(Color.red)
})// by default, buttons are full width.
// to prevent this, use the following
.buttonStyle(PlainButtonStyle())

Now, we need to create a function named deleteAnimal() in our DB_Manager class that will delete the row from the SQLite database.

// function to delete animal from search history
public func deleteAnimal(idValue: Int64) {
    do {
    
        // get animal using ID
        let animal: Table = animals.filter(id == idValue)
         
        // run the delete query
        try db.run(animal.delete())
        
    } catch {
        print(error.localizedDescription)
    }
}

Run the code now, and you will be able to see a delete button at the end of each history row. Upon clicking, it will remove the row from the SQLite database and from the list view as well.

So you have successfully created a search bar in Swift UI along with search history.

You can also learn to create a UICollectionView with a search bar from the following tutorial.

UICollectionview with Search Bar – Swift iOS

[wpdm_package id=’929′]