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.
相关产品推荐
相关产品推荐

