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

Swift6.3+iOS26:如何为UISegmentedControl选中项添加边框

解决方案

问题根源

iOS 13+ 后UISegmentedControl的内部视图结构发生了变化,直接遍历subviews无法获取到单个分段的视图,导致你遍历到的只有一个容器视图,因此index始终为0。同时直接操作私有视图结构在后续iOS版本中可能出现兼容性问题。

修改后的完整代码

enum DemoAppTab: CaseIterable, CustomStringConvertible {
    case info
    case backgrounds
    case photos
    case links
    
    var description: String {
        switch self {
        case .info: return "Info"
        case .backgrounds: return "Backgrounds"
        case .photos: return "Photos"
        case .links: return "Links"
        }
    }
}

// 扩展UISegmentedControl获取分段视图的frame
extension UISegmentedControl {
    func segmentFrame(at index: Int) -> CGRect {
        guard index >= 0, index < numberOfSegments else { return .zero }
        // 通过KVC获取私有分段视图数组(注意:私有API存在审核风险,若担心可改用计算方式)
        let segments = value(forKey: "segments") as? [UIView]
        return segments?[index].frame ?? .zero
    }
}

fileprivate struct CustomTabBar<T: Hashable & Equatable>: UIViewRepresentable {
    var tint: Color = .gray.opacity(0.15)
    var items: [T]
    var titleProvider: (T) -> String
    @Binding var selectedItem: T

    func makeUIView(context: Context) -> UISegmentedControl {
        let titles = items.map { titleProvider($0) }
        let control = UISegmentedControl(items: titles)
        let initialIndex = items.firstIndex(of: selectedItem) ?? 0
        control.selectedSegmentIndex = initialIndex
        
        // 基础样式配置:透明背景、透明选中 tint、移除分隔线
        control.backgroundColor = .clear
        control.selectedSegmentTintColor = .clear
        control.setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
        control.apportionsSegmentWidthsByContent = true
        
        // 设置文字样式
        control.setTitleTextAttributes([
            .foregroundColor: UIColor.gray,
            .font: UIFont.systemFont(ofSize: 14)
        ], for: .normal)
        control.setTitleTextAttributes([
            .foregroundColor: UIColor.black,
            .font: UIFont.systemFont(ofSize: 14, weight: .medium)
        ], for: .selected)
        
        control.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged(_:)), for: .valueChanged)
        
        DispatchQueue.main.async {
            applyBorder(to: control)
        }
        
        return control
    }
    
    func updateUIView(_ uiView: UISegmentedControl, context: Context) {
        let newIndex = items.firstIndex(of: selectedItem) ?? 0
        if uiView.selectedSegmentIndex != newIndex {
            uiView.selectedSegmentIndex = newIndex
            // 选中项变化时重新绘制边框
            applyBorder(to: uiView)
        }
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(parent: self)
    }
    
    class Coordinator: NSObject {
        var parent: CustomTabBar<T>
        
        init(parent: CustomTabBar<T>) {
            self.parent = parent
        }
        
        @objc func valueChanged(_ sender: UISegmentedControl) {
            let newIndex = sender.selectedSegmentIndex
            if newIndex >= 0 && newIndex < parent.items.count {
                parent.selectedItem = parent.items[newIndex]
            }
        }
    }
    
    private func applyBorder(to control: UISegmentedControl) {
        // 移除之前的边框视图(避免重复添加)
        control.subviews.first { $0.tag == 999 }?.removeFromSuperview()
        
        let selectedIndex = control.selectedSegmentIndex
        guard selectedIndex >= 0, selectedIndex < control.numberOfSegments else { return }
        
        // 获取选中分段的frame,添加内边距调整边框位置
        let segmentFrame = control.segmentFrame(at: selectedIndex).insetBy(dx: 2, dy: 2)
        let borderView = UIView(frame: segmentFrame)
        borderView.tag = 999 // 标记用于后续移除
        borderView.layer.borderColor = UIColor.gray.cgColor
        borderView.layer.borderWidth = 1.5
        borderView.layer.cornerRadius = 6
        borderView.layer.masksToBounds = true
        borderView.backgroundColor = .clear
        
        control.addSubview(borderView)
        control.bringSubviewToFront(borderView)
    }
}

struct CustomSegmentedControlDemo: View {
    @State private var activeTab: DemoAppTab = .info
    
    var body: some View {
        VStack {
            Text("Selected: \(activeTab.description)")
                .padding()
            
            CustomTabBar(
                items: Array(DemoAppTab.allCases),
                titleProvider: { $0.description },
                selectedItem: $activeTab
            )
            .padding(.horizontal)
            
            Button("Goto Links") {
                activeTab = .links
            }
        }
    }
}

#Preview {
    CustomSegmentedControlDemo()
}

关键修改点

  1. 基础样式重置:

    • 设置backgroundColor和selectedSegmentTintColor为透明,移除默认背景和选中填充色
    • 调用setDividerImage移除分段间的分隔线
    • 配置正常/选中状态的文字颜色和字体
  2. 正确获取分段frame:

    • 通过扩展添加segmentFrame(at:)方法,利用KVC获取私有分段视图数组(若担心审核风险,可改用基于内容宽度的计算方式)
  3. 动态添加边框视图:

    • 在applyBorder中先移除旧边框视图,避免重复添加
    • 根据选中分段的frame创建自定义边框视图,添加到控件上层并标记tag
  4. 更新时同步边框:

    • 在updateUIView中,当选中项变化时重新调用applyBorder,确保边框跟随选中状态更新

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.01 17:54:53