Fork me on GitHub

开始用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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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中存储图片是二进制,引用时不能用文件名:

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

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

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

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

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

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

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

保存新数据到数据库

  • AddTableViewController.swift中引入Core Data:import CoreData。添加变量var restaurant:RestaurantMO!
  • AppDelegate中加入上面例子一个变量和一方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    // 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 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有不同处理:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    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

1
2
3
4
5
6
7
8
9
10
11
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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》 的一篇记录

坚持原创技术分享,您的支持将鼓励我继续创作!
  • 本文标题: 开始用Swift开发iOS 10 - 17 使用Core Data
  • 本文作者: AndyRon
  • 发布时间: 2017年07月26日 - 00:00
  • 最后更新: 2017年09月09日 - 12:59
  • 本文链接: http://andyron.com/2017/beginning-ios-swift-17.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!