programming Peter Ent programming Peter Ent

Filter Bar with SwiftUI

An alternative to the text-only search bar.

Sometimes you want do a search on categories or groups rather than on specific words or strings. I threw together this filtered search bar to help. You will have to combine the parts in ways that make sense for you, but at least you’ll have some pieces to use.

The idea is that you have a search bar-like area that gets filled with “filters” which I represent using image+title pairs (SwiftUI’s Label component).

Let’s begin with the model. Create a file called FilterModel.swift and place this definition of FilterData into it (you could put this into its own file, but it goes with the model):

struct FilterData: Identifiable {
    var id = UUID()
    var imageName: String
    var title: String
    var isSelected: Bool = false
}

Below that, in this same file, define FilterModel itself.

class FilterModel: NSObject, ObservableObject {
    
    // 1. normally you would get this data from a remote service, so factor that in if you use
    // this in your own projects. If this data is not static, consider making it @Published
    // so that any changes to it will get reflected by the UI
    var data = [
        FilterData(imageName: "airplane", title: "Travel"),
        FilterData(imageName: "tag.fill", title: "Price"),
        FilterData(imageName: "bed.double.fill", title: "Product"),
        FilterData(imageName: "car.fill", title: "Vehicle")
    ]
    
    // 2. these are the FilterData that have been selected using the toggleFilter(at:)
    // function.
    @Published var selection = [FilterData]()
    
    // 3. toggles the selection of the filter at the given index
    func toggleFilter(at index: Int) {
        guard index >= 0 && index < data.count else { return }
        data[index].isSelected.toggle()
        refreshSelection()
    }
    
    // 4. clears the selected items
    func clearSelection() {
        for index in 0..<data.count {
            data[index].isSelected = false
        }
        refreshSelection()
    }
    
    // 5. remakes the published selection list
    private func refreshSelection() {
        let result = data.filter{ $0.isSelected }
        withAnimation {
            selection = result
        }
    }
}

The model’s job is to provide the data for the UI to display. Here we have:

  1. The data is the list of FilterData items. In a real app this list would come from a remote API server. For this example they are just hard-coded.

  2. The selection are those FilterData that have been picked by the user for the search. Notice that this is @Published which means the UI can observe changes to this variable. More precisely, the UI will not notice changes to the array itself, just whether or not selection itself has changed.

  3. The toggleFilter function adds or removes a FilterData from the selection. It does this by building a new array from the FilterData where an item’s isSelected value is true.

  4. The clearSelection function just sets every FilterData.isSelected in the data to false.

  5. The refreshSelection function is private and is called from toggleFilter and clearSelection. It is the function that builds a new selection. Notice that when it sets selection to the new value it does so within an withAnimation block. You’ll see how this is used later.

Now that we’ve got a model, let’s look at how a FilterData is represented visually. Create a new SwiftUI View file called FilterTag.swift and put this code into it:

struct FilterTag: View {
    // 1
    var filterData: FilterData
    
    // 2
    var body: some View {
        Label(filterData.title, systemImage: filterData.imageName)
            .font(.caption)
            .padding(4)
            .foregroundColor(.white)
            .background(
                RoundedRectangle(cornerRadius: 8)  // 3
                    .foregroundColor(filterData.isSelected ? .accentColor : Color.black.opacity(0.6))
            )
            // 4
            .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading)))
    }
}
  1. This struct has a FilterData property because that is what it going to reflect, visually.

  2. The body is just a SwiftUI Label with some modifiers to give it rounded rectangle background.

  3. Notice that background color is dependent on the isSelected property of the FilterData.

  4. Finally, you can see the transition property. This is where the withAnimation comes into play. This transition will be applied when the FilterTag is appears (inserted into the display list) and when it disappears (removed from the display list). I picked a slide effect but you can do whatever you think looks nice, of course.

The last piece is the FilterBar itself, so go ahead and create a FilterBar.swift SwiftUI View file and place this code into it:

struct FilterBar: View {
    // 1
    @EnvironmentObject var filterModel: FilterModel
    
    // 2
    var body: some View {
        HStack {
            Image(systemName: "magnifyingglass")
                .foregroundColor(.gray)
            // 3
            ScrollView(.horizontal, showsIndicators: false) {
                HStack {
                    ForEach(filterModel.selection) { item in
                        FilterTag(filterData: item)
                    }
                }
            }
            Spacer()
            Button(action: { filterModel.clearSelection() }) {
                Image(systemName: "xmark.circle.fill")
                    .foregroundColor(Color.black.opacity(0.6))
            }
        }
        .padding(6)
        .background(RoundedRectangle(cornerRadius: 8).foregroundColor(Color.gray.opacity(0.5)))
    }
}
  1. The FilterBar needs a FilterModel to work. I usually pass around models using @EnvironmentObject but you could also pass it in as a property and declare it with @ObservedObject since you want this UI to watch for changes to it (ie, the selection property).

  2. The “bar” is an HStack with a magnifying glass image, a horizontally scrollable collection of FilterTag views, and a clear button.

  3. The scrollable portion is itself an HStack built using a ForEach component using the model’s selection property. Because selection is a @Published property of an ObservableObject, SwiftUI automatically watches it. When it gets changed, SwiftUI will re-run this ForEach.

To see all this in action, let’s modify ContentView to display the list of filters in the model’s data and when on is tapped, make it selected (or not):

struct ContentView: View {
    @ObservedObject var filterModel = FilterModel()
    
    var body: some View {
        VStack {
            FilterBar()
                .environmentObject(filterModel)
            Spacer()
            List {
                ForEach(0..<filterModel.data.count) { index in
                    FilterTag(filterData: filterModel.data[index])
                        .onTapGesture {
                            filterModel.toggleFilter(at: index)
                        }
                }
            }
        }
        .padding()
    }
}

And there you have a different type of search bar - one that uses “filters” that represents subsets of the data.

I used a simple list to present the filter choices, but you might need something more complex. For example, you might have a section for pricing. Each item of that section might be prices from $1000 and up, from $500 to $1000, from $250 to $500, from $100 to $250, and below $100. But the filter bar would just show the same visual tag - its just so the user knows they are filtering by price. You could expand FilterData to include enums or whatever else you needed to capture all the information so it can be easily sent to your backend system to carry out the filtering (or if you have all the data already loaded, do it instantly).

I hope you find some use in this or that it sparks some ideas.




Read More