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

SwiftUI Segmented Picker iOS15+ Dynamic Type动态类型适配咨询

iOS15+ SwiftUI Segmented Picker动态类型适配方案

原有代码失效原因

  • 手动创建的UILabel实例不会被UISegmentedControl内部复用,在这些临时实例上配置adjustsFontForContentSizeCategory完全不生效
  • 通过UISegmentedControl.appearance()全局代理设置的titleTextAttributes是静态值,不会在系统动态类型变更时自动重新计算缩放后的字体
  • 缺少字体缩放范围限制逻辑,无法实现和SwiftUI原生.dynamicTypeSize(.medium ... .xxLarge)一致的裁剪规则

可直接复用的实现代码

基础工具扩展

import SwiftUI
import UIKit

extension DynamicTypeSize {
    /// 裁剪动态类型尺寸到指定范围
    func clamped(to range: ClosedRange<DynamicTypeSize>) -> DynamicTypeSize {
        if self < range.lowerBound { return range.lowerBound }
        if self > range.upperBound { return range.upperBound }
        return self
    }
    
    /// 转换为UIKit对应的UIContentSizeCategory
    var uiContentSizeCategory: UIContentSizeCategory {
        switch self {
        case .xSmall: return .extraSmall
        case .small: return .small
        case .medium: return .medium
        case .large: return .large
        case .xLarge: return .extraLarge
        case .xxLarge: return .extraExtraLarge
        case .xxxLarge: return .extraExtraExtraLarge
        case .accessibility1: return .accessibilityMedium
        case .accessibility2: return .accessibilityLarge
        case .accessibility3: return .accessibilityExtraLarge
        case .accessibility4: return .accessibilityExtraExtraLarge
        case .accessibility5: return .accessibilityExtraExtraExtraLarge
        @unknown default: return .large
        }
    }
    
    /// 从UIKit的UIContentSizeCategory初始化SwiftUI DynamicTypeSize
    init(uiContentSizeCategory: UIContentSizeCategory) {
        switch uiContentSizeCategory {
        case .extraSmall: self = .xSmall
        case .small: self = .small
        case .medium: self = .medium
        case .large: self = .large
        case .extraLarge: self = .xLarge
        case .extraExtraLarge: self = .xxLarge
        case .extraExtraExtraLarge: self = .xxxLarge
        case .accessibilityMedium: self = .accessibility1
        case .accessibilityLarge: self = .accessibility2
        case .accessibilityExtraLarge: self = .accessibility3
        case .accessibilityExtraExtraLarge: self = .accessibility4
        case .accessibilityExtraExtraExtraLarge: self = .accessibility5
        default: self = .large
        }
    }
}

自适应动态类型的Segmented组件

struct AdaptiveSegmentedControl: UIViewRepresentable {
    @Binding var selectedIndex: Int
    let options: [String]
    let baseFontFamily: String
    let baseFontSize: CGFloat
    let textStyle: UIFont.TextStyle
    let typeSizeRange: ClosedRange<DynamicTypeSize>
    let normalTextColor: UIColor
    let selectedTextColor: UIColor
    let selectedTintColor: UIColor
    
    func makeUIView(context: Context) -> UISegmentedControl {
        let control = UISegmentedControl(items: options)
        control.selectedSegmentTintColor = selectedTintColor
        control.addTarget(
            context.coordinator,
            action: #selector(Coordinator.valueChanged(_:)),
            for: .valueChanged
        )
        // 注册系统动态类型变更监听
        context.coordinator.observer = NotificationCenter.default.addObserver(
            forName: UIContentSizeCategory.didChangeNotification,
            object: nil,
            queue: .main
        ) { [weak control] _ in
            guard let control = control else { return }
            updateFontStyle(for: control)
        }
        updateFontStyle(for: control)
        return control
    }
    
    func updateUIView(_ uiView: UISegmentedControl, context: Context) {
        // 同步选项、选中状态更新
        if uiView.numberOfSegments != options.count {
            uiView.removeAllSegments()
            options.enumerated().forEach { idx, title in
                uiView.insertSegment(withTitle: title, at: idx, animated: false)
            }
        }
        uiView.selectedSegmentTintColor = selectedTintColor
        uiView.selectedSegmentIndex = selectedIndex
        updateFontStyle(for: uiView)
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator(selectedIndex: $selectedIndex)
    }
    
    /// 重新计算并应用缩放后的字体样式
    private func updateFontStyle(for control: UISegmentedControl) {
        // 获取裁剪后的动态类型尺寸对应的traitCollection
        let currentClampedSize = DynamicTypeSize(
            uiContentSizeCategory: UIApplication.shared.preferredContentSizeCategory
        ).clamped(to: typeSizeRange)
        let trait = UITraitCollection(preferredContentSizeCategory: currentClampedSize.uiContentSizeCategory)
        
        // 配置普通状态字体
        if let normalBaseFont = UIFont(name: "\(baseFontFamily)-Medium", size: baseFontSize) {
            let scaledNormalFont = UIFontMetrics(forTextStyle: textStyle)
                .scaledFont(for: normalBaseFont, compatibleWith: trait)
            control.setTitleTextAttributes([
                .font: scaledNormalFont,
                .foregroundColor: normalTextColor
            ], for: .normal)
        }
        
        // 配置选中状态字体
        if let selectedBaseFont = UIFont(name: "\(baseFontFamily)-Bold", size: baseFontSize) {
            let scaledSelectedFont = UIFontMetrics(forTextStyle: textStyle)
                .scaledFont(for: selectedBaseFont, compatibleWith: trait)
            control.setTitleTextAttributes([
                .font: scaledSelectedFont,
                .foregroundColor: selectedTextColor
            ], for: .selected)
        }
    }
    
    class Coordinator: NSObject {
        @Binding var selectedIndex: Int
        var observer: NSObjectProtocol?
        
        init(selectedIndex: Binding<Int>) {
            _selectedIndex = selectedIndex
        }
        
        @objc func valueChanged(_ sender: UISegmentedControl) {
            selectedIndex = sender.selectedSegmentIndex
        }
        
        deinit {
            if let observer = observer {
                NotificationCenter.default.removeObserver(observer)
            }
        }
    }
}

使用方式

直接替换你原来的Picker(.segmented)组件即可,参数完全匹配你现有的配置:

AdaptiveSegmentedControl(
    selectedIndex: $yourSelectedIndex, // 替换为你的选中状态绑定
    options: ["选项1", "选项2", "选项3"], // 替换为你的分段选项
    baseFontFamily: "HelveticaNeue",
    baseFontSize: 14,
    textStyle: .footnote,
    typeSizeRange: .medium ... .xxLarge, // 和原生.dynamicTypeSize缩放规则完全一致
    normalTextColor: UIColor(named: "TextSecondary")!,
    selectedTextColor: UIColor(named: "TextPrimary")!,
    selectedTintColor: UIColor(named: "optionsbkg")!
)

关键说明

  • 放弃全局appearance()静态配置方案,改为在组件内部监听系统动态类型变更通知,每次字体设置变化时实时重新计算缩放值,解决静态属性无法自动更新的问题
  • 不需要手动创建UILabel配置自适应属性,直接通过UIFontMetrics结合裁剪后的traitCollection计算字体大小,直接赋值给UISegmentedControl的标题属性,完全规避内部私有label无法访问的问题
  • 内置尺寸裁剪逻辑,当系统字体设置超出指定范围时自动取边界值计算,和SwiftUI原生动态类型限制行为完全对齐
  • 无私有API调用,兼容iOS15及以上所有系统版本

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 16:33:16