如何将动态缩放后的UIFont还原为原始UIFont?
我正在开发一款涉及NSAttributedString编解码的文本编辑器App,使用UITextView展示和编辑文本,通过NSKeyedArchiver实现了NSAttributedString的Codable适配,代码如下:
class AttributedStringCodable : Codable { var attributedString : NSAttributedString init(nsAttributedString : NSAttributedString) { self.attributedString = nsAttributedString } public required init(from decoder: Decoder) throws { let singleContainer = try decoder.singleValueContainer() guard let attributedString = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(singleContainer.decode(Data.self)) as? NSAttributedString else { throw DecodingError.dataCorruptedError(in: singleContainer, debugDescription: "Data is corrupted") } self.attributedString = attributedString } public func encode(to encoder: Encoder) throws { var singleContainer = encoder.singleValueContainer() try singleContainer.encode(NSKeyedArchiver.archivedData(withRootObject: attributedString, requiringSecureCoding: false)) } }
App需要支持动态字体,当用户在设置中调整字体大小时,富文本会随之放大。但保存(编码)NSAttributedString时,会连带保存动态缩放后的字体,导致下次以不同动态字体设置加载(解码)时,App崩溃并提示**"A scaled font returned by UIFontMetrics should not be used to create another scaled font."**
已知崩溃原因是尝试从已缩放的字体再次生成缩放字体,因此希望在保存NSAttributedString前将字体还原为原始字体。示例代码如下:
let originalFont = UIFont.systemFont(ofSize: 24, weight: .bold) let traitCollection = UITraitCollection(preferredContentSizeCategory: .accessibilityLarge) let dynamicFont = UIFontMetrics.default.scaledFont(for: originalFont, compatibleWith: traitCollection) print(originalFont.pointSize) print(dynamicFont.pointSize)
请问如何将dynamicFont还原为originalFont?是否有其他实现思路?
一、从缩放字体还原原始字体
方式1:通过缩放系数反向计算
缩放后的字体大小是原始字体乘以当前系统的缩放系数,我们可以反向推导原始字体:
extension UIFont { func originalFont() -> UIFont? { let currentScale = UIFontMetrics.default.scaledValue(for: 1.0) let originalSize = self.pointSize / currentScale return UIFont(descriptor: self.fontDescriptor, size: originalSize) } } // 使用示例 if let restoredFont = dynamicFont.originalFont() { // 得到原始字体 }
注意:该方法依赖当前系统的缩放系数,若保存时的缩放比例与创建动态字体时的比例不一致,结果会有偏差。
方式2:自定义属性存储原始字体
在应用动态字体时,将原始字体存入NSAttributedString的自定义属性,保存前再提取替换:
// 定义自定义属性键 let originalFontAttributeKey = NSAttributedString.Key("OriginalFont") // 应用动态字体时同步存储原始字体 let mutableAttrStr = NSMutableAttributedString(string: "测试文本") mutableAttrStr.addAttribute(.font, value: dynamicFont, range: NSRange(location: 0, length: mutableAttrStr.length)) mutableAttrStr.addAttribute(originalFontAttributeKey, value: originalFont, range: NSRange(location: 0, length: mutableAttrStr.length)) // 保存前还原字体 func restoreOriginalFonts(in attrStr: NSMutableAttributedString) { attrStr.enumerateAttributes(in: NSRange(location: 0, length: attrStr.length), options: []) { attrs, range, _ in if let originalFont = attrs[originalFontAttributeKey] as? UIFont { attrStr.addAttribute(.font, value: originalFont, range: range) attrStr.removeAttribute(originalFontAttributeKey, range: range) } } } // 调用还原 restoreOriginalFonts(in: mutableAttrStr)
这种方式能100%还原原始字体,是最可靠的方案。
二、优化编解码流程,从根源避免问题
1. 编码时自动替换为原始字体
修改AttributedStringCodable的编码逻辑,在归档前遍历替换所有缩放字体:
public func encode(to encoder: Encoder) throws { let mutableAttrStr = self.attributedString.mutableCopy() as! NSMutableAttributedString mutableAttrStr.enumerateAttributes(in: NSRange(location: 0, length: mutableAttrStr.length), options: []) { attrs, range, _ in if let font = attrs[.font] as? UIFont, let originalFont = font.originalFont() { mutableAttrStr.addAttribute(.font, value: originalFont, range: range) } } let data = NSKeyedArchiver.archivedData(withRootObject: mutableAttrStr, requiringSecureCoding: false) var singleContainer = encoder.singleValueContainer() try singleContainer.encode(data) }
2. 解码后重新应用动态字体
保存时只存原始字体,解码后根据当前系统设置重新生成动态字体:
public required init(from decoder: Decoder) throws { let singleContainer = try decoder.singleValueContainer() guard let attributedString = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(singleContainer.decode(Data.self)) as? NSAttributedString else { throw DecodingError.dataCorruptedError(in: singleContainer, debugDescription: "Data is corrupted") } let mutableAttrStr = attributedString.mutableCopy() as! NSMutableAttributedString mutableAttrStr.enumerateAttributes(in: NSRange(location: 0, length: mutableAttrStr.length), options: []) { attrs, range, _ in if let originalFont = attrs[.font] as? UIFont { let dynamicFont = UIFontMetrics.default.scaledFont(for: originalFont) mutableAttrStr.addAttribute(.font, value: dynamicFont, range: range) } } self.attributedString = mutableAttrStr }
这种方式彻底避免了重复缩放的问题,同时保证每次加载都适配当前系统字体设置。
三、使用系统原生动态字体描述符
直接创建支持动态缩放的字体描述符,无需手动调用UIFontMetrics,系统会自动适配:
// 创建带文本样式的字体描述符 let originalDescriptor = UIFont.systemFont(ofSize: 24, weight: .bold).fontDescriptor.addingAttributes([ UIFontDescriptor.AttributeName.textStyle: UIFont.TextStyle.body.rawValue ]) // size传0会自动根据文本样式和系统设置生成对应大小 let dynamicFont = UIFont(descriptor: originalDescriptor, size: 0)
该字体保存后,加载时会自动适配当前系统字体大小,不会出现重复缩放崩溃的问题,适合使用系统文本样式的场景。
内容的提问来源于stack exchange,提问作者FE_Tech

