10

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?

Zach Young
  • 10,137
  • 4
  • 32
  • 53
Melodius
  • 2,505
  • 3
  • 22
  • 37
  • What is the purpose of this concept? will try to give other approach. – Raja Kishan Jan 15 '21 at 13:01
  • 1
    Try at first https://stackoverflow.com/search?tab=votes&q=%5bswiftui%5d%2bSlider%2bInt – Asperi Jan 15 '21 at 13:04
  • 1
    I don't understand your question. If I have a data structure with Int properties, how can those be edited with a Slider which expects a BinaryFloatingPoint-type binding and Int is not BinaryFloatingPoint. – Melodius Jan 15 '21 at 13:06
  • 2
    This may help you: http://ootips.org/yonat/swiftui-binding-type-conversion/ – Raja Kishan Jan 15 '21 at 13:14

4 Answers4

24
struct IntSlider: View {
    @State var score: Int = 0
    var intProxy: Binding<Double>{
        Binding<Double>(get: {
            //returns the score as a Double
            return Double(score)
        }, set: {
            //rounds the double to an Int
            print($0.description)
            score = Int($0)
        })
    }
    var body: some View {
        VStack{
            Text(score.description)
            Slider(value: intProxy , in: 0.0...10.0, step: 1.0, onEditingChanged: {_ in
                print(score.description)
            })
        }
    }
}
lorem ipsum
  • 21,175
  • 5
  • 24
  • 48
  • I'm really glad I found this Q&A, it took me a bit to figure out what the crux of your answer was, so I refactored and edited to make it clearer, in my judgement. If you don't like it and want to revert, I totally understand. – Zach Young May 02 '21 at 00:48
  • 1
    Not a fan about creating variables in the body and while that is what is being done SO is full of people doing creating all sorts of variables. Maybe if it was outside if the body it would clarify what is being done. I can change it tomorrow. – lorem ipsum May 02 '21 at 01:32
16

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(from: $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 these are static extensions defined on Binding itself.)

Here are the aforementioned extension methods...

public extension Binding {

    static func convert<TInt, TFloat>(from 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>(from 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(from: $count), in: 1...8, step: 1)
            }
        }
        .padding()
    }
}

And finally, here are the results...

enter image description here

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • Einstein's Hair strikes again! This solution is awesome - until the range includes negative numbers. My slider code had to be `Slider(value: .convert(from: $count), in: -36.0...24.0, step: 1)` <-- using float literals in the range. – user1944491 Jun 17 '23 at 21:21
  • Odd! I just tried your slider code, copied exactly from your comment with the negative float literals and it worked without issue. Note that I did have to change the range in the `ForEach` by adding 37 to `count` (i.e. `ForEach(1...count + 37, id: \.self)`) or else the negative-36 value from the slider would make that range invalid, but that has nothing to do with the `convert` method itself. That's just demo code showing it working with an `Int` binding. Are you sure that wasn't what you experienced? And lol... how do you know my nickname? Didn't think that was on here, but that's me! – Mark A. Donohoe Jun 18 '23 at 06:22
  • Right - the float literals work, but `Int` literals don’t! Try with range `-36…24`. TBH I bounced from your SO profile to your web page. Had to see who came up with that elegant solution! – user1944491 Jun 20 '23 at 02:24
  • Can you double-check your code? Negative `Int` literals work fine for me. I just tried `Slider(value: .convert(from: $count), in: -36...24, step: 1)` and it works fine. You just have to make sure to compensate for that negative in the `ForEach` code (again, changing that part to `ForEach(1...count + 37, id: \.self)` because if you don't you end up with an invalid range, but that has nothing to do with the binding, data types or literals. – Mark A. Donohoe Jun 27 '23 at 16:58
1

There is also a way by using the double value with casting it to an int before.. NOTICE: You have to be careful with this way - you need to remember that it is a double value and no int.

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

    var body: some View {
        VStack{
            Text("\(Int(score))")
            Slider(value: $score, in: 0...10, step: 1)
        }
    }
}
Iskandir
  • 937
  • 1
  • 9
  • 21
0

To elaborate on lorem ipsum's answer before, you can use this handy struct:

struct IntDoubleBinding {
    let intValue : Binding<Int>
    
    let doubleValue : Binding<Double>
    
    init(_ intValue : Binding<Int>) {
        self.intValue = intValue
        
        self.doubleValue = Binding<Double>(get: {
            return Double(intValue.wrappedValue)
        }, set: {
            intValue.wrappedValue = Int($0)
        })
    }
}

and then in your code, just use it like this:

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

    var body: some View {
        VStack{
            Text(score.description)
            Slider(value: IntDoubleBinding($score).doubleValue, in: 0.0...10.0, step: 1.0)
        }
    }
}
Aviad Ben Dov
  • 6,351
  • 2
  • 34
  • 45