如何将单例购物车数据保存到CoreData,实现登出后重新登录留存?
解决登录后购物车持久化(CoreData)的问题
首先,咱们先梳理下你当前代码的核心问题:内存中的单例购物车和CoreData存储完全脱节了——你在DetailVC里添加商品时既存了CoreData又加了单例,但登录/启动时没把CoreData的数据同步回单例;而且MyCartVC的表格直接用单例数据,根本没和CoreData关联。另外你之前用UserDefaults存UITableViewDataSource肯定不行,因为它不是可序列化的类型,UserDefaults只能存基础类型或遵循Codable/NSCoding的对象。
下面是完整的解决方案,核心思路是让单例作为购物车的唯一数据源,同时负责和CoreData的双向同步,避免各个VC重复写CoreData逻辑:
第一步:完善CoreData实体(必须加用户标识)
先给你的MyCartData实体新增一个userId字段(String类型),这是关键! 不然多个用户的购物车数据会混在一起。
第二步:重构SingletonCart,让它管理CoreData同步
把CoreData的读写逻辑封装到单例里,这样所有页面只需要调用单例的方法,不用自己操作CoreData:
import Foundation import CoreData class SingletonCart { static let shared = SingletonCart() var food: [Food] = [] private init() {} /// 从CoreData加载当前用户的购物车 func loadCart(for userId: String) { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } let managedContext = appDelegate.persistentContainer.viewContext let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "MyCartData") // 只加载当前用户的购物车 fetchRequest.predicate = NSPredicate(format: "userId == %@", userId) do { let savedItems = try managedContext.fetch(fetchRequest) // 把CoreData对象转成你的Food模型 food = savedItems.compactMap { item in guard let title = item.value(forKey: "fromDetailFoodNames") as? String, let priceStr = item.value(forKey: "fromDetailFoodPrices") as? String else { return nil } // 根据你的Food模型初始化,这里假设你有对应的初始化方法 return Food(ProductTitle: title, PriceString: priceStr) } print("购物车从CoreData加载完成,共\(food.count)件商品") } catch let error as NSError { print("加载购物车失败: \(error), \(error.userInfo)") } } /// 添加商品到购物车(自动同步CoreData) func addFood(_ foodItem: Food, userId: String) { food.append(foodItem) saveFoodToCoreData(foodItem, userId: userId) } /// 删除购物车商品(自动同步CoreData) func removeFood(at index: Int, userId: String) { let removedItem = food.remove(at: index) deleteFoodFromCoreData(removedItem, userId: userId) } /// 保存单个商品到CoreData private func saveFoodToCoreData(_ foodItem: Food, userId: String) { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } let managedContext = appDelegate.persistentContainer.viewContext let entity = NSEntityDescription.entity(forEntityName: "MyCartData", in: managedContext)! let cartItem = NSManagedObject(entity: entity, insertInto: managedContext) cartItem.setValue(foodItem.ProductTitle, forKey: "fromDetailFoodNames") cartItem.setValue(foodItem.PriceString, forKey: "fromDetailFoodPrices") cartItem.setValue(userId, forKey: "userId") // 关联当前用户 do { try managedContext.save() print("商品已保存到CoreData") } catch let error as NSError { print("保存商品失败: \(error), \(error.userInfo)") } } /// 从CoreData删除单个商品 private func deleteFoodFromCoreData(_ foodItem: Food, userId: String) { guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } let managedContext = appDelegate.persistentContainer.viewContext let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "MyCartData") fetchRequest.predicate = NSPredicate(format: "userId == %@ AND fromDetailFoodNames == %@ AND fromDetailFoodPrices == %@", userId, foodItem.ProductTitle, foodItem.PriceString) do { let itemsToDelete = try managedContext.fetch(fetchRequest) itemsToDelete.forEach(managedContext.delete) try managedContext.save() print("商品已从CoreData删除") } catch let error as NSError { print("删除商品失败: \(error), \(error.userInfo)") } } /// 清空内存中的购物车(登出时调用,CoreData数据保留) func clearCart() { food.removeAll() } }
第三步:修改DetailVC的添加逻辑
删掉DetailVC里自己写的CoreData代码,直接调用单例的方法:
@IBAction func addBasket(_ sender: Any) { // 先判断用户是否登录,获取当前用户ID(这里假设你把用户ID存在UserDefaults里) guard let currentUserId = UserDefaults.standard.string(forKey: "currentUserId") else { print("用户未登录,无法保存购物车") return } guard let foodItem = food else { return } // 调用单例方法,自动同步内存和CoreData SingletonCart.shared.addFood(foodItem, userId: currentUserId) self.performSegue(withIdentifier: "toMyCart", sender: nil) self.navigationController?.navigationBar.isHidden = false self.tabBarController?.tabBar.isHidden = false self.isLoading(true) }
第四步:简化MyCartVC,直接用单例数据
去掉MyCartVC里冗余的CoreData代码,所有操作都基于单例:
import UIKit import CoreData class MyCartViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { @IBOutlet weak var myCartTableView: UITableView! @IBOutlet weak var totalPriceLabel: UILabel! // 直接用单例的购物车数据 private var cartFoods: [Food] { SingletonCart.shared.food } @IBAction func approveCart(_ sender: Any) { // 这里处理结算逻辑 } override func viewDidLoad() { super.viewDidLoad() self.tabBarController?.tabBar.isHidden = false myCartTableView.dataSource = self myCartTableView.delegate = self } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) myCartTableView.reloadData() // 更新TabBar的badge let badgeValue = cartFoods.count > 0 ? "\(cartFoods.count)" : nil self.tabBarController?.viewControllers?[1].tabBarItem.badgeValue = badgeValue // 显示空视图 if cartFoods.isEmpty { myCartTableView.setEmptyView(title: "Sepetinizde ürün bulunmamaktadır", message: "Seçtiğiniz yemekler burada listelenir.") } else { myCartTableView.restore() } } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return cartFoods.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let foodItem = cartFoods[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: "myCartCell", for: indexPath) as! MyCartTableViewCell cell.myCartFoodNameLabel.text = foodItem.ProductTitle cell.myCartFoodPriceLabel.text = foodItem.PriceString return cell } func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true } func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { if editingStyle == .delete { guard let currentUserId = UserDefaults.standard.string(forKey: "currentUserId") else { print("用户未登录,无法删除购物车商品") return } // 调用单例方法删除,自动同步CoreData SingletonCart.shared.removeFood(at: indexPath.row, userId: currentUserId) tableView.beginUpdates() tableView.deleteRows(at: [indexPath], with: .automatic) tableView.endUpdates() // 更新badge和空视图 let badgeValue = cartFoods.count > 0 ? "\(cartFoods.count)" : nil self.tabBarController?.viewControllers?[1].tabBarItem.badgeValue = badgeValue if cartFoods.isEmpty { myCartTableView.setEmptyView(title: "Sepetinizde ürün bulunmamaktadır", message: "Seçtiğiniz yemekler burada listelenir.") } } } }
第五步:处理登录/登出逻辑
- 登录成功后:保存用户ID到UserDefaults,然后加载CoreData中的购物车:
// 登录成功的回调示例 func onLoginSuccess(userId: String) { UserDefaults.standard.set(userId, forKey: "currentUserId") // 加载当前用户的购物车到单例 SingletonCart.shared.loadCart(for: userId) }
- 登出时:清空内存中的购物车,但保留CoreData数据(下次登录再加载):
// 登出方法示例 func logout() { UserDefaults.standard.removeObject(forKey: "currentUserId") SingletonCart.shared.clearCart() // 跳转到登录页面 let loginVC = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "LoginVC") UIApplication.shared.keyWindow?.rootViewController = UINavigationController(rootViewController: loginVC) }
额外注意事项
- 确保你的
Food模型可以从CoreData的MyCartData实体正确转换,上面的代码假设Food有对应的初始化方法,你需要根据自己的模型调整; - 如果支持同一商品多次添加,建议给
MyCartData实体加quantity字段,而不是重复存储相同商品; - 可以给
MyCartData实体添加唯一约束(userId+fromDetailFoodNames+fromDetailFoodPrices),避免重复添加相同商品; - 在App启动时,如果用户已经登录(UserDefaults里有
currentUserId),要调用SingletonCart.shared.loadCart(for: userId)加载购物车。
内容的提问来源于stack exchange,提问作者Emre Değirmenci
相关产品推荐
相关产品推荐

