Search code examples
swiftuislider

How do I create a slider in SwiftUI for 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 a solution which leverages type-constrained, generic extension methods to simplify the call site. Here's an example of all you have to do:

    Slider(value: .convert($count), in: 1...8, step: 1)
    

    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 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 it directly inline (see 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