iosswiftuikituistepper

Problems customizing UIStepper with translucent divider, button sizes and deferring tint color


Having problems customizing the UIStepper control for my iOS app.

So far I've been able to customize it to the point where the background and divider are translucent, as well as changed the increment and decrement images using the code below...

func decorate(stepper: UIStepper) {
    let greyLeft   = UIImage(named: Assets.Button.leftGrey)
    let greenLeft  = UIImage(named: Assets.Button.leftGreen)
    let greyRight  = UIImage(named: Assets.Button.rightGrey)
    let greenRight = UIImage(named: Assets.Button.rightGreen)
    let blank      = UIImage(named: Assets.Button.translucent)
    let color      = UIColor(red:   147/255,
                             green: 148/255,
                             blue:   81/255,
                             alpha: 1.0)
    
    stepper.setDecrementImage(greenLeft,  for: .normal)
    stepper.setDecrementImage(greyLeft,   for: .disabled)
    stepper.setIncrementImage(greenRight, for: .normal)
    stepper.setIncrementImage(greyRight,  for: .disabled)
    stepper.setBackgroundImage(blank, for: .normal)
    stepper.setDividerImage(blank,
                            forLeftSegmentState: .normal,
                            rightSegmentState: .normal)
    stepper.tintColor = color
}

I have 3 problems though...

  1. While the divider is translucent in normal state, when I tap the control it seems to flash an image. I've tried using a second call to the "setDividerImage" method for all the other states available including "selected", "highlighted", "focused", "disabled", "application" and "reserved" but the flash is still there (see screenshot below). How do I replace that image with my own or prevent the flash?

flashing divider

  1. Even though I'm using two different images (same shape, different color) for the increment and decrement images their original colors are being overridden by the tint color. I think I can just cycle the tint color when it changes, but is it possible to defer the tint color so it doesn't override the image? Then I can avoid a solid color.

  2. The images that I've got for the increment and decrement images are too small. Can I adjust the size programmatically? So I don't have to edit the images or go back to my graphic artist for larger ones.

Question #1 is my primary concern, because I think I can deal with #2 and #3 if needs be. However, it would be great if I could at least get confirmation that the answers to those are "NO" if that's the case.


Solution

  • Change this line:

    let blank = UIImage(named: Assets.Button.translucent)
    

    to:

    let blank = UIImage()
    

    that should get rid of the highlighted separator.

    If your images are simple arrows, you could easily generate those via code "on-the-fly" with the desired colors, or use them as tint-able images.

    If you load the images like this:

    let greyLeft   = UIImage(named: Assets.Button.leftGrey)?.withRenderingMode(.alwaysOriginal)
    let greenLeft  = UIImage(named: Assets.Button.leftGreen)?.withRenderingMode(.alwaysOriginal)
    let greyRight  = UIImage(named: Assets.Button.rightGrey)?.withRenderingMode(.alwaysOriginal)
    let greenRight = UIImage(named: Assets.Button.rightGreen)?.withRenderingMode(.alwaysOriginal)
    

    they shouldn't be affected by the tint color.


    Edit

    Take a look at these changes:

    func decorate(stepper: UIStepper) {
        
        let colorNormal         = UIColor(red:   147/255,
                                          green: 148/255,
                                          blue:   81/255,
                                          alpha: 1.0)
        
        let colorHighlighted    = UIColor(red:   0.9,
                                          green: 0.0,
                                          blue:   0.0,
                                          alpha: 1.0)
        
        let colorDisabled       = UIColor(red:   0.25,
                                          green: 0.25,
                                          blue:   0.25,
                                          alpha: 1.0)
        
        // adjust size to your liking
        let fnt = UIFont.systemFont(ofSize: 32)
        let configuration = UIImage.SymbolConfiguration(font: fnt)
        
        let lArrow              = UIImage(systemName: "arrowtriangle.left.fill", withConfiguration: configuration)?.withRenderingMode(.alwaysOriginal)
        let leftNormal          = lArrow?.withTintColor(colorNormal)
        let leftHighlighted     = lArrow?.withTintColor(colorHighlighted)
        let leftDisabled        = lArrow?.withTintColor(colorDisabled)
    
        let rArrow              = UIImage(systemName: "arrowtriangle.right.fill", withConfiguration: configuration)?.withRenderingMode(.alwaysOriginal)
        let rightNormal         = rArrow?.withTintColor(colorNormal)
        let rightHighlighted    = rArrow?.withTintColor(colorHighlighted)
        let rightDisabled       = rArrow?.withTintColor(colorDisabled)
        
        let blank               = UIImage()
    
        stepper.setDecrementImage(leftNormal, for: .normal)
        stepper.setDecrementImage(leftHighlighted, for: .highlighted)
        stepper.setDecrementImage(leftDisabled, for: .disabled)
    
        stepper.setIncrementImage(rightNormal, for: .normal)
        stepper.setIncrementImage(rightHighlighted, for: .highlighted)
        stepper.setIncrementImage(rightDisabled, for: .disabled)
        
        stepper.setBackgroundImage(blank, for: .normal)
        stepper.setDividerImage(blank,
                                forLeftSegmentState: .normal,
                                rightSegmentState: .normal)
    
    }
    

    This uses left and right filled triangle SF Symbol images (adjust the size to your liking), and I've added colors for Highlighted state (not required).


    Edit 2

    If you want to add some horizontal space between the dec/inc buttons, we can do so by using a clear image for the "divider" images -- but, Apple's docs are not particularly clear on how to go about it.

    To avoid the "weird highlight" between the buttons, we have to set the clear image for 3 "state combinations":

    normal / normal
    normal / highlighted
    highlighted / normal
    

    So, if we wanted, for example, 64-points of horizontal spacing:

        // create a clear image of desired width
        //  (height will be auto-stretched)
        let blank = UIGraphicsImageRenderer(size: CGSize(width: 64, height: 8)).image(actions: {c in})
    
        stepper.setBackgroundImage(blank, for: .normal)
    
        // set blank image for normal / normal
        stepper.setDividerImage(blank,
                                forLeftSegmentState: .normal,
                                rightSegmentState: .normal)
    
        // set blank image for normal / highlighted
        stepper.setDividerImage(blank,
                                forLeftSegmentState: .normal,
                                rightSegmentState: .highlighted)
    
        // set blank image for highlighted / normal
        stepper.setDividerImage(blank,
                                forLeftSegmentState: .highlighted,
                                rightSegmentState: .normal)
    

    We have to set at least those 3 state combinations.

    Curiously, we can also set images to be used if one (or both) of the buttons is .disabled ... but, if we don't it uses the matching .normal state.

    Note: Using a custom-width divider image compresses the width of the buttons, so this may or may not be useful information.