Implement YouTube-like filtering in SwiftUI
Tags: ios, swift, ux, programming
Published: 2021-12-12 10:10:35

SwiftUI has its own radio button implementation using [Pickers](https://developer.apple.com/documentation/swiftui/picker) 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](https://cdn.hashnode.com/res/hashnode/image/upload/v1639295034971/oeGtgzbV4.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](https://docs.swift.org/swift-book/LanguageGuide/Enumerations.html). 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:
```swift
enum CarType: String, CaseIterable, Identifiable {
    var id: String { self.rawValue }
    case all, family, city, luxury, supercar
}
```

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:
```swift
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)
    }
}
```

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

FilterButtonStyle:
```swift
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)
      )
  }
}
```

FilterRowView should now look something like this:

![Screenshot 2021-12-12 at 09.10.01.png](https://cdn.hashnode.com/res/hashnode/image/upload/v1639296653504/NuhW9CXrP.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](https://reactpatterns.js.org/docs/state-hoisting/), and as of recently, Google is recommending using it when developing [Jetpack Compose](https://developer.android.com/jetpack/compose/state) 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](https://docs.swift.org/swift-book/LanguageGuide/Closures.html).

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:
```swift
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)
    }
}
```

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:
```swift
FilterRowView<CarType>(block: { type in
    viewModel.filterCars(by: type)
})
```
 

## 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](https://buymeacoffee.com/kfurjan).

Connect with me on [LinkedIn](https://www.linkedin.com/in/kevin-furjan/).