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

在AppStorage中设置自定义Struct值时应用崩溃,求排查解决

Budget结构体配合@AppStorage使用时崩溃问题分析

问题场景

定义了同时实现Codable和RawRepresentable的Budget结构体,用于@AppStorage存储:

struct Budget: Codable, RawRepresentable {

    enum CodingKeys: String, CodingKey {
        case total, spent
    }

    var total: Double
    var spent: Double

    init(total: Double = 5000.0, spent: Double = 3000.0) {
        self.total = total
        self.spent = spent
    }

    init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode(Budget.self, from: data)
        else { return nil }

        self = result
    }

    var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return ""
        }

        return result
    }

}

使用该结构体的视图代码:

struct DemoView: View {

    @AppStorage(UserDefaults.StorageKeys.budget.rawValue) var budget = Budget()

    var body: some View {
        Button("Update") {
            budget.total = 10
        }
    }
}

点击按钮时,应用在Budget.rawValue的guard let data = try? JSONEncoder().encode(self)处崩溃,报错Thread 1: EXC_BAD_ACCESS。

崩溃原因

崩溃根源是无限递归调用:
当@AppStorage需要将Budget实例转换为rawValue存储时,会调用rawValue计算属性,而该属性内部通过JSONEncoder.encode(self)来序列化实例。但JSONEncoder在编码Budget实例时,触发的默认Codable合成逻辑会间接再次触发rawValue的访问,形成无限递归,最终耗尽栈空间引发内存访问错误。

解决方案

有两种可行的修复方案:

方案一:手动实现Codable的编码/解码逻辑

明确指定编码和解码仅操作total和spent存储属性,避免默认合成逻辑引发的递归:

struct Budget: Codable, RawRepresentable {
    enum CodingKeys: String, CodingKey {
        case total, spent
    }

    var total: Double
    var spent: Double

    init(total: Double = 5000.0, spent: Double = 3000.0) {
        self.total = total
        self.spent = spent
    }

    // RawRepresentable实现
    init?(rawValue: String) {
        guard let data = rawValue.data(using: .utf8),
              let result = try? JSONDecoder().decode(Budget.self, from: data)
        else { return nil }
        self = result
    }

    var rawValue: String {
        guard let data = try? JSONEncoder().encode(self),
              let result = String(data: data, encoding: .utf8)
        else {
            return ""
        }
        return result
    }

    // 手动实现Codable编码,仅处理存储属性
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(total, forKey: .total)
        try container.encode(spent, forKey: .spent)
    }

    // 手动实现Codable解码,仅处理存储属性
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        total = try container.decode(Double.self, forKey: .total)
        spent = try container.decode(Double.self, forKey: .spent)
    }
}

方案二:简化RawRepresentable实现,绕过Codable

直接通过字符串拼接/拆分实现RawRepresentable,完全避免使用JSONEncoder/JSONDecoder,从根源上消除递归可能:

struct Budget: Codable, RawRepresentable {
    var total: Double
    var spent: Double

    init(total: Double = 5000.0, spent: Double = 3000.0) {
        self.total = total
        self.spent = spent
    }

    init?(rawValue: String) {
        let components = rawValue.split(separator: ",")
        guard components.count == 2,
              let total = Double(components[0]),
              let spent = Double(components[1])
        else { return nil }
        self.total = total
        self.spent = spent
    }

    var rawValue: String {
        return "\(total),\(spent)"
    }
}

内容的提问来源于stack exchange,提问作者Kex

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.15 08:25:14