Dale
Dale

Reputation: 5785

How can I keep the image color of UIButton from resetting to default?

This question is about Swift 4, Xcode 9.3.1.

The process to change the color of an image button is well documented. That's not the problem.

This question is about WHY the color of an image button resets to the default color if the button happens to be the target inside IBAction, and what do to about it.

I created a fresh project in Xcode to demonstrate what I'm seeing. This is a project that has an IBAction in the view controller that attempts to change the color of the images on a button. I added three buttons(oneButton, twoButton, threeButton) to the app, and wired them, so each has the following outlets:

outlets defined for the typical button

Each button has a different image defined. Here's an example:

example of image named 'local'

And here's the ViewController:

import UIKit

class ViewController: UIViewController {

  @IBOutlet weak var oneButton: UIButton!
  @IBOutlet weak var twoButton: UIButton!
  @IBOutlet weak var threeButton: UIButton!

  override func viewDidLoad() {
    super.viewDidLoad()
  }
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }

  @IBAction func myAction(sender: UIButton) {
    let oneImageView = oneButton.imageView
    if sender == oneButton {
      print("oneButton")
      oneImageView?.setImageColor(color: UIColor.red)
    } else if sender == twoButton {
      print("twoButton")
      oneImageView?.setImageColor(color: UIColor.blue)
    } else if sender == threeButton {
      print("threeButton")
      oneImageView?.setImageColor(color: UIColor.purple)
    }
  }
}

extension UIImageView {
  func setImageColor(color: UIColor) {
    let templateImage = self.image?.withRenderingMode(UIImageRenderingMode.alwaysTemplate)
    self.image = templateImage
    self.tintColor = color
  }
}

Clicking each icon shows the appropriate text in the output window (oneButton, twoButton, threeButton), depending on which one is clicked. That works fine.

When the app starts, the color of the first image is black (default), as expected.

Clicking twoButton causes the color on the first image to change to blue, as expected.

Clicking threeButton causes the color on the first image to change to purple, as expected.

Clicking oneButton causes the color on the first image to reset to the default (black). This is NOT expected.

I imagine what is happening is that, because the button is currently processing the system event (the touch), the color I'm setting gets wiped-out by some system process.

I changed the code so that rather than Touch Up Inside, the myAction() was called upon Touch Down, and the color of the first image DID turn red! But only while touching... as soon as I released, the color went back to default.

What I would like is to be able to touch oneButton and have it change to whatever color is in the code, and stay that way, rather than having it reset to the default color.

ADDITION / EDIT

The code above does set the image rendering mode over and over. It shouldn't be necessary to do that, I realize. But when I didn't do it this way, no colors would be changed when the app ran in the simulator. The code above at least changes the color of the image as long as it's not the item being touched. In the code below, although the appropriate portion of the if/else in myAction() runs, setting .tintColor has no effect at all, no matter which item is clicked. I tried with experiment true and false, to no avail.

class ViewController: UIViewController {

  @IBOutlet weak var oneButton: UIButton!
  @IBOutlet weak var twoButton: UIButton!
  @IBOutlet weak var threeButton: UIButton!

  var oneImageView: UIImageView!
  var oneImage: UIImage!

  override func viewDidLoad() {
    super.viewDidLoad()
    oneImageView = oneButton.imageView
    oneImage = oneImageView?.image
    let experiment = true
    if experiment {
      oneImageView?.image = UIImage(named: "image")?.withRenderingMode(.alwaysTemplate)
    } else {
      oneImage.withRenderingMode(.alwaysTemplate)
      oneImageView.image = oneImage
    }
  }
  override func didReceiveMemoryWarning() {
    super.didReceiveMemoryWarning()
  }

  @IBAction func myAction(sender: UIButton) {
    //let oneImageView = oneButton.imageView
    let tintColor: UIColor = {
      print("switch doesn't work here for some reason")
      if sender == oneButton {
        print("oneButton")
        return .red
      } else if sender == twoButton {
        print("twoButton")
        return .blue
      } else if sender == threeButton {
        print("threeButton")
        return .purple
      } else {
        return .cyan
      }
    }()
    oneImageView?.tintColor = tintColor
  }
}

Upvotes: 4

Views: 5742

Answers (2)

Dale
Dale

Reputation: 5785

Set Template Image

The short answer is that the image you expect to apply the color to needs to be designated as a template image. If this designation is made in the image asset's attribute inspector, all the code in the original question will work. Any calls to .withRenderingMode(.alwaysTemplate) would become unnecessary.

In Attributes Inspector

Unless the image is set as a Template Image, the color won't be applied.

The way that currently works (Xcode 9.4, Swift 4.1) is by setting the image to a template image in the GUI Attributes Inspector.

set template image attribute in GUI

Once this is done, all versions of the code in the original and edited question should work as expected.

In Code

Setting the image to a template image in code seems like it should work, but, at least with Xcode 9.4, Swift 4.1, on a simulator, it does not have permanence; the first user touch resets it.

In the code below, the .withRenderingMode(.alwaysTemplate) does cause the icon to become a template, but as soon as the user touches the icon the first time, it becomes default rendering again. This is true in the simulated iPhone (didn't test on a physical device).

override func viewDidLoad() {
  super.viewDidLoad()
  oneImageView = oneButton.imageView
  // NOT PERMANENT
  oneImageView?.image = UIImage(named:   "image")?.withRenderingMode(.alwaysTemplate) 
}

Upvotes: 5

glyvox
glyvox

Reputation: 58029

You don't need to set the image again and again, just set the image once as template and change the tint color later. Also, it's cleaner to use switch instead of multiple ifs.

override func viewDidLoad() {
    super.viewDidLoad()

    oneImageView?.image = UIImage(named: "image").withRenderingMode(.alwaysTemplate)
}

@IBAction func myAction(_ sender: UIButton) {
    let tintColor: UIColor = {
        switch sender {
            case oneButton: return .red
            case twoButton: return .blue
            case threeButton: return .purple
            default: return .clear
        }
    }()

    oneImageView?.tintColor = tintColor
}

Upvotes: 1

Related Questions