SwiftData+UIKit:保存不同模型时数据丢失并触发报错
我有Bag和Disc两个模型,Disc可添加至「购物车」或「Bag」中。当前存在两个问题:
- 将Disc保存到购物车时,Disc能成功存储,但此前创建的所有Bag会被彻底删除,推测问题出在数据处理逻辑上。
- 保存Disc到购物车时偶尔(约50%概率)触发报错,错误信息如下:
error: (1) I/O error for database at
/Users/kinneykare/Library/Developer/CoreSimulator/Devices/598D3C68-FEB3-4D2C-AE97-FA7B58D4CD0D/data/Containers/Data/Application/591F6E15-E5E9-4624-8267-DB838828D807/Library/Application
Support/default.store. SQLite error code:1, 'no such table:
ZDISCSWIFTDATAMODEL' error: SQLCore dispatchRequest: exception
handling request: <NSSQLFetchRequestContext: 0x600003b0ed80> , I/O
error for database at
/Users/kinneykare/Library/Developer/CoreSimulator/Devices/598D3C68-FEB3-4D2C-AE97-FA7B58D4CD0D/data/Containers/Data/Application/591F6E15-E5E9-4624-8267-DB838828D807/Library/Application
Support/default.store. SQLite error code:1, 'no such table:
ZDISCSWIFTDATAMODEL' with userInfo of {
NSFilePath = "/Users/kinneykare/Library/Developer/CoreSimulator/Devices/598D3C68-FEB3-4D2C-AE97-FA7B58D4CD0D/data/Containers/Data/Application/591F6E15-E5E9-4624-8267-DB838828D807/Library/Application
Support/default.store";
NSSQLiteErrorDomain = 1; }
相关代码
1. 数据库操作核心类(DatabaseService)
import UIKit import SwiftData class DatabaseService { static let shared = DatabaseService() private var discContext: ModelContext? private var bagContext: ModelContext? private init() { do { discContext = try ModelContainer(for: DiscSwiftDataModel.self).map(ModelContext.init) bagContext = try ModelContainer(for: BagSwiftDataModel.self).map(ModelContext.init) } catch { print("Swift Data Error: \(error)") } } // MARK: Discs func saveDisc( discName: String, discImageData: Data, discStability: String, discSpeed: String, discGlide: String, discTurn: String, discFade: String, discBrand: String, viewController: UIViewController ) { saveData( context: discContext, data: DiscSwiftDataModel( id: UUID().uuidString, name: discName, imageData: discImageData, stability: discStability, speed: discSpeed, glide: discGlide, turn: discTurn, fade: discFade, brand: discBrand ), viewController: viewController ) } func fetchDiscList(onCompletion: @escaping ([DiscSwiftDataModel]?, Error?) -> Void) { fetchData(context: discContext, descriptor: FetchDescriptor<DiscSwiftDataModel>(sortBy: [SortDescriptor<DiscSwiftDataModel>(\.brand)]), onCompletion: onCompletion) } func updateDisc(disc: DiscSwiftDataModel, newDiscName: String) { updateData(data: disc, updateBlock: { $0.name = newDiscName }, context: discContext) } func deleteDisc(disc: DiscSwiftDataModel) { deleteData(data: disc, context: discContext) } // MARK: Bags func saveBag(bagName: String, bagType: String, bagHexColor: String, viewController: UIViewController) { saveData( context: bagContext, data: BagSwiftDataModel( id: UUID().uuidString, bagHexColor: bagHexColor, bagTitle: bagName, bagType: bagType ), viewController: viewController ) } func fetchBagList(onCompletion: @escaping ([BagSwiftDataModel]?, Error?) -> Void) { fetchData(context: bagContext, descriptor: FetchDescriptor<BagSwiftDataModel>(sortBy: [SortDescriptor<BagSwiftDataModel>(\.bagTitle)]), onCompletion: onCompletion) } func updateBag(bag: BagSwiftDataModel, with disc: DiscGolfDisc) { updateData(data: bag, updateBlock: { $0.discs.append(disc) }, context: bagContext) } func removeDiscFromBag(disc: DiscGolfDisc, from bag: BagSwiftDataModel) { if let index = bag.discs.firstIndex(where: { $0.id == disc.id }) { bag.discs.remove(at: index) updateData(data: bag, updateBlock: { _ in }, context: bagContext) } } func deleteBag(bag: BagSwiftDataModel) { deleteData(data: bag, context: bagContext) } // MARK: Generic Functions private func saveData<T: Storable>(context: ModelContext?, data: T, viewController: UIViewController) { guard let context = context else { print("ERROR SAVING DATA!") // Show an alert to the user let alert = UIAlertController(title: "Error", message: "An error occurred while saving the data.", preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) viewController.present(alert, animated: true, completion: nil) return } context.insert(data) } private func fetchData<T: Storable>( context: ModelContext?, descriptor: FetchDescriptor<T>, onCompletion: @escaping ([T]?, Error?) -> Void ) { guard let context = context else { onCompletion(nil, NSError(domain: "", code: 0, userInfo: nil)) return } do { let data = try context.fetch(descriptor) onCompletion(data, nil) } catch { onCompletion(nil, error) } } private func updateData<T: Storable>(data: T, updateBlock: (T) -> Void, context: ModelContext?) { guard let context = context else { return } updateBlock(data) } private func deleteData<T: Storable>(data: T, context: ModelContext?) { guard let context = context else { return } context.delete(data) } }
2. 委托协议与操作视图(AddDiscToView)
import UIKit protocol AddDiscToViewDelegate { func dissmiss() func saveDiscToCart() func saveDiscToBag() } class AddDiscToView: UIView { var delegate: AddDiscToViewDelegate? @IBAction func addToCartTapped(_ sender: Any) { //Send user to fillout form then pop to view delegate?.saveDiscToCart() delegate?.dissmiss() } @IBAction func addToBagTapped(_ sender: Any) { //Send user to fillout form then pop to view delegate?.saveDiscToBag() delegate?.dissmiss() } @IBAction func cancelButtonTapped(_ sender: Any) { print("Cancel Tapped") delegate?.dissmiss() } }
3. 触发保存的视图控制器(DiscViewController)
import UIKit class DiscViewController: UIViewController { @IBOutlet weak var discNameLabel: UILabel! @IBOutlet weak var discImageView: UIImageView! @IBOutlet weak var speedLabel: UILabel! @IBOutlet weak var glideLabel: UILabel! @IBOutlet weak var turnLabel: UILabel! @IBOutlet weak var fadeLabel: UILabel! @IBOutlet weak var companyNameLabel: UILabel! @IBOutlet weak var discView: UIView! @IBOutlet weak var mainView: UIView! @IBOutlet weak var simularDiscsCollectionView: UICollectionView! var dropDownView: AddDiscToView! var disc: DiscGolfDisc? var similarDiscs: [DiscGolfDisc] = [] override func viewDidLoad() { super.viewDidLoad() setupCollectionView() setupAddDiscToView() // Add a "Select All" button to the navigation bar let selectAllButton = UIBarButtonItem(image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(addDiscTapped)) navigationItem.rightBarButtonItem = selectAllButton } private func setupAddDiscToView() { dropDownView = Bundle.main.loadNibNamed("AddDiscToView", owner: nil, options: nil)!.first as? AddDiscToView dropDownView.delegate = self dropDownView.translatesAutoresizingMaskIntoConstraints = false mainView.addSubview(dropDownView) dropDownView.isHidden = true dropDownView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 5).isActive = true dropDownView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 25).isActive = true dropDownView.widthAnchor.constraint(equalToConstant: 249).isActive = true dropDownView.heightAnchor.constraint(equalToConstant: 115).isActive = true } private func setupCollectionView() { simularDiscsCollectionView.dataSource = self simularDiscsCollectionView.delegate = self let flowLayout = UICollectionViewFlowLayout() // Set the scroll direction to horizontal flowLayout.scrollDirection = .horizontal // Calculate the item width based on your desired width let itemWidth: CGFloat = 200.0 // Adjust this value to your preferred width flowLayout.itemSize = CGSize(width: itemWidth, height: simularDiscsCollectionView.frame.height) flowLayout.minimumInteritemSpacing = 10.0 // Adjust spacing between items simularDiscsCollectionView.collectionViewLayout = flowLayout } @objc private func addDiscTapped() { dropDownView.isHidden.toggle() } } extension DiscViewController: UICollectionViewDelegate, UICollectionViewDataSource { func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return similarDiscs.count } func numberOfSections(in collectionView: UICollectionView) -> Int { return 1 } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "discCell", for: indexPath) as! DiscCell let disc = similarDiscs[indexPath.item] cell.similarDiscNameBottomLabel.text = disc.name getImageFromLink(imageView: cell.discImageView, disc: disc) return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { disc = similarDiscs[indexPath.item] DispatchQueue.main.async { self.createSimilarDiscsCollection(disc: self.disc) self.setupFlightPathAnimationView() } } } extension DiscViewController: AddDiscToViewDelegate { func saveDiscToBag() { BagDatabaseService.shared.fetchDiscBagList { [weak self] bags, error in guard let self else { return } if error == nil, bags == [] || bags == nil { K.showAlert(title: "No Bags Created", message: "You need to add a bag before you can add a disc to your bags. Click the bag tab and tap the + to add your first bag.", presentingViewController: self) } else { let storyboard = UIStoryboard(name: "AddDiscToBag", bundle: nil) if let vc = storyboard.instantiateViewController(withIdentifier: "addDiscToBagVC") as? AddDiscToBagViewController, let disc = disc { vc.disc = disc dropDownView.isHidden = true hidesBottomBarWhenPushed = true navigationController?.pushViewController(vc, animated: true) hidesBottomBarWhenPushed = false } } } } func saveDiscToCart() { guard let disc = disc else { K.showAlertWithRetryOption(title: "Issues Getting Disc Data", message: "We ran into a problem attempting to get the disc's data.", presentingViewController: self) { [weak self] _ in guard let self else { return } self.saveDiscToCart() } return } if let image = discImageView.image { if let imageData = image.pngData() { SwiftSpinner.show(delay: 3, title: "Saving Disc To Cart!", animated: true) DiscDatabaseService.shared.saveDisc(discName: disc.name, discImageData: imageData, discStability: disc.stability, discSpeed: disc.speed, discGlide: disc.glide, discTurn: disc.turn, discFade: disc.fade, discBrand: disc.brand, viewController: self) SwiftSpinner.hide() } } else { K.showAlert(title: "Issues Saving Disc", message: "We ran into a problem attempting to save the disc.", presentingViewController: self) SwiftSpinner.hide() } } func dissmiss() { dropDownView.isHidden = true } }
内容的提问来源于stack exchange,提问作者KinneyKare

