Search code examples
swiftlistviewswiftui

How to Protect Filter Settings When Searching in a List in SwiftUI


I was trying to implement a filter to my List in my app. Actually I just used the same code when I was searching. It works really well but after the filtering, when I start to search with the filtered results. It gives me non-filtered results. I'm going to explain with screenshots:

This is the screenshot of filtered result. I do the filtering with "Filter" button on the right side. I filtered the results only contains "Halkalı" and the results are true.

Filtered Results

And this the screenshot of the situation that I have when I was filtering. When I start to search with filtered results, it destroys the filtered results. I didn't put "Halkalı" value for "EN 10305-5" and "DIN Norm Sample" but it shows that too. And when I cancel the search, it shows all the results without filter.

The Search Destroys The Filtered Results

Actually, I tried to do something but I failed. I would like to have your help as a newbie in SwiftUI. I'm going to put my codes below for inspection. Thank you all in advance.

Codes in Main Page:

//  Created by Caner Altuner on 31.07.2023.
//

import SwiftUI

struct NormCatalogView: View {

//To access to the model files
@State private var normsPlant: [NormModel] = allNorms
@State private var normsSearch: [NormModel] = allNorms

//The variable for search
@State private var searchText: String = ""

@State private var filterPlant: String = ""

//Boolean for the alert
@State private var showAlert = false

var body: some View {
    NavigationView {
        VStack {
            List {
                //ForEach ile bu işlemi daha basit bir yolla structlar içinde yer alan elemanları liste içine getirebiliyoruz
                ForEach(normsSearch) {norm in
                    //Normları bölümlere göre ayırlmak için Section elementini kullanıyoruz
                    Section(header: Text(norm.title)) {
                        ForEach(norm.elements) { element in
                            //Sonraki sayfaya geçişi sağlayan bölüm
                            NavigationLink(destination: NormDetailsView(chosenNormElement: element)) {
                                //İsimleri listeye aktardığımız yer
                                Text(element.name)
                            }
                        }
                    }
                }
                //En üstteki başlığın belirlenmesi ve stilinin ayarlanması
            }.toolbar(content: {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Filter") {
                       showAlert = true
                    }.alert("Filter by Production Plant", isPresented: $showAlert) {
                        Button("Filter as All Production Plants", role: .cancel) {
                            filterPlant = "All"
                            filterByPlant(filterPlant)
                        }
                        Button("Filter as Halkalı") {
                            filterPlant = "Halkalı"
                            filterByPlant(filterPlant)
                        }
                        Button("Filter as Gemlik") {
                            filterPlant = "Gemlik"
                            filterByPlant(filterPlant)
                        }
                        Button("Filter as Vobarno") {
                            filterPlant = "Vobarno"
                            filterByPlant(filterPlant)
                        }
                        Button("Filter as Houston") {
                            filterPlant = "Houston"
                            filterByPlant(filterPlant)
                        }
                    } message: {
                        Text("How would you like to filter your search?")
                    }
                }
            }).navigationBarTitle("Borkatalog").navigationBarTitleDisplayMode(.inline)
                .searchable(text: $searchText, prompt: "Search in Norms").onChange(of: searchText) { search in
                    //Aramanın gerçekleştiği bölüm
                    searchNorms(search)
                }
        }
    }
}

//Search Function
func searchNorms(_ searchText: String) {
    if searchText == "" {
        normsSearch = allNorms
        return
    }
    
    var temp = allNorms
    for i in temp.indices {
        temp[i].elements = temp[i].elements.filter { $0.name.localizedStandardContains(searchText)
        }
    }
    
    normsSearch = temp.filter { !$0.elements.isEmpty }
}

//Filter by Plant
func filterByPlant(_ filterPlant: String) {
    if filterPlant == "All" {
        normsPlant = allNorms
        return
    }
    
    var temp = allNorms
    for i in temp.indices {
        temp[i].elements = temp[i].elements.filter { $0.productionPlant.localizedStandardContains(filterPlant) }
    }
    
    normsPlant = temp.filter { !$0.elements.isEmpty }
    print(normsPlant)
}
}

struct NormCatalogView_Previews: PreviewProvider {
static var previews: some View {
    NormCatalogView()
}
}

Norms.swift (Files that contains structs):

struct NormModel : Identifiable {
    var id = UUID()
    var title : String
    var elements : [NormElements]
}

struct NormElements : Identifiable {
    var id = UUID()
    var name : String
    var productionPlant : String
}

let en103052 = NormElements(name: "EN 10305-2", productionPlant: "Halkalı")
let en103053 = NormElements(name: "EN 10305-3", productionPlant: "Halkalı, Gemlik")
let en103055 = NormElements(name: "EN 10305-5", productionPlant: "Gemlik, Vobarno")

let ornekDinNorm = NormElements(name: "DIN Norm Sample", productionPlant: "Vobarno")

let enNorms = NormModel(title: "EN Normları", elements:[en103052, en103053, en103055])
let dinNorms = NormModel(title: "DIN Normları", elements: [ornekDinNorm])

let allNorms = [enNorms, dinNorms]

Solution

  • You are nearly there! searchNorms should do a further filter on normsPlant, instead of filtering on allNorms.

    func searchNorms(_ searchText: String) {
        if searchText == "" {
            normsSearch = normsPlant // <---
            return
        }
        
        var temp = normsPlant // <---
        for i in temp.indices {
            temp[i].elements = temp[i].elements.filter { $0.name.localizedStandardContains(searchText)
            }
        }
        
        normsSearch = temp.filter { !$0.elements.isEmpty }
    }
    

    You should also update what is currently displayed when you filterByPlant, by updating normsSearch:

    func filterByPlant(_ filterPlant: String) {
        defer {
            searchText = ""
            normsSearch = normsPlant
        }
        // the rest of filterByPlant...
    }
    

    Actually, I'd suggest having a single search function, and hence, a single @State array. The search function would take into account both searchText and filterPlant.

    @State private var normsSearch: [NormModel] = allNorms
    @State private var searchText: String = ""
    @State private var filterPlant: String = "All"
    
    ...
    
    .searchable(text: $searchText, prompt: "Search in Norms")
    .onChange(of: searchText) { _ in
        searchNorms()
    }
    // Do this to avoid calling the search function after every assignment to filterPlant
    .onChange(of: filterPlant) { _ in
        searchNorms()
    }
    
    ...
    
    // combining the old searchNorms and filterByPlant
    func searchNorms() {
        var temp = allNorms
        for i in temp.indices {
            temp[i].elements = temp[i].elements.filter {
                (searchText == "" || $0.name.localizedStandardContains(searchText)) &&
                (filterPlant == "All" || $0.productionPlant.localizedStandardContains(filterPlant))
            }
        }
        
        normsSearch = temp.filter { !$0.elements.isEmpty }
    }