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
相关产品推荐
相关产品推荐

