BLACK FRIDAY: Save 50% on all books and bundles! >>

SOLVED: Filter whole custom JSON object for SearchBarView

Forums > SwiftUI

Hello,

I am using the below code to filter the array contents. It works for a single property, in this case propertyToFilter.

How to extend this functionality to cover the whole custom object? I feel like I need to write a custom method to cover everything, but cannot quite put my finger on it.

Something like $0.lowercased().contains() would be splendid, but it doesn't work as expected of course.

SearchBarView(text: $searchText)
List {
    ForEach(items.filter {searchText.isEmpty ? true : $0.propertyToFilter.lowercased().contains(searchText.lowercased()}, id: \.id) { item in
        NavigationLink(destination: DetailView(itemID: item.id - 1)) {
            RowView(tamga: tamga)
        }
    }
}

What is the logic here?

   

In HWS+. A suggestion for filtering list which give the break down how it made, and well worth it to subscribe as lots of great coding tips.

For your make a swift file called FilteringList with this in

import SwiftUI

extension Binding {
    func onChange(_ handler: @escaping () -> Void) -> Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                handler()
            }
        )
    }
}

struct FilteringList<T: Identifiable, Content: View>: View {
    @State private var filteredItems = [T]()
    @State private var filterString = ""

    let listItems: [T]
    let filterKeyPaths: [KeyPath<T, String>]
    let content: (T) -> Content

    var body: some View {
        VStack {
            TextField("Type to filter", text: $filterString.onChange(applyFilter))
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding(.horizontal)

            List(filteredItems, rowContent: content)
                .onAppear(perform: applyFilter)
        }
    }

    init(_ data: [T], filterKeys: KeyPath<T, String>..., @ViewBuilder rowContent: @escaping (T) -> Content) {
        listItems = data
        filterKeyPaths = filterKeys
        content = rowContent
    }

    func applyFilter() {
        let cleanedFilter = filterString.trimmingCharacters(in: .whitespacesAndNewlines)

        if cleanedFilter.isEmpty {
            filteredItems = listItems
        } else {
            filteredItems = listItems.filter { element in
                filterKeyPaths.contains {
                  element[keyPath: $0]
                    .localizedCaseInsensitiveContains(cleanedFilter)
                }
            }
        }
    }
}

Then in replace List with

struct ContentView: View {
    let users = Bundle.main.decode([User].self, from: "users.json")

    var body: some View {
        NavigationView {
            FilteringList(users, filterKeys: \.name, \.company) { user in
                VStack(alignment: .leading) {
                    Text(user.name)
                        .font(.headline)
                    Text(user.company)
                        .foregroundColor(.secondary)
                }
            }
            .navigationBarTitle("Address Book")
        }
    }
}

1      

Or you can from WWDC Embrace Swift type inference Make a swift file called FilteringList with this in

import SwiftUI

extension String {
    func hasSubstring(_ substring: String) -> Bool {
        substring.isEmpty || contains(substring)
    }
}

public struct FilteredList<Element, FilterKey, RowContent>: View where Element : Identifiable, RowContent: View {
    private let data: [Element]
    private let filterKey : KeyPath<Element, FilterKey>
    private let isIncluded: (FilterKey) -> Bool
    private let rowContent: (Element) -> RowContent

    public init(
        _ data: [Element],
        filterBy key: KeyPath<Element, FilterKey>,
        isIncluded: @escaping (FilterKey) -> Bool,
        @ViewBuilder rowContent: @escaping (Element) -> RowContent
    ) {
        self.data = data
        self.filterKey = key
        self.isIncluded = isIncluded
        self.rowContent = rowContent
    }

    public var body: some View {
        let filteredData = data.filter {
            isIncluded($0[keyPath: filterKey])
        }

        return List(filteredData, rowContent: rowContent)
    }
}

then for the list

struct ContentView: View {
    let users = Bundle.main.decode([User].self, from: "users.json")
    @State var searchPhrase = ""

    var body: some View {
        NavigationView {
            VStack {
                TextField("Search", text: $searchPhrase)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .padding(.horizontal)

                FilteredList(users, filterBy: \.name, isIncluded: { name in name.hasSubstring(self.searchPhrase)}, rowContent: { user in
                    VStack(alignment: .leading) {
                        Text(user.name)
                            .font(.headline)
                        Text(user.company)
                            .foregroundColor(.secondary)
                    }
                })
            }
            .navigationBarTitle("Address Book")
        }
    }
}

The first one give you more option fields to filter with but both will filter the list

2      

Thank you! Both work great, and at the same time a reminder to watch more WWDC...

   

Hi

Has anyone managed to make this work with a CoreData project where the list to be filtered is a coredata entity? I'm really struggling <doh!>

Thanks

   

@NigelGee, using the first approach, the selected list item remains selected after getting back to the list. Moreover, the detail view always redraws itself with animation (if tapped through a filtered list), otherwise loads directly without any kind of redrawing. I assume it tries to defilter itself? Any ideas?

   

Save 50% in my Black Friday sale.

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

Not logged in

Log in
 

Link copied to your pasteboard.