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

Swift中向UserDefaults存储非Codable结构体数组崩溃问题

问题:非Codable结构体数组存入UserDefaults崩溃

尝试将不兼容Codable的HabitConfiguration结构体数组存入UserDefaults,先转字典再存储,编译无错误但运行时崩溃。


待存储的结构体模型(非Codable)

struct HabitConfiguration {
    var name: String = ""
    var description: String = ""
    var appearance: String = "planet"
    var planetColor: Color = Color("PlanetColor1")
    var planetHasRing: Bool = false
    var planetRingStyle = PlanetRingStyle()
    var planetRotation: Double = 0
    var emoji: String = "🚦"
    var repetitionType: RepetitionType = .unique(date: [UniqueDate(day: 1, month: 1, time: Time(hours: 12, minutes: 0))])
}

关联依赖代码

struct PlanetRingStyle {
    var lineWidth: Double = 5
    var lineCap: CGLineCap = .round
    var dash: [CGFloat] = [1]
}

enum RepetitionType {
    case daily(time: Time)
    case weekly(days: Days, time: Time)
    case monthly(dates: DaysNumbers, time: Time)
    case unique(date: [UniqueDate])
}

extension RepetitionType {
    var isDaily: Bool {
        if case .daily = self { return true }
        return false
    }
    var isWeekly: Bool {
        if case .weekly = self { return true }
        return false
    }
    var isMonthly: Bool {
        if case .monthly = self { return true }
        return false
    }
    var isUnique: Bool {
        if case .unique = self { return true }
        return false
    }
}

当前字典转换实现(存在问题)

extension HabitConfiguration {
    func toDictionary() -> [String: Any] {
        return [
        "name": name,
        "description": description,
        "appearance": appearance,
        "planetColor": planetColor.description,
        "planetHasRing": planetHasRing,
        "planetRingStyle": planetRingStyle,
        "planetRotation": planetRotation,
        "emoji": emoji,
        "repetitionType": repetitionType
        ]
    }

    init?(from dictionary: [String: Any]) {
        guard
        let name = dictionary["name"] as? String,
        let description = dictionary["description"] as? String,
        let appearance = dictionary["appearance"] as? String,
        let planetColorDescription = dictionary["planetColor"] as? String,
        let planetHasRing = dictionary["planetHasRing"] as? Bool,
        let planetRingStyle = dictionary["planetRingStyle"] as? Int,
        let planetRotation = dictionary["planetRotation"] as? Double,
        let emoji = dictionary["emoji"] as? String,
        let repetitionType = dictionary["repetitionType"] as? RepetitionType
        else {
            return nil
        }

        self.name = name
        self.description = description
        self.appearance = appearance
        self.planetColor = Color(planetColorDescription)
        self.planetHasRing = planetHasRing
        self.planetRingStyle = PlanetRingStyle(lineWidth: 0, lineCap: .round, dash: [0])
        self.planetRotation = planetRotation
        self.emoji = emoji
        self.repetitionType = repetitionType
    }
}

UserDefaults读写代码

struct HabitsList {
    private let habitsKey = "habitsList"

    func loadHabits() -> [HabitConfiguration] {
        guard let savedHabits = UserDefaults.standard.array(forKey: habitsKey) as? [[String: Any]] else {
            return [] 
        }
        return savedHabits.compactMap { HabitConfiguration(from: $0) }
    }

    func saveHabits(_ habits: [HabitConfiguration]) {
        let habitDictionaries = habits.map { $0.toDictionary() }
        UserDefaults.standard.set(habitDictionaries, forKey: habitsKey)
    }

    func habitsList() -> Binding<[HabitConfiguration]> {
        Binding(
            get: { self.loadHabits() },
            set: { self.saveHabits($0) }
        )
    }
}

崩溃原因及修复方案

核心问题

UserDefaults仅支持存储属性列表类型(NSString、NSNumber、NSDate、NSData、NSArray、NSDictionary),当前代码直接将PlanetRingStyle、RepetitionType这类自定义结构体/枚举存入字典,无法被UserDefaults序列化,导致运行时崩溃;同时字典转结构体的代码存在类型转换错误(比如把PlanetRingStyle强制转成Int)。

分步修复

1. 为自定义类型实现属性列表兼容的序列化/反序列化

处理PlanetRingStyle

给结构体添加字典转换方法,将所有属性转为UserDefaults支持的类型:

extension PlanetRingStyle {
    func toDictionary() -> [String: Any] {
        return [
            "lineWidth": lineWidth,
            "lineCap": lineCap.rawValue, // CGLineCap的rawValue为Int类型
            "dash": dash.map { $0 as Double } // 转Double避免CGFloat兼容性问题
        ]
    }

    init?(from dictionary: [String: Any]) {
        guard let lineWidth = dictionary["lineWidth"] as? Double,
              let lineCapRaw = dictionary["lineCap"] as? Int,
              let dashValues = dictionary["dash"] as? [Double] else {
            return nil
        }
        self.lineWidth = lineWidth
        self.lineCap = CGLineCap(rawValue: lineCapRaw) ?? .round
        self.dash = dashValues.map { CGFloat($0) }
    }
}
处理RepetitionType

枚举需转为包含类型标识和关联值的字典再存储:

extension RepetitionType {
    func toDictionary() -> [String: Any] {
        switch self {
        case .daily(let time):
            return ["type": "daily", "time": time.toDictionary()]
        case .weekly(let days, let time):
            return ["type": "weekly", "days": days.toDictionary(), "time": time.toDictionary()]
        case .monthly(let dates, let time):
            return ["type": "monthly", "dates": dates.toDictionary(), "time": time.toDictionary()]
        case .unique(let dates):
            return ["type": "unique", "dates": dates.map { $0.toDictionary() }]
        }
    }

    init?(from dictionary: [String: Any]) {
        guard let type = dictionary["type"] as? String else { return nil }
        switch type {
        case "daily":
            guard let timeDict = dictionary["time"] as? [String: Any], let time = Time(from: timeDict) else { return nil }
            self = .daily(time: time)
        case "weekly":
            guard let daysDict = dictionary["days"] as? [String: Any], let days = Days(from: daysDict),
                  let timeDict = dictionary["time"] as? [String: Any], let time = Time(from: timeDict) else { return nil }
            self = .weekly(days: days, time: time)
        case "monthly":
            guard let datesDict = dictionary["dates"] as? [String: Any], let dates = DaysNumbers(from: datesDict),
                  let timeDict = dictionary["time"] as? [String: Any], let time = Time(from: timeDict) else { return nil }
            self = .monthly(dates: dates, time: time)
        case "unique":
            guard let dateDicts = dictionary["dates"] as? [[String: Any]] else { return nil }
            let dates = dateDicts.compactMap { UniqueDate(from: $0) }
            guard !dates.isEmpty else { return nil }
            self = .unique(date: dates)
        default:
            return nil
        }
    }
}

注意:需给Time、Days、DaysNumbers、UniqueDate这些关联类型也添加对应的toDictionary()和init?(from:)方法,确保它们能转为属性列表类型。

2. 修正HabitConfiguration的字典转换

extension HabitConfiguration {
    func toDictionary() -> [String: Any] {
        return [
            "name": name,
            "description": description,
            "appearance": appearance,
            "planetColor": planetColor.description,
            "planetHasRing": planetHasRing,
            "planetRingStyle": planetRingStyle.toDictionary(), // 转为兼容字典
            "planetRotation": planetRotation,
            "emoji": emoji,
            "repetitionType": repetitionType.toDictionary() // 转为兼容字典
        ]
    }

    init?(from dictionary: [String: Any]) {
        guard let name = dictionary["name"] as? String,
              let description = dictionary["description"] as? String,
              let appearance = dictionary["appearance"] as? String,
              let planetColorDescription = dictionary["planetColor"] as? String,
              let planetHasRing = dictionary["planetHasRing"] as? Bool,
              let ringStyleDict = dictionary["planetRingStyle"] as? [String: Any],
              let ringStyle = PlanetRingStyle(from: ringStyleDict),
              let planetRotation = dictionary["planetRotation"] as? Double,
              let emoji = dictionary["emoji"] as? String,
              let repetitionTypeDict = dictionary["repetitionType"] as? [String: Any],
              let repetitionType = RepetitionType(from: repetitionTypeDict) else {
            return nil
        }

        self.name = name
        self.description = description
        self.appearance = appearance
        self.planetColor = Color(planetColorDescription)
        self.planetHasRing = planetHasRing
        self.planetRingStyle = ringStyle // 使用反序列化后的对象
        self.planetRotation = planetRotation
        self.emoji = emoji
        self.repetitionType = repetitionType
    }
}

3. 验证存储

现在habitDictionaries中的所有值均为属性列表兼容类型,可安全存入UserDefaults,不会再出现崩溃问题。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 07:17:05