How to Create a Custom Weight Picker in SwiftUI

Lately, I’ve been working on a health app and needed a custom weight picker. I didn’t want to use the standard iOS picker. I was aiming for something that stands out and looks much better. I also wanted users to be able to choose between kg and lbs. Here’s what I came up with.

What Will You Learn?

  • How to use built-in pickers in SwiftUI.
  • How to create a custom component for more advanced requirements.
  • How to handle dynamic switching between lbs and kg units.

Picker in SwiftUI – Where to Start?

SwiftUI comes with a native Picker, which works great in many scenarios. For simple use cases like selecting a number, just a few lines of code are enough:

struct WeightPicker: View {
    @State private var selectedWeight: Int = 150
    
    var body: some View {
        Picker("Select Weight", selection: $selectedWeight) {
            ForEach(50...300, id: \.self) { weight in
                Text("\(weight) lbs")
            }
        }
        .pickerStyle(.wheel)
    }
}

This kind of picker works well, but sometimes a designer has a different vision – or maybe you just want your app to stand out. That was the case for me. So, I built my own custom picker.

Custom Weight Picker – Step by Step

Let’s create a WheelPickerView that will:

  • Support both lbs and kg units.
  • Have a clean design and support different styles.
  • Allow dynamic adjustment of the weight range.

Implementation of WheelPicker

Here’s the code for our custom component. It’s a complete implementation that you can copy into your project and customize as needed:

struct WheelPicker: View {
    var config: Config
    @Binding var value: CGFloat
    @State var isLoaded: Bool = false
    @State private var scrollPosition: Int?

    var body: some View {
        GeometryReader {
            let size = $0.size
            let horizontalPadding = size.width / 2

            ScrollView(.horizontal) {
                HStack(spacing: config.spacing) {
                    let totalSteps = config.steps * config.count

                    ForEach(0...totalSteps, id: \.self) { index in
                        let isMajorTick = index % config.steps == 0

                        Divider()
                            .background(isMajorTick ? Color.primary : .gray)
                            .frame(width: 0, height: isMajorTick ? 20 : 10)
                            .frame(maxHeight: 20, alignment: .bottom)
                            .overlay(alignment: .bottom) {
                                if isMajorTick && config.showsText {
                                    Text("\(index / config.steps * config.multiplier)")
                                        .font(.caption)
                                        .fontWeight(.semibold)
                                        .fixedSize()
                                        .offset(y: 20)
                                }
                            }
                    }
                }
                .frame(height: size.height)
                .scrollTargetLayout()
            }
            .scrollIndicators(.hidden)
            .scrollTargetBehavior(.viewAligned)
            .scrollPosition(id: $scrollPosition)
            .overlay(alignment: .bottom) {
                Rectangle()
                    .frame(width: 1, height: 40)
                    .padding(.bottom, 20)
            }
            .safeAreaPadding(.horizontal, horizontalPadding)
            .onAppear {
                scrollPosition = (Int(value) * config.steps) / config.multiplier
                isLoaded = true
            }
            .onChange(of: scrollPosition) { newValue in
                if let newValue {
                    value = (CGFloat(newValue) / CGFloat(config.steps)) 
                        * CGFloat(config.multiplier)
                }
            }
        }
    }

    struct Config: Equatable {
        var count: Int
        var steps: Int = 10
        var multiplier: Int = 10
        var spacing: CGFloat = 5
        var showsText: Bool = true
    }
}


How to Use It in Practice?

Now that we have our component ready, let’s see how to use it. Let’s say we want to create a view that allows selecting weight in lbs or kg:

struct WeightPickerView: View {
    @State private var unit: Unit = .lbs
    @State private var weight: CGFloat = 150

    var body: some View {
        VStack {
            
            Picker("Unit", selection: $unit) {
                Text("lbs").tag(Unit.lbs)
                Text("kg").tag(Unit.kg)
            }
            .pickerStyle(SegmentedPickerStyle())
            .padding()

            WheelPicker(
                config: .init(
                    count: unit == .lbs ? 300 : 150, 
                    multiplier: unit == .lbs ? 1 : 1,
                    steps: 10
                ),
                value: $weight
            )
            .frame(height: 200)
            .padding()

            Text("Selected Weight: \(weight, specifier: "%.1f") \(unit.rawValue)")
                .font(.headline)
                .padding()
        }
    }

    enum Unit: String {
        case lbs = "lbs"
        case kg = "kg"
    }
}

Visualization in Action

Here’s an example of how our picker works in action:

Customization and Optimization

Thanks to the modular structure, you can easily customize the WheelPicker to fit your needs. For example:

  • Change the number of steps (config.steps).
  • Add animations or visual effects.
  • Adjust weight ranges based on the user’s region (e.g., different ranges for lbs and kg).

Summary

In this post, I showed you:

  • How to build a WheelPicker from scratch.
  • How to integrate it with dynamic units (lbs and kg).
  • How to easily adjust weight ranges.

I hope you found this article helpful.