開(kāi)始用Swift開(kāi)發(fā)iOS 10 - 17 使用Core Data

上一篇 開(kāi)始用Swift開(kāi)發(fā)iOS 10 - 16 介紹靜態(tài)Table Views,UIImagePickerController和NSLayoutConstraint 中添加新建restaurant頁(yè)面,但最后數(shù)據(jù)并沒(méi)有保存下來(lái),這一篇使用Core Data方式來(lái)持久化保存數(shù)據(jù)。

數(shù)據(jù)持久化一般是指數(shù)據(jù)庫(kù)保存。在Web開(kāi)發(fā)中,常用Oracle或MySQL等關(guān)系數(shù)據(jù)庫(kù)來(lái)保存數(shù)據(jù),通過(guò)SQL語(yǔ)句查詢。在iOS中對(duì)應(yīng)的數(shù)據(jù)庫(kù)是SQLite。Core Data不是數(shù)據(jù)庫(kù),它是讓開(kāi)發(fā)者通過(guò)面向?qū)ο蠓绞脚c數(shù)據(jù)庫(kù)進(jìn)行交互的庫(kù)。

使用Core Data的例子

新建一個(gè)使用Core Data的項(xiàng)目,在AppDelegate類中會(huì)比平常多了一個(gè)變量和一方法,另外還多了一個(gè)文件CoreDataDemo.xcdatamodeld。

    lazy var persistentContainer: NSPersistentContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
        */
        let container = NSPersistentContainer(name: "CoreDataDemo")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                 
                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()

    // MARK: - Core Data Saving support

    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
  • 變量persistentContainerNSPersistentContainer的實(shí)例,let container = NSPersistentContainer(name: "CoreDataDemo")對(duì)應(yīng)CoreDataDemo.xcdatamodeld文件,如果是自己添加時(shí)名字需要對(duì)應(yīng)。
  • 當(dāng)數(shù)據(jù)變化(insert/update/delete)時(shí) ,調(diào)用saveContext方法保存數(shù)據(jù)。

向項(xiàng)目中添加Data Model

  • 右擊FoodPin文件夾,選擇新建Data Model文件,文件名為FoodPin。
  • 選中新生成的FoodPin.xcdatamodeld,添加一個(gè)Restaurant Entity,然后再在此Entity下添加一些屬性。

選中特定屬性后可在右側(cè)檢查器中設(shè)置相關(guān)特性,比如是否強(qiáng)制需要。


創(chuàng)建Managed Objects

Core Data框架中的 Managed ObjectsEntity之間的關(guān)系,有點(diǎn)像代碼中 接口變量UI objects之間的關(guān)系。xcode可自動(dòng)生成Managed Objects

  • 選中Restaurant Entity,在檢查器中修改class的nameRestaurantMO,CodegenClass Definition

  • command-R 或 comman-B一下,表面上沒(méi)有什么變化,在project navigator中沒(méi)有多出文件。實(shí)際上已經(jīng)生成RestaurantMO類,代碼已經(jīng)可以使用了,如果使用command+點(diǎn)擊 RestaurantMO,就可以看到RestaurantMO的代碼:

  • 修改相關(guān)受影響的代碼

    • RestaurantTableViewController.swift
      重新定義restaurants:
      var restaurants:[RestaurantMO] = []
      由于CoreData中存儲(chǔ)圖片是二進(jìn)制,引用時(shí)不能用文件名:

      cell.thumbnailImageView.image = UIImage(data: restaurants[indexPath.row].image as! Data)
      
      if let imageToShare = UIImage(data: self.restaurants[indexPath.row].image as! Data) {
      

      由于RestaurantMO的屬性值是可選值,使用時(shí)需要解包:

      let defaultText = "Just checking in at " + self.restaurants[indexPath.row].name! 
    
    • RestaurantDetailViewController.swift

       var restaurant:RestaurantMO!
      
      restaurantImageView.image = UIImage(data: restaurant.image as! Data)
      
       geoCoder.geocodeAddressString(restaurant.location!, completionHandler: { placemarks, error in
      
    • MapViewController.swift

       var restaurant:RestaurantMO!
      
      leftIconView.image = UIImage(data: restaurant.image as! Data)
      
    • ReviewViewController.swift

       var restaurant:RestaurantMO!
      
      restaurantImageView.image = UIImage(data: restaurant.image as! Data)       
       ```
      
      

現(xiàn)在能成功運(yùn)行,發(fā)現(xiàn)是沒(méi)有數(shù)據(jù)的。

保存新數(shù)據(jù)到數(shù)據(jù)庫(kù)

  • AddTableViewController.swift中引入Core Data:import CoreData。添加變量var restaurant:RestaurantMO!
  • AppDelegate中加入上面例子一個(gè)變量和一方法。
// MARK: - Core Data stack
    
    lazy var persistentContainer: NSPersistentContainer = {
        /*
         The persistent container for the application. This implementation
         creates and returns a container, having loaded the store for the
         application to it. This property is optional since there are legitimate
         error conditions that could cause the creation of the store to fail.
         */
        let container = NSPersistentContainer(name: "FoodPin")
        container.loadPersistentStores(completionHandler: { (storeDescription, error) in
            if let error = error as NSError? {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                
                /*
                 Typical reasons for an error here include:
                 * The parent directory does not exist, cannot be created, or disallows writing.
                 * The persistent store is not accessible, due to permissions or data protection when the device is locked.
                 * The device is out of space.
                 * The store could not be migrated to the current model version.
                 Check the error message to determine what the actual problem was.
                 */
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        })
        return container
    }()
    
    // MARK: - Core Data Saving support
    
    func saveContext () {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
  • AddTableViewControllersave方法的dismiss之前插入:
    // 1
    if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
        restaurant = RestaurantMO(context: appDelegate.persistentContainer.viewContext)
        restaurant.name = nameTextField.text
        restaurant.type = typeTextField.text
        restaurant.location = locationTextField.text
        restaurant.isVisited = isVisited
        
        if let restaurantImage = photoImageView.image {
            // 2
            if let imageData = UIImagePNGRepresentation(restaurantImage) {
                restaurant.image = NSData(data: imageData)
            }
        }
        
        print("Saving data to context ...")
        appDelegate.saveContext()
    }
  • UIApplication.shared這種形式是iOS SDK中比較常用單例模式,就是通過(guò)一個(gè)類屬性shared獲取整個(gè)app運(yùn)行過(guò)程只需要一個(gè)實(shí)例的方法。UIApplication.shared.delegate as? AppDelegate就獲取了AppDelegate對(duì)象。
  • 獲取圖片的二進(jìn)制數(shù)據(jù)對(duì)象。

運(yùn)行,添加新的restaurant后并沒(méi)有在Food Pin中顯示,實(shí)際已經(jīng)添加到數(shù)據(jù)庫(kù)中,在RestaurantTableViewController里沒(méi)有向數(shù)據(jù)庫(kù)獲取。

通過(guò)CoreData獲取數(shù)據(jù)

  • RestaurantTableViewController.swift中添加import CoreData。實(shí)現(xiàn)協(xié)議NSFetchedResultsControllerDelegate,這個(gè)協(xié)議中有方法,任何時(shí)候當(dāng)獲取來(lái)的數(shù)據(jù)有變化時(shí)立即通知代理。
    class RestaurantTableViewController: UITableViewController, NSFetchedResultsControllerDelegate
  • 定義一個(gè)變量
    var fetchResultController: NSFetchedResultsController<RestaurantMO>!
  • viewDidLoad中添加
        // 1   
        let fetchRequest: NSFetchRequest<RestaurantMO> = RestaurantMO.fetchRequest()
        // 2
        let sortDescriptor = NSSortDescriptor(key: "name", ascending: true)
        fetchRequest.sortDescriptors = [sortDescriptor]
        if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
            let context = appDelegate.persistentContainer.viewContext
            fetchResultController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
            fetchResultController.delegate = self
        }
        
        do {
            // 3
            try fetchResultController.performFetch()
            if let fetchedObjects = fetchResultController.fetchedObjects {
                // 4
                restaurants = fetchedObjects
            }
        } catch {
            print(error)
        }
  • 1 從RestaurantMO對(duì)象獲得數(shù)據(jù)請(qǐng)求對(duì)象NSFetchRequest。
  • 2 通過(guò)NSSortDescriptor來(lái)設(shè)置獲取結(jié)果的排序方式。
  • 3 performFetch方法執(zhí)行從數(shù)據(jù)庫(kù)中獲取數(shù)據(jù)請(qǐng)求。
  • 4 把請(qǐng)求結(jié)果復(fù)制給變量restaurants。
  • 數(shù)據(jù)庫(kù)中數(shù)據(jù)變化,將調(diào)用來(lái)自NSFetchedResultsControllerDelegate三個(gè)方法,調(diào)用三個(gè)方法的時(shí)間可以簡(jiǎn)單的理解分別為數(shù)據(jù)將要改變、數(shù)據(jù)正在改變、數(shù)據(jù)改變后:
    controllerWillChangeContent(_:)
    controller(_:didChange:at:for:newIndexPath:)
    controllerDidChangeContent(_:)

    方法的實(shí)現(xiàn),也分別對(duì)table view有不同處理:

    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }
    
    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type:
        NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            if let newIndexPath = newIndexPath {
                tableView.insertRows(at: [newIndexPath], with: .fade)
            }
        case .delete:
            if let indexPath = indexPath {
                tableView.deleteRows(at: [indexPath], with: .fade)
            }
        case .update:
            if let indexPath = indexPath {
                tableView.reloadRows(at: [indexPath], with: .fade)
            } default:
                tableView.reloadData()
        }
        if let fetchedObjects = controller.fetchedObjects {
            restaurants = fetchedObjects as! [RestaurantMO]
        }
    }
    
    func controllerDidChangeContent(_ controller:
        NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }

現(xiàn)在運(yùn)行程序,添加新的Restaurant就能同步顯示了。

通過(guò)CoreData刪除數(shù)據(jù)

更新RestaurantTableViewControllertableView(_:editActionsForRowAt:_)方法中的 deleteAction。

let deleteAction = UITableViewRowAction(style: .default, title: "Delete", handler: {
            (action, indexPath) -> Void in
            
            if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
                let context = appDelegate.persistentContainer.viewContext
                let restaurantToDelete = self.fetchResultController.object(at: indexPath)
                context.delete(restaurantToDelete)
                
                appDelegate.saveContext()
            }
        })

現(xiàn)在刪除一項(xiàng)后,重新啟動(dòng)后,數(shù)據(jù)消失。

更新數(shù)據(jù)

更新RestaurantDetailViewController的中的ActionratingButtonTapped:

    @IBAction func ratingButtonTapped(segue: UIStoryboardSegue) {
        if let rating = segue.identifier {
            restaurant.isVisited = true
            
            switch rating {
            case "great":
                restaurant.rating = "Absolutely love it! Must try."
            case "good":
                restaurant.rating = "Pretty good."
            case "dislike":
                restaurant.rating = "I don't like it."
            default:
                break
            }
        }
        
        if let appDelegate = (UIApplication.shared.delegate as? AppDelegate) {
            appDelegate.saveContext()
        }
        
        tableView.reloadData()
    }

現(xiàn)在評(píng)價(jià)一項(xiàng)后,重新啟動(dòng)后評(píng)價(jià)就會(huì)保留。

Exercise:添加新字段

之前新建Restaurant頁(yè)面沒(méi)有Phone字段,現(xiàn)在添加

  • 在SB的New Restaurant添加新Cell,在AddRestaurantController中添加相關(guān)接口并關(guān)聯(lián)。
  • 更新AddRestaurantController中的save:Action相關(guān)代碼。

代碼

Beginning-iOS-Programming-with-Swift

說(shuō)明

此文是學(xué)習(xí)appcode網(wǎng)站出的一本書 《Beginning iOS 10 Programming with Swift》 的一篇記錄

系列文章目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容