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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.05 06:14:51