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

Mac Catalyst下Swift自定义分段控件悬停与显示异常问题

自定义分段控件显示异常及悬停问题修复(Mac Catalyst)

问题现象

在Mac Catalyst平台开发自定义分段控件时,出现以下问题:

  • 第二个按钮显示为第三个按钮的文本内容
  • 第三个按钮完全不可见
  • 鼠标悬停在第二个按钮的预期位置时,仍能正常触发悬停效果

问题根源分析

  1. 多余的中间视图层导致布局偏移:原代码中setUpView方法创建了额外的中间视图,再将按钮容器holderView添加进去,且holderView的x坐标设为100,若控件本身宽度不足,会导致右侧按钮被裁剪。
  2. 硬编码宽度的精度问题:按钮宽度使用163.3333这类浮点数硬编码,容易因布局精度误差导致按钮重叠或偏移。
  3. Frame布局的不稳定性:按钮内的label使用frame手动布局,在复杂场景下容易出现位置偏移,导致文本显示错位。

修复方案

1. 移除多余中间视图层

直接将holderView添加到自定义控件本身,避免额外的布局层级带来的偏移问题。

2. 动态计算按钮宽度

根据容器宽度平均分配按钮宽度,避免硬编码浮点数的精度问题。

3. 使用AutoLayout布局label

替换手动frame布局,确保label始终居中显示,避免文本错位。

4. 确保控件宽度足够容纳所有按钮

初始化自定义控件时,设置足够的宽度以显示完整的分段按钮。

修改后的完整代码

class MySegmentedControl: UIView {
    var state = MySegmentedControl_State.unknown
    var buttons = [MySegmentedControl_ButtonView]()
    
    func setUpView(){
        // 移除多余中间层,直接添加holderView到当前控件
        let holder = holderView()
        holder.frame = self.bounds // 让容器填满整个控件
        self.addSubview(holder)
    }
    
    func holderView() -> UIView{
        let view = UIView(frame: self.bounds)
        view.layer.borderWidth = 5
        view.layer.borderColor = UIColor.gray.cgColor
        view.layer.cornerRadius = view.frame.height / 2
        view.layer.masksToBounds = true
        view.clipsToBounds = true
        view.backgroundColor = #colorLiteral(red: 0.5741485357, green: 0.5741624236, blue: 0.574154973, alpha: 1)
        
        // 动态计算按钮宽度,避免硬编码精度问题
        let buttonWidth = view.bounds.width / 3
        
        // 创建三个按钮
        let button_1 = MySegmentedControl_ButtonView(text: "One", frame: CGRect(x: 0, y: 0, width: buttonWidth, height: view.bounds.height), type: .one, delegate: self)
        let button_2 = MySegmentedControl_ButtonView(text: "Two", frame: CGRect(x: buttonWidth, y: 0, width: buttonWidth, height: view.bounds.height), type: .two, delegate: self)
        let button_3 = MySegmentedControl_ButtonView(text: "Three", frame: CGRect(x: buttonWidth*2, y: 0, width: buttonWidth, height: view.bounds.height), type: .three, delegate: self)
        
        buttons.append(button_1); buttons.append(button_2); buttons.append(button_3)
        view.addSubview(button_1)
        view.addSubview(button_2)
        view.addSubview(button_3)
        
        return view
    }
    
    class MySegmentedControl_ButtonView: UIView{
        private var selected = false
        private var hovering = false
        var text = ""
        private var type: MySegmentedControl_State
        private var delegate: MySegmentedControl_ButtonView_Delegate
        private var label = UILabel()
        
        func setUpView(){
            layer.cornerRadius = frame.height/2
            
            // 使用AutoLayout布局label,确保居中
            let label = UILabel()
            self.label = label
            label.tintColor = .white
            label.text = text
            label.font = label.font.withSize(15)
            label.translatesAutoresizingMaskIntoConstraints = false
            addSubview(label)
            
            NSLayoutConstraint.activate([
                label.centerXAnchor.constraint(equalTo: self.centerXAnchor),
                label.centerYAnchor.constraint(equalTo: self.centerYAnchor)
            ])
            
            // 添加悬停手势
            let regionizer = UIHoverGestureRecognizer(target: self, action: #selector(didHover(_:)))
            addGestureRecognizer(regionizer)
            
            // 创建点击按钮,明确设置透明背景避免遮挡label
            let button = UIButton(frame: bounds, primaryAction: UIAction(handler: { [self] action in
                selected = true
                delegate.shouldChangeState(to: type)
                delegate.shouldDeselectOthers(without: text)
                backgroundColor = hovering ? #colorLiteral(red: 0.7540688515, green: 0.7540867925, blue: 0.7540771365, alpha: 0.9435433103) : #colorLiteral(red: 0.9961533629, green: 0.9931518435, blue: 1, alpha: 0.8)
            }))
            button.backgroundColor = .clear
            addSubview(button)
        }
        
        func deselect(){
            self.selected = false
            let bgColor = hovering ? #colorLiteral(red: 0.7540688515, green: 0.7540867925, blue: 0.7540771365, alpha: 0.15) : #colorLiteral(red: 0.2605174184, green: 0.2605243921, blue: 0.260515637, alpha: 0.15)
            backgroundColor = bgColor
            
            UIView.animate(withDuration: 0.2) {[self]in
                label.frame.origin.y = 10
                label.font = label.font.withSize(12)
            }
        }
        
        @objc
        func didHover(_ recognizer: UIHoverGestureRecognizer){
            switch recognizer.state{
            case .began, .changed:
                hovering = true
                let bgColor = selected ? #colorLiteral(red: 0.7540688515, green: 0.7540867925, blue: 0.7540771365, alpha: 0.15) : #colorLiteral(red: 0.2605174184, green: 0.2605243921, blue: 0.260515637, alpha: 0.15)
                backgroundColor = bgColor
                
                UIView.animate(withDuration: 0.2) {[self]in
                    label.frame.origin.y = 10
                    label.font = label.font.withSize(12)
                }
                
            case .ended:
                hovering = false
                let bgColor = selected ? #colorLiteral(red: 0.9961533629, green: 0.9931518435, blue: 1, alpha: 0.2) : .clear
                backgroundColor = bgColor
                
                UIView.animate(withDuration: 0.2) {[self]in
                    label.center.y = center.y
                    label.font = label.font.withSize(15)
                }
            default:break
            }
        }
        
        init(text: String, frame: CGRect, type: MySegmentedControl_State, delegate: MySegmentedControl_ButtonView_Delegate){
            self.type = type
            self.delegate = delegate
            super.init(frame: frame)
            self.text = text
            setUpView()
        }
        
        required init?(coder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    }
}

extension MySegmentedControl: MySegmentedControl_ButtonView_Delegate{
    func shouldDeselectOthers(without: String) {
        buttons.forEach { button in
            if button.text != without {
                button.deselect()
            }
        }
    }
    
    func shouldChangeState(to state: MySegmentedControl_State) {
        self.state = state
    }
}

protocol MySegmentedControl_ButtonView_Delegate{
    func shouldDeselectOthers(without: String)
    func shouldChangeState(to state: MySegmentedControl_State)
}

enum MySegmentedControl_State: String{
    case unknown = "Non specific case avalaible"
    case one = "One selected"
    case two = "Two selected"
    case three = "Three selected"
}

使用示例

初始化控件时确保宽度足够容纳所有按钮:

let segmentedControl = MySegmentedControl(frame: CGRect(x: 50, y: 100, width: 490, height: 70))
segmentedControl.setUpView()
view.addSubview(segmentedControl)

内容的提问来源于stack exchange,提问作者Michaelll

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 06:40:52