DEV Community

Cover image for Implement YouTube-like filtering in SwiftUI
Kevin Furjan
Kevin Furjan

Posted on • Originally published at kevin-furjan.hashnode.dev

Implement YouTube-like filtering in SwiftUI

SwiftUI has its own radio button implementation using Pickers that come with few default designs, such as .pickerStyle(.segmented) or .pickerStyle(.wheel). Pickers can be useful for implementing filtering functionality but what if you want custom design and behavior, for example YouTube-like design and behavior?

YouTube-like filtering uses horizontally scrollable view where buttons are placed, each with its own state, and selected button has different color to the others.

79CB4010-0B70-4ED6-9001-249DAA231B49_4_5005_c.jpeg

Requirements

Before implementing custom YouTube-like filtering, let's define a few requirements that custom view needs to meet:

  • view needs to mimic YouTube-like filtering

  • view needs to use enumerations

  • view needs to be generic

  • view needs to be stateless

State handling

For handling state, custom filter view will use Swift Enumerations. Enumerations in Swift are quite powerful and are an ideal way to filter data displayed on view once each enum case is selected. Custom filter view will also require that enum conforms to a few protocols, such as String, CaseIterable and Identifiable.

Let's imagine that the application is showing a list of cars available for purchase, and you would like to filter them based on type. In such a case you would create CarType enum to define the type of each car in the list.

CarType:

enum CarType: String, CaseIterable, Identifiable {
    var id: String { self.rawValue }
    case all, family, city, luxury, supercar
}
Enter fullscreen mode Exit fullscreen mode

Explanation of why each protocol is needed:

  • String - we want each enum case to have its raw value represented as string, needed for Text view in SwiftUI

  • CaseIterable - by conforming to this protocol, you can access a collection of all the type’s cases by using the type’s allCases property, needed for ForEach loop in SwiftUI

  • Identifiable - to provide unique id to each enum state, needed for ForEach loop in SwiftUI

Implementing view

In order to meet previously defined requirements, custom view needs to support generics that match Enumeration model defined in previous step.

FilterRowView:

import SwiftUI

struct FilterRowView<T>: View
    where T: RawRepresentable, T: CaseIterable, T: Identifiable, T.AllCases == [T], T.RawValue == String {

    @State private var selectedFilter: T = T.allCases[0]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 15) {
                ForEach(T.allCases) { filter in
                    Button(action: {
                        withAnimation(Animation.spring().speed(1.5)) {
                            selectedFilter = filter
                        }
                    }) {
                        Text(filter.rawValue.capitalized)
                            .font(.subheadline)
                    }
                    .buttonStyle(FilterButtonStyle(isSelected: selectedFilter == filter))
                }
            }
        }
        .frame(height: 40)
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, view is using custom ButtonStyle to mimic YouTube-like design.

FilterButtonStyle:

struct FilterButtonStyle: ButtonStyle {

    let isSelected: Bool

    func makeBody(configuration: Self.Configuration) -> some View {
    configuration.label
      .foregroundColor(
        isSelected ? .white : .black
      )
      .padding()
      .frame(maxWidth: .infinity, maxHeight: 30)
      .background(
        isSelected ? .black : .white
      )
      .cornerRadius(24)
      .overlay(
        RoundedRectangle(cornerRadius: 24)
            .stroke(isSelected ? .white : .black, lineWidth: 0.3)
      )
  }
}
Enter fullscreen mode Exit fullscreen mode

FilterRowView should now look something like this:

Screenshot 2021-12-12 at 09.10.01.png

It's horizontally scrollable view with buttons, each with its own state, and selected button has different color to the others.

State hoisting

As you probably noticed yourself, FilterRowView doesn't really do anything so we need to add the ability to filter data, but the view must be stateless, as stated in the Requirements section.

How to do that? Using state hoisting pattern. What is that? Let's explain it.

State hoisting is a well-known pattern for developing React applications, and as of recently, Google is recommending using it when developing Jetpack Compose applications.

State hoisting is a pattern of moving state to a caller to make view stateless.

In practice that means using higher order functions, which in Swift terms, means using closures.

To start using state hoisting pattern in FilterRowView, we only need two more lines of code. One line for new parameter which will accept closure and one line to provide generic type to closure.

Updated FilterRowView:

import SwiftUI

struct FilterRowView<T>: View
    where T: RawRepresentable, T: CaseIterable, T: Identifiable, T.AllCases == [T], T.RawValue == String {

    let block: (T) -> Void
    @State private var selectedFilter: T = T.allCases[0]

    var body: some View {
        ScrollView(.horizontal, showsIndicators: false) {
            LazyHStack(spacing: 15) {
                ForEach(T.allCases) { filter in
                    Button(action: {
                        withAnimation(Animation.spring().speed(1.5)) {
                            selectedFilter = filter
                        }
                        block(filter)
                    }) {
                        Text(filter.rawValue.capitalized)
                            .font(.subheadline)
                    }
                    .buttonStyle(FilterButtonStyle(isSelected: selectedFilter == filter))
                }
            }
        }
        .frame(height: 40)
    }
}
Enter fullscreen mode Exit fullscreen mode

With this, FilterRowView is completely stateless and to do anything useful, it relies on its caller.

To use this in your application, you would call FilterRowView view with something like this:

FilterRowView<CarType>(block: { type in
    viewModel.filterCars(by: type)
})
Enter fullscreen mode Exit fullscreen mode

Conclusion

Thank you for reading and I hope this article was useful to you! In conclusion, this article went over how to implement custom YouTube-like filtering in SwiftUI using enumerations, generics, and closures.


If you like my content and find it useful, please consider following me. If you are feeling extra generous, please consider buying me a coffee.

Connect with me on LinkedIn.

Discussion (0)