如何修复iOS按钮图片随手指位置偏移的异常行为?
静音按钮状态异常问题修复
问题描述
我有一个控制声音静音/取消静音的按钮,配有正常状态(扬声器)和按下状态(带叉的扬声器)的SVG图片。正常点击时图片切换和功能执行都正常,但存在以下异常:
- 按下按钮后不抬起手指,将手指移出按钮区域再松开,图片会切换为带叉的扬声器,但静音功能未执行;
- 手指在按钮外和内来回移动时,图片也会随之切换。
控制器代码
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) } }
按钮类代码
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) } }
补充说明
为何使用configurationHandler而非setImage(UIImage?, for: UIControlState)?使用setImage时点击按钮图片会被灰色背景覆盖,曾用adjustsImageWhenHighlighted = false解决,但该属性已废弃,会产生警告,因此改用configuration方案。
问题分析
- 状态切换逻辑错误:当前代码在
isHighlighted变为false时(无论用户是在按钮内抬起手指,还是滑出按钮后松开),都会调用deselected()方法切换选中状态,但只有touchUpInside事件才应该触发静音功能和状态变更。 - 高亮状态误触发状态变更:手指在按钮内外移动时,
isHighlighted反复切换,导致isSelected被频繁触发,引发图片乱变。 - 配置更新逻辑不完善:
configurationUpdateHandler仅处理.normal和.selected状态,忽略了.highlighted状态,导致高亮时图片显示逻辑混乱。
修复方案
- 将
isSelected的切换逻辑从高亮回调中移除,绑定到touchUpInside事件处理方法,确保只有合法点击才触发状态变更和功能执行。 - 高亮状态仅处理按钮缩放动画,不修改选中状态。
- 调整
configurationUpdateHandler,根据isSelected直接决定显示图片,不受高亮状态影响。
修改后代码
按钮类代码
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 isHighlighted { selected() } else { deselected() } } } override init(frame: CGRect) { super.init(frame: frame) configuration = .plain() configuration?.baseBackgroundColor = .clear configurationUpdateHandler = { button in // 根据选中状态显示对应图片,不受高亮影响 let targetImageName = button.isSelected ? self.selectedImage : self.normalImage button.configuration?.image = UIImage(named: targetImageName) } } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func selected() { animateScale(to: buttonSelectedScale, duration: buttonScaleDownDuration) } func deselected() { animateScale(to: 1, duration: buttonScaleUpDuration) } // 移除选中状态切换 private func animateScale(to scale: CGFloat, duration: TimeInterval) { UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.5, options: [], animations: { self.transform = CGAffineTransform(scaleX: scale, y: scale) }, completion: nil) } }
控制器代码
class ViewController: UIViewController { var button: SomeButtonWithImage = { let button = SomeButtonWithImage() button.translatesAutoresizingMaskIntoConstraints = false return button }() override func viewDidLoad() { super.viewDidLoad() 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(rightButtonAction), for: .touchUpInside) rightStackView.addArrangedSubview(button) } @objc func rightButtonAction(_ sender: SomeButtonWithImage) { // 切换按钮选中状态 sender.isSelected.toggle() // 执行静音/取消静音逻辑 toggleMute() } private func toggleMute() { // 替换为你的实际静音控制代码 // 示例: // let audioSession = AVAudioSession.sharedInstance() // try? audioSession.setActive(!audioSession.isActive, options: .notifyOthersOnDeactivation) } }
补充说明
- 修复后仅当用户在按钮区域内完成点击(按下并抬起)时,才会切换状态并执行静音功能,滑出按钮后松开不会触发任何变更。
- 手指在按钮内外移动时仅触发缩放动画,不会改变按钮状态和显示图片。
configurationUpdateHandler的逻辑确保按钮在任何状态下都显示与功能匹配的图片,同时避免了setImage带来的灰色背景问题。
内容的提问来源于stack exchange,提问作者Alex Smith
相关产品推荐
相关产品推荐

