如何使UITextView中NSTextAttachment的图片尺寸在App重启后仍保持40×40
这个问题我之前也碰到过,核心原因是默认的NSAttributedStringTransformer没有完整保存NSTextAttachment的bounds属性——当你把attributedText存到CoreData再重启App加载时,attachment的尺寸信息丢失了,所以图片会用原始大小渲染,自然就超出屏幕了。
下面给你两种可行的解决思路:
思路一:加载时重新设置所有图片的尺寸
这个方法最简单,适合快速修复。在你从CoreData读取并设置textView的attributedText之后,遍历所有文本附件,强制把bounds改回40×40:
// 假设你从CoreData取出了savedAttributedText let mutableAttributedText = NSMutableAttributedString(attributedString: savedAttributedText) let range = NSRange(location: 0, length: mutableAttributedText.length) mutableAttributedText.enumerateAttribute(.attachment, in: range, options: []) { value, range, stop in guard let attachment = value as? NSTextAttachment else { return } // 重新设置固定尺寸 attachment.bounds = CGRect(x: 0, y: 0, width: 40, height: 40) } textView.attributedText = mutableAttributedText
优点是代码改动小,不需要修改CoreData的Transformer配置;缺点是如果以后要支持不同尺寸的图片,这个方法就不适用了。
思路二:自定义Transformer保存完整的Attachment信息
如果需要支持不同尺寸的图片,或者想从根源解决问题,可以自定义一个Transformer,让它序列化NSTextAttachment时同时保存bounds和image数据:
步骤1:创建自定义Transformer
import CoreData import UIKit class CustomAttributedStringTransformer: ValueTransformer { override class func transformedValueClass() -> AnyClass { return NSAttributedString.self } override class func allowsReverseTransformation() -> Bool { return true } override func transformedValue(_ value: Any?) -> Any? { guard let attributedText = value as? NSAttributedString else { return nil } // 用NSKeyedArchiver序列化,处理NSTextAttachment的自定义属性 do { let data = try NSKeyedArchiver.archivedData(withRootObject: attributedText, requiringSecureCoding: false) return data } catch { print("Failed to archive attributed text: \(error)") return nil } } override func reverseTransformedValue(_ value: Any?) -> Any? { guard let data = value as? Data else { return nil } do { let attributedText = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSAttributedString.self, from: data) return attributedText } catch { print("Failed to unarchive attributed text: \(error)") return nil } } } // 注册Transformer extension ValueTransformer { static let customAttributedStringTransformer = CustomAttributedStringTransformer() }
步骤2:在CoreData模型中配置Transformer
打开你的CoreData模型文件,找到对应的Transformable类型属性,在Inspector面板的"Transformer"字段中输入customAttributedStringTransformer(即你注册的Transformer名称)。
额外处理:让NSTextAttachment支持完整归档
默认的NSTextAttachment不会自动归档bounds,我们给它加个扩展实现NSSecureCoding:
extension NSTextAttachment: NSSecureCoding { static var supportsSecureCoding: Bool { return true } func encode(with coder: NSCoder) { coder.encode(image, forKey: "image") coder.encode(bounds, forKey: "bounds") } required init?(coder: NSCoder) { super.init(data: nil, ofType: nil) self.image = coder.decodeObject(forKey: "image") as? UIImage self.bounds = coder.decodeCGRect(forKey: "bounds") } }
这样归档NSAttributedString时,NSTextAttachment的bounds和image都会被完整保存,重启App加载后尺寸就不会丢失了。
小提示(iOS 15+适用)
如果你项目的最低版本是iOS 15+,可以用NSAttributedString.DocumentType.rtfd格式保存文本和附件,RTFD会完整保留所有格式信息,包括图片尺寸:
// 保存时 if let rtfdData = textView.attributedText?.rtfdFileWrapper(from: NSRange(location: 0, length: textView.attributedText.length), documentAttributes: [:])?.regularFileContents { // 把rtfdData存到CoreData } // 加载时 if let rtfdData = savedRtfdData, let fileWrapper = FileWrapper(regularFileWithContents: rtfdData), let attributedText = NSAttributedString(rtfdFileWrapper: fileWrapper, documentAttributes: nil) { textView.attributedText = attributedText }
这个方法不需要自定义Transformer,操作更简洁,适合高版本iOS项目。
内容的提问来源于stack exchange,提问作者mahan

