开始用Swift开发iOS 10 - 17 使用Core Data

上一篇 开始用Swift开发iOS 10 - 16 介绍静态Table Views,UIImagePickerController和NSLayoutConstraint 中添加新建restaurant页面,但最后数据并没有保存下来,这一篇使用Core Data方式来持久化保存数据。

数据持久化一般是指数据库保存。在Web开发中,常用Oracle或MySQL等关系数据库来保存数据,通过SQL语句查询。在iOS中对应的数据库是SQLite。Core Data不是数据库,它是让开发者通过面向对象方式与数据库进行交互的库。

使用Core Data的例子

新建一个使用Core Data的项目,在AppDelegate类中会比平常多了一个变量和一方法,另外还多了一个文件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的实例,let container = NSPersistentContainer(name: "CoreDataDemo")对应CoreDataDemo.xcdatamodeld文件,如果是自己添加时名字需要对应。
  • 当数据变化(insert/update/delete)时 ,调用saveContext方法保存数据。

向项目中添加Data Model

  • 右击FoodPin文件夹,选择新建Data Model文件,文件名为FoodPin
  • 选中新生成的FoodPin.xcdatamodeld,添加一个Restaurant Entity,然后再在此Entity下添加一些属性。

选中特定属性后可在右侧检查器中设置相关特性,比如是否强制需要。


创建Managed Objects

Core Data框架中的 Managed ObjectsEntity之间的关系,有点像代码中 接口变量UI objects之间的关系。xcode可自动生成Managed Objects

  • 选中Restaurant Entity,在检查器中修改class的nameRestaurantMOCodegenClass Definition

  • command-R 或 comman-B一下,表面上没有什么变化,在project navigator中没有多出文件。实际上已经生成RestaurantMO类,代码已经可以使用了,如果使用command+点击 RestaurantMO,就可以看到RestaurantMO的代码:

  • 修改相关受影响的代码

    • RestaurantTableViewController.swift
      重新定义restaurants:
      var restaurants:[RestaurantMO] = []
      由于CoreData中存储图片是二进制,引用时不能用文件名:

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

      由于RestaurantMO的属性值是可选值,使用时需要解包:

      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)       
       ```
      
      

现在能成功运行,发现是没有数据的。

保存新数据到数据库

  • AddTableViewController.swift中引入Core Data:import CoreData。添加变量var restaurant:RestaurantMO!
  • AppDelegate中加入上面例子一个变量和一方法。
// 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中比较常用单例模式,就是通过一个类属性shared获取整个app运行过程只需要一个实例的方法。UIApplication.shared.delegate as? AppDelegate就获取了AppDelegate对象。
  • 获取图片的二进制数据对象。

运行,添加新的restaurant后并没有在Food Pin中显示,实际已经添加到数据库中,在RestaurantTableViewController里没有向数据库获取。

通过CoreData获取数据

  • RestaurantTableViewController.swift中添加import CoreData。实现协议NSFetchedResultsControllerDelegate,这个协议中有方法,任何时候当获取来的数据有变化时立即通知代理。
    class RestaurantTableViewController: UITableViewController, NSFetchedResultsControllerDelegate
  • 定义一个变量
    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对象获得数据请求对象NSFetchRequest
  • 2 通过NSSortDescriptor来设置获取结果的排序方式。
  • 3 performFetch方法执行从数据库中获取数据请求。
  • 4 把请求结果复制给变量restaurants
  • 数据库中数据变化,将调用来自NSFetchedResultsControllerDelegate三个方法,调用三个方法的时间可以简单的理解分别为数据将要改变、数据正在改变、数据改变后:
    controllerWillChangeContent(_:)
    controller(_:didChange:at:for:newIndexPath:)
    controllerDidChangeContent(_:)

    方法的实现,也分别对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()
    }

现在运行程序,添加新的Restaurant就能同步显示了。

通过CoreData删除数据

更新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()
            }
        })

现在删除一项后,重新启动后,数据消失。

更新数据

更新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()
    }

现在评价一项后,重新启动后评价就会保留。

Exercise:添加新字段

之前新建Restaurant页面没有Phone字段,现在添加

  • 在SB的New Restaurant添加新Cell,在AddRestaurantController中添加相关接口并关联。
  • 更新AddRestaurantController中的save:Action相关代码。

代码

Beginning-iOS-Programming-with-Swift

说明

此文是学习appcode网站出的一本书 《Beginning iOS 10 Programming with Swift》 的一篇记录

系列文章目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容