2

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
  }
}
Community
  • 1
  • 1
Dale
  • 5,520
  • 4
  • 43
  • 79
  • 2
    This is probably *not* what you're looking for, but... have you tried (1) changing the button type from *custom* to *system* and then (2) setting the *tintColor*? That's how I set image colors. –  May 29 '18 at 21:54
  • I did try changing the button type. That didn't help, but what DID resolve the problem was going to xcassets and changing `Render As` to `Template Image` (from `Default`). For some reason, setting the rendering mode in code to `.alwaysTemplate` did not provide the desired result. – Dale May 29 '18 at 22:06
  • You should never change button image directly. There are special methods for that. Note that a button has an image for every state. – Sulthan May 29 '18 at 22:43
  • @Sulthan, the state of the buttons in the real application depend not just on the state of the button, but on the state of other things, thus the need to manage the color manually. – Dale May 29 '18 at 23:39
  • @Dale I was speaking merely about the fact that you shouldn't change `imageView.image` manually since it get set every time you change button state (e.g. when highlighting, selecting etc). You should use `setImage(UIImage?, for: UIControlState)` instead. – Sulthan May 30 '18 at 09:22

2 Answers2

3

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) 
}
Dale
  • 5,520
  • 4
  • 43
  • 79
1

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
}
Tamás Sengel
  • 55,884
  • 29
  • 169
  • 223
  • The code in the question wasn't meant to be elegant. It was meant to demonstrate a problem. I don't think your answer addresses the problem of resetting the color to the default. – Dale May 29 '18 at 20:19
  • The problem is that you unnecessarily extend UIImageView and setting the image. That way of accessing and modifying the image causes the reset that you described. – Tamás Sengel May 29 '18 at 20:25
  • Thanks for the idea. I attempted to implement it, but couldn't get it working (I added the code to the original question). I tried several variations, but setting the tintColor never did anything (`myAction` ran, the appropriate tintColor was returned, but setting the tintColor had no effect). I can see this SHOULD work, but it just doesn't work, at least in the tiny trivial sample app I posted. – Dale May 29 '18 at 21:14
  • I implemented the code in a sample app and it worked fine. Are you sure that `oneImageView` is not nil when `myAction` is executed? Also, is the image of `oneImageView` rendered as template? – Tamás Sengel May 29 '18 at 21:16
  • Your implementation was using `UIButton`s with images on them and it worked? Wow. My "from scratch" app had all defaults... very few clicks...not sure which click I missed. `oneImageView` is certainly not nil when `myAction` is executed. In the attributes inspector for the buttons, I can't find the "render as" for the image contained in the button to set it to template. That's why I was doing it in code. – Dale May 29 '18 at 21:44
  • You can change the default rendering mode of an asset in the Asset Catalog by navigating to the asset and changing the Render As option in the Attributes inspector on the right. [Check out this answer.](https://stackoverflow.com/a/27747157/3151675) – Tamás Sengel May 29 '18 at 21:46
  • Printing the `oneImageView` object shows `userInteractionEnabled = NO`. Not sure if that could be the problem or not. – Dale May 29 '18 at 21:49
  • **Render As** was it!! That fixed it! – Dale May 29 '18 at 21:52