Search code examples
swiftuislider

How do I create a slider in SwiftUI bound to an Int-type property?


I have a view with with an Int property named "score" that I want to adjust with a slider.

struct IntSlider: View {
    @State var score:Int = 0

    var body: some View {
        VStack{
            Text(score.description)
            Slider(value: $score, in: 0.0...10.0, step: 1.0)
        }
    }
}

But SwiftUI's Slider only works with doubles/floats.

How can I make it work with my integer?


Solution

  • TL;DR

    As an alternative to the other answers on this page, I propose .convert methods implemented as type-constrained, generic extension methods on Binding which automatically do the conversion for you inline. Using your code above, here's all you have to do:

    Slider(value: .convert($score), in: 0.0...10.0, step: 1.0)
    

    Benefits of this approach:

    1. It automatically converts any Int type (e.g. Int, Int8, Int64) to any Float type (e.g. Float, Float16, CGFloat, Double), (and vice versa should you need it) thanks to generics/overloads.

    2. Once you have added the extension to your project (or in a package referenced by your project), you simply call .convert at any binding site and it 'just works.'

    3. There is nothing else needed to use it (i.e. no 'proxy' structs, vars or other local items to clutter your view.) You simply use the method directly inline as shown above.

    4. When in any place that takes a Binding, simply type . and code-completion will automatically suggest convert as an option. This works because each extension method is both defined on Binding, and it returns a Binding (i.e. Self) as its result type, thus making it discoverable to the auto-complete system. (This is true for any such static methods on any type.)

    Here are the aforementioned extension methods...

    public extension Binding {
    
        static func convert<TInt, TFloat>(_ intBinding: Binding<TInt>) -> Binding<TFloat>
        where TInt:   BinaryInteger,
              TFloat: BinaryFloatingPoint{
    
            Binding<TFloat> (
                get: { TFloat(intBinding.wrappedValue) },
                set: { intBinding.wrappedValue = TInt($0) }
            )
        }
    
        static func convert<TFloat, TInt>(_ floatBinding: Binding<TFloat>) -> Binding<TInt>
        where TFloat: BinaryFloatingPoint,
              TInt:   BinaryInteger {
    
            Binding<TInt> (
                get: { TInt(floatBinding.wrappedValue) },
                set: { floatBinding.wrappedValue = TFloat($0) }
            )
        }
    }
    

    ...and here's a playground-ready demonstration showing them in use.
    (Note: Don't forget to also copy over the extension methods above!)

    struct ConvertTestView: View {
    
        @State private var count: Int = 1
    
        var body: some View {
    
            VStack{
                HStack {
                    ForEach(1...count, id: \.self) { n in
                        Text("\(n)")
                            .font(.title).bold().foregroundColor(.white)
                            .frame(maxWidth: .infinity, maxHeight: .infinity)
                            .background(.blue)
                    }
                }
                .frame(maxHeight: 64)
                HStack {
                    Text("Count: \(count)")
                    Slider(value: .convert($count), in: 1...8, step: 1)
                }
            }
            .padding()
        }
    }
    

    And finally, here are the results...

    Example screenshot