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

如何将动态缩放后的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 04:20:33