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

SwiftUI存储引用类型至UserDefaults时如何保留对象引用?

问题:在UserDefaults中存储引用类型时如何保留对象间的引用关系?

我有两个数组:nameList存储NameItem类的实例,playerList包含从nameList中引用的NameItem实例。由于NameItem是引用类型,修改nameList中实例的属性时,playerList中对应实例的属性会同步变化,两者内存地址一致。

但当应用关闭后,我把两个数组存入UserDefaults;重新打开应用加载数据时,数组内容能正确重建,但对象间的引用关系被破坏——playerList和nameList中的实例内存地址不再相同,关联失效。

有没有办法在UserDefaults中存储引用类型时保留这种引用关系?以下是仅含单个元素的演示代码:

import SwiftUI

@main
struct SavingPointerTypesApp: App {
    private var nLM: NameListManager = NameListManager()
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(nLM)
        }
    }
}

import Foundation
import SwiftUI

class NameListManager: ObservableObject {
    // MARK: - Published Objects
    // List of active players
    @Published
    var playerList: [NameItem] = []
    // List of all persons who are registered
    @Published
    var nameList: [NameItem] =  []
    
    init() {
        fillLists()
    }
    
    func fillLists() {
        // Fill name list
        nameList.append(NameItem("Adam"))
        //Fill player list
        playerList.append(nameList[0])
    }

    // MARK: - Save/Store/Delete Methods
    func load(type: ListType){
        let key = type.rawValue
        
        if let data = UserDefaults.standard.value(forKey: key) as? Data {
            if let list = try? PropertyListDecoder().decode([NameItem].self, from: data){
                switch type {
                case .playerList:
                    self.playerList = list
                case .nameList:
                    self.nameList = list
                }
            }
        }
    }
    
    func save(type: ListType) {
        let key = type.rawValue
        switch type {
        case .playerList:
            UserDefaults.standard.set(try? PropertyListEncoder().encode(self.playerList), forKey: key)
        case .nameList:
            UserDefaults.standard.set(try? PropertyListEncoder().encode(self.nameList), forKey: key)
        }
    }
}


class NameItem: Encodable, Decodable, Identifiable {
    var name: String
    var id =  UUID()
    
    init( _ name: String) {
        self.name = name
    }
}

enum ListType: String {
    case playerList = "player-list"
    case nameList  = "name-list"
}


import SwiftUI

struct ContentView: View {
    @EnvironmentObject var nML: NameListManager
    @State var nameAddrBefore: String = ""
    @State var playerAddrBefore: String = ""
    @State var nameAddrAfter: String = ""
    @State var playerAddrAfter: String = ""


    var body: some View {
        makeView()
        .onAppear(){
            generateAddresses()
        }
    }
    
    @ViewBuilder
    func makeView() -> some View {
        List() {
            VStack {
                Text("Names before save/load").font(.title3)
                Text("NameList  : \(nML.nameList[0].name)")
                Text("PlayerList: \(nML.playerList[0].name)")
                Text("Addresses before save/load").font(.title3)
                Text("Address name  : " + nameAddrBefore)
                Text("Address player: " + playerAddrBefore)
                    .padding(.bottom,20)
            }

            VStack{
                Text("Names after save/load").font(.title3)
                Text("NameList  : \(nML.nameList[0].name)")
                Text("PlayerList: \(nML.playerList[0].name)")
                Text("Addresses after save/load").font(.title3)
                Text("Address name  : " + nameAddrAfter)
                Text("Address player: " + playerAddrAfter)
            }
        }
    }

    func generateAddresses() {
        var addr: UnsafeMutableRawPointer = Unmanaged.passUnretained(nML.nameList[0]).toOpaque()
        var addrInt = Int(bitPattern: addr)
        self.nameAddrBefore = String(format:"%02X", addrInt)
        
        addr = Unmanaged.passUnretained(nML.playerList[0]).toOpaque()
        addrInt = Int(bitPattern: addr)
        self.playerAddrBefore = String(format:"%02X", addrInt)
        
        //Save data
        nML.save(type: .nameList)
        nML.save(type: .playerList)
        
        //Load Data
        nML.load(type: .nameList)
        nML.load(type: .playerList)
        
        addr = Unmanaged.passUnretained(nML.nameList[0]).toOpaque()
        addrInt = Int(bitPattern: addr)
        self.nameAddrAfter = String(format:"%02X", addrInt)
        
        addr = Unmanaged.passUnretained(nML.playerList[0]).toOpaque()
        addrInt = Int(bitPattern: addr)
        self.playerAddrAfter = String(format:"%02X", addrInt)
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(NameListManager())
    }
}

解决方案:通过唯一标识符重建引用关系

UserDefaults本质是序列化存储,无法直接保存内存引用(每次启动应用内存地址都会重新分配)。要保留对象间的关联,需要通过唯一标识符重建引用,而非直接存储对象引用。具体步骤如下:

1. 核心思路

利用NameItem已有的UUID类型id属性(序列化后值保持稳定),只存储nameList完整对象和playerList对应的id数组。加载时先恢复nameList,再通过id从nameList中匹配实例,重建playerList的引用关系。

2. 修改后的代码示例

调整NameListManager的存储/加载逻辑

class NameListManager: ObservableObject {
    @Published var playerList: [NameItem] = []
    @Published var nameList: [NameItem] = []
    
    init() {
        // 优先加载本地数据,无数据时初始化
        load()
        if nameList.isEmpty {
            fillLists()
        }
    }
    
    func fillLists() {
        nameList.append(NameItem("Adam"))
        playerList.append(nameList[0])
    }
    
    // 统一保存:存储nameList和playerList的id数组
    func save() {
        // 保存nameList
        if let nameData = try? PropertyListEncoder().encode(nameList) {
            UserDefaults.standard.set(nameData, forKey: "name-list")
        }
        // 保存player对应的id数组
        let playerIds = playerList.map { $0.id }
        if let idData = try? PropertyListEncoder().encode(playerIds) {
            UserDefaults.standard.set(idData, forKey: "player-ids")
        }
    }
    
    // 统一加载:先恢复nameList,再通过id重建playerList
    func load() {
        // 加载nameList
        if let data = UserDefaults.standard.value(forKey: "name-list") as? Data,
           let list = try? PropertyListDecoder().decode([NameItem].self, from: data) {
            self.nameList = list
        }
        
        // 加载player的id数组,匹配nameList中的实例
        if let idData = UserDefaults.standard.value(forKey: "player-ids") as? Data,
           let playerIds = try? PropertyListDecoder().decode([UUID].self, from: idData) {
            // 用字典优化查找效率(数据量大时更明显)
            let nameDict = Dictionary(uniqueKeysWithValues: nameList.map { ($0.id, $0) })
            self.playerList = playerIds.compactMap { nameDict[$0] }
        }
    }
}

调整ContentView的generateAddresses方法

func generateAddresses() {
    // 记录初始地址
    var addr: UnsafeMutableRawPointer = Unmanaged.passUnretained(nML.nameList[0]).toOpaque()
    var addrInt = Int(bitPattern: addr)
    self.nameAddrBefore = String(format:"%02X", addrInt)
    
    addr = Unmanaged.passUnretained(nML.playerList[0]).toOpaque()
    addrInt = Int(bitPattern: addr)
    self.playerAddrBefore = String(format:"%02X", addrInt)
    
    // 保存数据
    nML.save()
    
    // 清空列表模拟应用重启
    nML.nameList.removeAll()
    nML.playerList.removeAll()
    
    // 加载数据
    nML.load()
    
    // 记录加载后的地址
    addr = Unmanaged.passUnretained(nML.nameList[0]).toOpaque()
    addrInt = Int(bitPattern: addr)
    self.nameAddrAfter = String(format:"%02X", addrInt)
    
    addr = Unmanaged.passUnretained(nML.playerList[0]).toOpaque()
    addrInt = Int(bitPattern: addr)
    self.playerAddrAfter = String(format:"%02X", addrInt)
}

3. 效果验证

修改后加载数据时,playerList中的实例会和nameList中的实例指向同一内存地址,修改其中一方的属性,另一方会同步变化,引用关系得以保留。


内容的提问来源于stack exchange,提问作者Heinz M.

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 16:05:02