Search bar implementation

Hi There,

I recently completed the Apple Swift UI Tutorial Series and was wondering how to implement a search function in a new tab view?
I tried following this tutorial but was unsuccessful.

Any help would be appreciated,

Thanks,

Josh

Josh,

It’s not possible to give you advice on what you need to fix the issue if you do not include the code you currently have. Can you copy the code in your View and paste it in a reply. Remember to format the code block by selecting all of the code block and tapping on the </> option in the toolbar. This wraps the code block with 3 back-ticks ``` on the line above the code block and 3 back-ticks ``` on the line below it.

Sorry for that,

This is my search view

import SwiftUI

struct SearchView: View {

@State private var searchText = ""

var product: Product

var body: some View {

NavigationStack {

List {

ForEach(searchResults, id: \.self) { name in

NavigationLink {

Text(name)

} label: {

Text(name)

}

}

}

.navigationTitle("Search")

.navigationBarTitleDisplayMode(.inline)

}

.searchable(text: $searchText, prompt: "Tap to Search")

}

var searchResults: [String] {

if searchText.isEmpty {

return [product].name

} else {

return product.name.filter { $0.contains(searchText) }

}

}

}

struct SearchView_Previews: PreviewProvider {

static var previews: some View {

SearchView()

}

}

I also have a Product file

import Foundation

import SwiftUI

struct Product: Hashable, Codable, Identifiable {

var id: Int

var name: String

var flavour: String

var description: String

var category: Category

enum Category: String, CaseIterable, Codable {

case drink = "Drinks"

case yoghurt = "Yoghurts"

}

private var imageName: String

var image: Image {

Image(imageName)

}

}

A Model Data file

import Foundation

var products: [Product] = load("ProductData.json")

func load<T: Decodable>(_ filename: String) -> T {

let data: Data

guard let file = Bundle.main.url(forResource: filename, withExtension: nil)

else {

fatalError("Couldn't find \(filename) in main bundle.")

}

do {

data = try Data(contentsOf: file)

} catch {

fatalError("Couldn't load \(filename) from main bundle:\n\(error)")

}

do {

let decoder = JSONDecoder()

return try decoder.decode(T.self, from: data)

} catch {

fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")

}

}

And a .json file with formatted like this

[

{

"name": "_",

"category": "_",

"id": 1001,

"flavour": "_",

},

"description": "_",

"imageName": "_"

}
]

My goal is for the search view to search the .json file’s name and flavour field’s for a matching result.

Sorry about before,

Josh

@The-Wolf

Hi Josh,

The code you have in SearchView (courtesy of Paul Hudson) is relevant to a simple array of words but what you are doing is defining a struct to represent the json code you have included.

Your struct needs to be modified to fit work with the json code you have specified but the json code itself also needs to be changed to work with the struct.

Your json refers to an imageName and that means that imageName cannot be defined as private otherwise you can’t access the property in your View.

With that said this is what your Data struct needs to look like:

import Foundation
import SwiftUI

struct Product: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var flavour: String
    var description: String
    var category: Category
    enum Category: String, CaseIterable, Codable {
        case drink = "Drink"
        case yoghurt = "Yoghurt"
    }
    var imageName: String
    private var image: Image {
        Image(imageName)
    }
}

I have modified your json code and included some code that you can uses to test the search function.

[
    {
        "id": 1001,
        "name": "Pepsi Max",
        "category": "Drink",
        "flavour": "Cola",
        "description": "Awesome tasting stuff",
        "imageName": "pepsimax"
    },
    {
        "id": 1002,
        "name": "Lemon Sprite",
        "category": "Drink",
        "flavour": "Lemon",
        "description": "Awesome tasting stuff",
        "imageName": "sprite"
    },
    {
        "id": 1003,
        "name": "Yoplait Whips",
        "category": "Yoghurt",
        "flavour": "Chocolate",
        "description": "Awesome tasting stuff",
        "imageName": "yoplaitwhips"
    }
]

Your DataModel wont work in the format that you have it. You can’t declare a variable and assign it to the result of a function in the same breath.

Your load function has been defined at a global level as has the products variable. Both need to be in a class which conforms to ObservableObject so this is the way it should be set up.

import Foundation

class DataModel: ObservableObject {
    @Published var products = [Product]()

    init() {
        products = load("ProductData.json")
    }

    func load<T: Decodable>(_ filename: String) -> T {
        let data: Data
        guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
        else {
            fatalError("Couldn't find \(filename) in main bundle.")
        }
        do {
            data = try Data(contentsOf: file)
        } catch {
            fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
        }
        do {
            let decoder = JSONDecoder()
            return try decoder.decode(T.self, from: data)
        } catch {
            fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
        }
    }
}

Note that products is now a @Published variable named products and is an array of type Product. It is initially declared as an empty array. When a Published item is changed it broadcasts to any View that relies on it causing the View to be redrawn and thus displaying any change that has occurred.

The init() function is run as soon as the DataModel is accessed and what that now does is call the load function and pass the name of the json file to that function which processes the json code and returns the array of data which is assigned to products.

So let’s take a look at the revised SearchView.

import SwiftUI

struct SearchView: View {
    @StateObject var model = DataModel()
    @State private var searchText = ""
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(filteredProducts, id: \.self) { product in
                    NavigationLink {
                        Text(product.name)
                    } label: {
                        Text(product.name)
                    }
                }
            }
            .navigationTitle("Search")
            .navigationBarTitleDisplayMode(.inline)
        }
        .searchable(text: $searchText, prompt: "Tap to Search")
    }

    var filteredProducts: [Product] {
        if searchText.isEmpty {
            return model.products
        } else {
            return model.products.filter {
                $0.name.lowercased().contains(searchText.lowercased()) ||
                $0.flavour.lowercased().contains(searchText.lowercased())}
        }
    }
}

struct SearchView_Previews: PreviewProvider {
    static var previews: some View {
        SearchView()
    }
}

The first thing to note is the declaration @StateObject var model = DataModel(). This provides access to the DataModel and therefore the @Published property products.

The variable filteredProducts is what is known as a computed property. I changed it to filteredProducts since that more accurately describes what the resulting data is. Computed properties are very handy once you understand how they work. If you are not sure what they do then read on otherwise jump to the next paragraph. A computed property declares a type and in this case an array of Product. The code inside the closure { } returns the required result based on the searchText. If the searchText is empty then provide all the array items from products. We refer to model, which is the reference to DataModel and then using dot notation .products which is the array of data from the json code. If there IS searchText then return the array of products filtered such that if any part of either name or flavour .contains the searchText then they are selected. The comparison is done with both the searchText and the name or the flavour converted to lowercase.

Back to the body property code.

Since we are processing an array of products, with the ForEach then each element of the array is a product so our ForEach looks like this:
ForEach(filteredProducts, id: \.self) { product in

I hope all of this makes sense.

1 Like