SwiftData模型首次保存后仅含print才生效?求原因与优化方案
SwiftData中AttributedStringModel修改不保存的问题分析与解决
问题现象
开发基于SwiftData的文档型应用,采用苹果WWDC演示模板存储AttributedString数据时,首次保存完成后,只有在value的didSet闭包中加入print("old value: \(oldValue)")调用,AttributedStringModel才会保存字符串的修改;移除该打印语句后,修改无法被持久化。
相关代码
import SwiftData import Foundation /// A SwiftData model for storing attributed strings, preserving all attributes /// in a configurable attribute scope. @Model final class AttributedStringModel: Identifiable { @Transient lazy var value: AttributedString = initializeValue() { didSet { print("old value: \(oldValue)") valueChanged = true } } @Transient private var valueChanged: Bool = false @Attribute(.externalStorage) private var data: Data private var scope: Scope /// Create a new SwiftData model for the given attributed string. /// /// The model preserves any attributes in the given scope when SwiftData /// persists the model in between app launches. init(value: AttributedString, scope: Scope) { self.data = Data() self.scope = scope self.value = value // Add observer for the case where the app's logic manually instantiates // the model. NotificationCenter.default.addObserver( self, selector: #selector(willSave), name: ModelContext.willSave, object: nil) } private func initializeValue() -> AttributedString { // Add observer for the case where SwiftData's synthesized initializer // instantiates the model. NotificationCenter.default.addObserver( self, selector: #selector(willSave), name: ModelContext.willSave, object: nil) do { let string = try JSONDecoder().decode( AttributedString.self, from: data, configuration: scope.decodingConfiguration) // Real-world apps may need better logging infrastructure. return string } catch { // Real-world apps may need better error handling. print(error) return "" } } @objc private func willSave() { guard valueChanged else { return } valueChanged = false do { self.data = try JSONEncoder().encode( value, configuration: scope.encodingConfiguration) print("will save \(value)") // Real-world apps may need better logging infrastructure. } catch { // Real-world apps may need better error handling. print(error) } } } extension AttributedStringModel { /// An enum listing all the attribute scopes that this app's attributed /// string SwiftData model can serialize. enum Scope: String, Codable { /// The case representing `AttributeScopes.EntryModelAttributes`. case entry /// The case representing `AttributeScopes.LinkNameAttributes`. case link fileprivate var encodingConfiguration: AttributeScopeCodableConfiguration { switch self { case .entry: AttributeScopes.EntryModelAttributes.encodingConfiguration case .link: AttributeScopes.LinkNameAttributes.encodingConfiguration } } fileprivate var decodingConfiguration: AttributeScopeCodableConfiguration { switch self { case .entry: AttributeScopes.EntryModelAttributes.decodingConfiguration case .link: AttributeScopes.LinkNameAttributes.decodingConfiguration } } } }
问题原因
这个现象由Swift编译器优化和AttributedString的值类型特性共同导致:
- 当
didSet闭包中未访问oldValue时,编译器会对闭包进行优化,在首次保存后的修改场景中,可能跳过didSet的完整执行,导致valueChanged始终为false,willSave方法因此不会执行data的编码逻辑,修改无法被持久化。 - 访问
oldValue会强制Swift生成旧值的完整副本,阻止了编译器的优化行为,确保didSet内的valueChanged = true代码被执行,进而触发后续的保存逻辑。
优化解决方案
方案1:空访问oldValue(最小改动)
不需要打印oldValue,仅在didSet中访问该变量即可阻止编译器优化,同时避免控制台输出:
@Transient lazy var value: AttributedString = initializeValue() { didSet { _ = oldValue // 仅访问oldValue,确保didSet完整执行 valueChanged = true } }
方案2:改用计算属性(更健壮)
抛弃原有的lazy var+didSet+willSave的间接逻辑,将value改为计算属性,直接在setter中完成编码,逻辑更清晰且避免优化问题:
import SwiftData import Foundation @Model final class AttributedStringModel: Identifiable { @Attribute(.externalStorage) private var data: Data private var scope: Scope /// 直接关联data的计算属性,实时处理编码解码 var value: AttributedString { get { do { return try JSONDecoder().decode( AttributedString.self, from: data, configuration: scope.decodingConfiguration) } catch { print(error) return "" } } set { do { data = try JSONEncoder().encode( newValue, configuration: scope.encodingConfiguration) } catch { print(error) } } } init(value: AttributedString, scope: Scope) { self.scope = scope // 初始化时直接编码值到data do { self.data = try JSONEncoder().encode( value, configuration: scope.encodingConfiguration) } catch { self.data = Data() print(error) } } // 移除原有的lazy var、valueChanged、willSave方法及通知观察者 } // 保留原有的Scope扩展 extension AttributedStringModel { enum Scope: String, Codable { case entry case link fileprivate var encodingConfiguration: AttributeScopeCodableConfiguration { switch self { case .entry: AttributeScopes.EntryModelAttributes.encodingConfiguration case .link: AttributeScopes.LinkNameAttributes.encodingConfiguration } } fileprivate var decodingConfiguration: AttributeScopeCodableConfiguration { switch self { case .entry: AttributeScopes.EntryModelAttributes.decodingConfiguration case .link: AttributeScopes.LinkNameAttributes.decodingConfiguration } } } }
该方案优势:
- 逻辑直接,修改
value时实时更新data,无需依赖willSave通知和状态标记。 - 避免了
lazy var和didSet带来的编译器优化问题,同时减少了冗余的通知注册逻辑。
内容的提问来源于stack exchange,提问作者Andy Shamel
相关产品推荐
相关产品推荐

