You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何修复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方案。


问题分析

  1. 状态切换逻辑错误:当前代码在isHighlighted变为false时(无论用户是在按钮内抬起手指,还是滑出按钮后松开),都会调用deselected()方法切换选中状态,但只有touchUpInside事件才应该触发静音功能和状态变更。
  2. 高亮状态误触发状态变更:手指在按钮内外移动时,isHighlighted反复切换,导致isSelected被频繁触发,引发图片乱变。
  3. 配置更新逻辑不完善: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 00:34:53