上一篇 开始用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)")
}
}
}
- 变量
persistentContainer
是NSPersistentContainer
的实例,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 Objects与Entity之间的关系,有点像代码中 接口变量 和 UI objects之间的关系。xcode可自动生成Managed Objects。
-
选中
Restaurant
Entity,在检查器中修改class的name
为RestaurantMO
,Codegen
为Class 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)")
}
}
}
- 在
AddTableViewController
的save
方法的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删除数据
更新RestaurantTableViewController
的tableView(_: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》 的一篇记录
系列文章目录
- 开始用Swift开发iOS 10 - 1 前言
- 开始用Swift开发iOS 10 - 2 Hello World!第一个Swift APP
- 开始用Swift开发iOS 10 - 3 介绍Auto Layout
- 开始用Swift开发iOS 10 - 4 用Stack View设计UI
- [开始用Swift开发iOS 10 - 5 原型的介绍]
- 开始用Swift开发iOS 10 - 6 创建简单的Table Based App
- 开始用Swift开发iOS 10 - 7 定制Table Views
- 开始用Swift开发iOS 10 - 8 Table View和UIAlertController的交互
- 开始用Swift开发iOS 10 - 9 Table Row的删除, UITableViewRowAction和UIActivityViewController的使用
- 开始用Swift开发iOS 10 - 10 Navigation Controller的介绍和Segue
- 开始用Swift开发iOS 10 - 11 面向对象编程介绍
- 开始用Swift开发iOS 10 - 12 丰富Detail View和定制化Navigation Bar
- 开始用Swift开发iOS 10 - 13 Self Sizing Cells and Dynamic Type
- 开始用Swift开发iOS 10 - 14 基础动画,模糊效果和Unwind Segue
- 开始用Swift开发iOS 10 - 15 使用地图
- 开始用Swift开发iOS 10 - 16 介绍静态Table Views,UIImagePickerController和NSLayoutConstraint