WWDC22 SALE: Save 50% on all my Swift books and bundles! >>

Custom Stepper View

Forums > 100 Days of SwiftUI

Hi,

I am trying to modify the Stepper view's buttons (increment and decrement) so that the - button is green and the + button is red.

How would I go about doing this properly in SwiftUI so that I maintain all of the current behavior of Stepper?

   

This was an interesting challenge. Below is an attempt I made at doing this. I haven't thoroughly tested it out and there's probably better ways to do it, but it seems to work. You'll need to add the image to your Assets.

import SwiftUI

struct coloredStepper: View {
    let text: String
    @Binding var value: Double
    let range: ClosedRange<Double>
    let step: Double
    let onIncrement: (() -> Void)?
    let onDecrement: (() -> Void)?
    @State private var valueChanged = false

    var body: some View {
        HStack {
            Text(text)
            Spacer()
            ZStack {
                Image("minusPlus")
                    .resizable()
                    .frame(width: 90, height: 35)
                HStack {
                    Button {
                        decrement()
                    } label: {
                        Image(systemName: "minus")
                            .frame(width: 38, height: 35)
                    }
                    .buttonStyle(.borderless)
                    .foregroundColor(.green)

                    Button {
                        increment()
                    } label: {
                        Image(systemName: "plus")
                            .frame(width: 38, height: 35)
                    }
                    .buttonStyle(.borderless)
                    .foregroundColor(.red)
                }
            }
        }
        .onAppear() {
            if value < range.lowerBound {
                value = range.lowerBound
            } else if value > range.upperBound {
                value = range.upperBound
            }
        }
    }

    func decrement() {
        if value > range.lowerBound {
            value -= step
            valueChanged = true
        }
        if value < range.lowerBound {
            value = range.lowerBound
        }
        if let onDecrement = onDecrement {
            if valueChanged {
                onDecrement()
                valueChanged = false
            }
        }
    }

    func increment() {
        if value < range.upperBound {
            value += step
            valueChanged = true
        }
        if value > range.upperBound {
            value = range.upperBound
        }
        if let onIncrement = onIncrement {
            if valueChanged {
                onIncrement()
                valueChanged = false
            }
        }
    }
}

struct ContentView: View {
    @State private var value = 0.1

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Stepper("\(value.formatted())", value: $value, in: 1.0...2.0, step: 0.1)
                }
                Section {
                    coloredStepper(text: "\(value.formatted())", value: $value, range: 1.0...2.0, step: 0.1, onIncrement: { print("Incremented") }, onDecrement: { print("Decremented")} )
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

   

@vtabmow that's an interesting approach to the problem. One thing I noticed is that this does not display properly in dark mode. Also, when holding down the stepper button of the custom stepper view, it does not perform the same as the regular stepper. One more inconsistency between the two is that the button doesn't disable at range min/max, though that one shouldn't be too challenging to overcome.

   

I kind of figured there'd be issues with it.

I didn't realize you could hold down on the stepper and get it to increment or decrement like that. That could probably be handled with using gestures.

You could create another image for dark mode and use something like this: https://www.hackingwithswift.com/quick-start/swiftui/how-to-detect-dark-mode#:~:text=SwiftUI%20lets%20us%20detect%20whether%20dark%20mode%20or,automatically%20be%20reloaded%20when%20the%20color%20scheme%20changes. to switch between images.

And like you said the last one should be the easiest to fix.

   

Here is my attempt at this. It's not very reusable at the moment, and the minus button seems to be slightly off for some reason regardless of how I try to hardcode the values in. Does anyone have a better way of doing this or have any additional tips?

import SwiftUI

struct ContentView: View {
    @State private var value = 0.1

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Stepper("\(value.formatted())", value: $value, in: 0...2, step: 0.1)
                }

                Section {
                    ZStack {
                        Stepper("\(value.formatted())", value: $value, in: 0...2, step: 0.1)
                        HStack {
                            Spacer()
                            HStack {
                                Image(systemName: "minus")
                                    .padding(.trailing, 22)
                                    .padding(.bottom, 1)
                                    .foregroundColor(value != 0 ? .red : .gray)
                                Image(systemName: "plus")
                                    .padding(.trailing, 14)
                                    .foregroundColor(value != 2 ? .green : .gray)
                            }
                            .allowsHitTesting(false)
                        }
                    }
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

   

I spent some more time trying to tackle this problem and this is my current solution. It's the best that I can come up with for now... thoughts?

Oh and if someone has a better way... or a way that will enable the custom funcionality just for that view as listed in my TODO in the code comments please let me know!

As a side note, it feels so great accomplishing something like this. HWS has really empowered me here. Thank you!

import SwiftUI

// WARNING: Placing this function in a view's initializer will overide the Stepper look and feel for all views.
// TODO: Figure out a way to make it only override the Stepper look and feel for the view that calls the initializer.
func enableCustomStepper(decrementImage: UIImage? = UIImage(systemName: "minus"), decrementColor: UIColor = .label, incrementImage: UIImage? = UIImage(systemName: "plus")!, incrementColor: UIColor = .label, disabledColor: UIColor? = nil) {

    // Ability to override the decrement image and/or the color of the decrement image while in a normal state.
    UIStepper.appearance().setDecrementImage(decrementImage?.withTintColor(decrementColor, renderingMode: .alwaysOriginal), for: .normal)

    // Ability to override the increment image and/or the color of the increment image while in a normal state.
    UIStepper.appearance().setIncrementImage(incrementImage?.withTintColor(incrementColor, renderingMode: .alwaysOriginal), for: .normal)

    if disabledColor != nil {
        // Ability to override the decrement image and/or the color of the decrement image while in a disabled state.
        UIStepper.appearance().setDecrementImage(decrementImage?.withTintColor(disabledColor!, renderingMode: .alwaysOriginal), for: .disabled)

        // Ability to override the increment image and/or the color of the increment image while in a disabled state.
        UIStepper.appearance().setIncrementImage(UIImage(systemName: "plus")?.withTintColor(disabledColor!, renderingMode: .alwaysOriginal), for: .disabled)
    }
}

struct ContentView: View {

    init() {
        enableCustomStepper(decrementImage: UIImage(systemName: "minus"), decrementColor: .systemRed, incrementImage: UIImage(systemName: "plus"), incrementColor: .systemGreen, disabledColor: .systemGray)
    }

    @State private var value = 0.5

    var body: some View {
        NavigationView {
            Form {
                Section {
                    Stepper(String(value.formatted()), value: $value, in: 0...1, step: 0.1)
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

   

Hacking with Swift is sponsored by Fernando Olivares

SPONSORED Fernando's book will guide you in fixing bugs in three real, open-source, downloadable apps from the App Store. Learn applied programming fundamentals by refactoring real code from published apps. Hacking with Swift readers get a $10 discount!

Read the book

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.