I have a button and an action. I also have an image for the button in its normal state and when it is pressed. These are .svg images. My button is to turn the sound mute/unmute. So I have two images with the speaker and with the speaker crossed out. And if I click on the button, then the image should change. Everything works fine. The image changes when pressed and the action works normally. But the problem is that if I press the button, but do not remove my finger from the screen, but move it outside the button area, then my image will change to a crossed out speaker, and if I then remove my finger from the screen outside the button area, then in the end I will get that the image will change to a crossed out speaker, but the action will not be performed and the sound will continue to play. By moving my finger outside the button area, and then back into its area, image will change. How to fix this?
controller class
class ViewController: UIViewController {
var button: SomeButtonWithImage = {
let button = SomeButtonWithImage()
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
button = SomeButtonWithImage()
button.normalImage = "mute"
button.selectedImage = "unmute"
button.tag = index
button.widthAnchor.constraint(equalToConstant: controlButtonsHeight).isActive = true
button.heightAnchor.constraint(equalToConstant: controlButtonsHeight).isActive = true
button.addTarget(self, action: #selector(self.rightButtonAction), for: .touchUpInside)
rightStackView.addArrangedSubview(button)
}
}
button class
class SomeButtonWithImage: UIButton {
var buttonSelectedScale: CGFloat = 0.9
var buttonScaleDownDuration: TimeInterval = 0.15
var buttonScaleUpDuration: TimeInterval = 0.25
public var normalImage: String = ""
public var selectedImage: String = ""
override var isHighlighted: Bool {
didSet { if oldValue == false && isHighlighted { selected() }
else if oldValue == true && !isHighlighted { deselected() }
}
}
override init(frame: CGRect) {
super.init(frame: frame)
configuration = .plain()
configuration?.baseBackgroundColor = .clear
configurationUpdateHandler = { button in
switch button.state {
case .normal: button.configuration?.image = UIImage(named: self.normalImage)
case .selected: button.configuration?.image = UIImage(named: self.selectedImage)
default: break
}
}
}
required init?(coder: NSCoder) { fatalError("error") }
func selected() { animateScale(to: buttonSelectedScale, duration: buttonScaleDownDuration) }
func deselected() { animateScale(to: 1, duration: buttonScaleUpDuration); isSelected.toggle() }
private func animateScale(to scale: CGFloat, duration: TimeInterval) {
UIView.animate( withDuration: duration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: {
self.transform = .init(scaleX: scale, y: scale)
}, completion: nil)
}
}
addition:
Why do I use configurationHandler
instead of setImage(UIImage?, for: UIControlState)
? If I use setImage
when clicked the image will be covered with a gray background. I used this adjustsImageWhenHighlighted = false
to solve the problem. But this is deprecated. That's why I started using the configuration. I know that with the status "deprecated" it still works. But I have warnings. And I wanted to find another way, this was the configuration.
Your problem come from use of isHighlighted
's property, as Apple indicate in their docs:
Controls automatically set and clear this state in response to appropriate touch events. You can change the value of this property as needed to apply or remove a highlight programmatically
When your finger go inside button
, isHighlighted
will be true and vice-versa.
In ViewController
, you only add target to event .touchUpInside
so your function will only be called when users lift their finger in side button
.
To fix your problem, we need to change button's image
whenever it receive .touchUpInside
event, instead of depend on isHighlighted
to change button image, you should use isSelected
and toggle it when button
got touch up inside:
class TestViewController: UIViewController {
var button: SomeButtonWithImage = {
let button = SomeButtonWithImage(frame: .init(x: 100, y: 200, width: 100, height: 100))
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
button.normalImage = "mute"
button.selectedImage = "unmute"
button.isSelected = false // <- set intial isSelected to render image
button.addTarget(self, action: #selector(self.rightButtonAction), for: .touchUpInside)
view.addSubview(button)
}
@objc func rightButtonAction() {
button.isSelected.toggle() // <-- toggle button isSelected here
}
}
class SomeButtonWithImage: UIButton {
var buttonSelectedScale: CGFloat = 0.9
var buttonScaleDownDuration: TimeInterval = 0.15
var buttonScaleUpDuration: TimeInterval = 0.25
public var normalImage: String = ""
public var selectedImage: String = ""
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .red
configuration = .plain()
configuration?.baseBackgroundColor = .clear
}
override var isSelected: Bool {
didSet {
if isSelected {
configuration?.image = UIImage(named: self.selectedImage)
} else {
configuration?.image = UIImage(named: self.normalImage)
}
}
}
override var isHighlighted: Bool {
didSet {
print(isHighlighted)
}
}
required init?(coder: NSCoder) { fatalError("error") }
private func animateScale(to scale: CGFloat, duration: TimeInterval) {
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: {
self.transform = .init(scaleX: scale, y: scale)
}, completion: nil)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
animateScale(to: buttonSelectedScale, duration: buttonScaleDownDuration)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
animateScale(to: 1, duration: buttonScaleDownDuration)
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
animateScale(to: 1, duration: buttonScaleDownDuration)
}
}