Castie!

正态分布, 优劣伴生

北冥有鱼,其名为鲲(kūn)。鲲之大,不知其几千里也;化而为鸟,其名为鹏。鹏之背,不知其几千里也;怒而飞,其翼若垂天之云。是鸟也,海运则将徙于南冥。南冥者,天池也。


北海若曰:“井鼃不可以语于海者,拘于虚也;夏虫不可以语于冰者,笃于时也;曲士不可以语于道者,束于教也。今尔出于崖涘,观于大海,乃知尔丑,尔将可与语大理矣。

CoreData SQL写烦了? 试试亲儿子!

目前来说 iOS的数据库一般都使用FMDB, 之前看了下挺火的Rleam, 以我目前的水平还分不清楚各种移动数据库孰优孰劣, 以一般覆盖80%的页面缓存, 个人愚见使用写.plist的方式最为简单有效, 但作为iOS开发者, 怎么能不试试亲儿子Core Data呢?

从来没有看过Core Data的我, 只是道听途说底层也是SQLite, 性能不好, 学习曲线陡峭, 诶… 听起来就很丧, 但作为亲儿子, 如此不成气候, 莫非是地主家的傻儿子? 从iOS3 ~ iOS10 Apple老爹还一直在推, ObjC.io 和 Raywenderlich的亲戚朋友们还一直帮忙出书? 素以逻辑严谨的我表示百思不得其解, 带着这份疑惑, 我也就毅然决然的踏上了Core Data踩坑之旅.

Core Data 架构

一个基本的 Core Data 栈由四个主要部分组成:托管对象 (managed objects) (NSManagedObject),托管对象上下文 (managed object context) (NSManagedObjectContext),持久化存储协调器 (persistent store coordinator) (NSPersistentStoreCoordinator),以及持久化存储 (persistent store) (NSPersistentStore), 我们分别看下对应的API.

NSManagedObject:

NSManagedObject以我理解, 就如同WWDC2017中开放的Core ML的MLModel一样应该是一个模型对象, 用以缓存数据的模型, Core Data中能缓存的数据只有继承自NSManagedObject的才可以, 和Rleam一样需要对模型对象进行标注, 这也是我对Rleam无感的原因 – 对第三方的耦合性太强了!!

@available(iOS 3.0, *)
open class NSManagedObject : NSObject 
@available(iOS 10.0, *)
    public init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) //根据实体描述及托管对象上下文创建托管对象
@available(iOS 10.0, *)
    open class func fetchRequest() -> NSFetchRequest<NSFetchRequestResult> //返回当前查询对象
    public init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) //通过实体及上下文进行初始化
@available(iOS 10.0, *)
    public convenience init(context moc: NSManagedObjectContext) //通过上下文进行初始化
    unowned(unsafe) open var managedObjectContext: NSManagedObjectContext? { get } //当前对象的上下文
    open var objectID: NSManagedObjectID { get } //当前对象ID
@available(iOS 5.0, *)
    open var hasChanges: Bool { get } //当前对象中是否有属性改变
    open func value(forKey key: String) -> Any? //KVC取值
    open func setValue(_ value: Any?, forKey key: String) //KVC赋值

上述具体分析NSManagedObject中的关键属性及方法, 可以看到的是, iOS10新增的API的数量还是很多的, 这也侧边证明了老爹对儿子的关心与疼爱.

NSManagedObjectContext:
@available(iOS 3.0, *)
open class NSManagedObjectContext : NSObject, NSCoding, NSLocking 
@available(iOS 5.0, *)
    open var parent: NSManagedObjectContext? //父级上下文
    open var hasChanges: Bool { get } //上下文是否有更改
    open func save() throws //保存上下文
    open func insert(_ object: NSManagedObject) //将托管对象插入上下文
    open func delete(_ object: NSManagedObject) //将托管对象从上下文中删除
@available(iOS 8.0, *)
    open func execute(_ request: NSPersistentStoreRequest) throws -> NSPersistentStoreResult //执行获取请求
    public func fetch<T : NSFetchRequestResult>(_ request: NSFetchRequest<T>) throws -> [T] //获取数据
    @available(iOS 3.0, *)
NSPersistentStoreCoordinator:
@available(iOS 3.0, *)
open class NSPersistentStoreCoordinator : NSObject, NSLocking 
   open var persistentStores: [NSPersistentStore] { get } //持续化存储集
NSPersistentStore:
@available(iOS 3.0, *)
open class NSPersistentStore : NSObject

Core Data 基本概念

我们创建一个Core Data文件 后缀名为.xcdatamodeld, 图形化界面配置存储数据, 由于是基本使用, 就简单的创建一个Person实体, Person实体中有一个name的字符串, 如图: Core Data 文件 Person实体中添加一个name属性 简单设置

NSPersistentContainer:
@available(iOS 10.0, *)
open class NSPersistentContainer : NSObject
open var viewContext: NSManagedObjectContext { get } //托管对象上下文
public convenience init(name: String) //通过Bundle中的名称来创建持久化容器
open func loadPersistentStores(completionHandler block: @escaping (NSPersistentStoreDescription, Error?) -> Swift.Void) //加载持久化存储, 完成回调
open func performBackgroundTask(_ block: @escaping (NSManagedObjectContext) -> Swift.Void) //后台线程执行

在此, 我们需要事先了解下iOS10中Core Data的新特性NSPersistentContainer

NSEntityDescription:
@available(iOS 3.0, *)
open class NSEntityDescription : NSObject, NSCoding, NSCopying, NSFastEnumeration
open class func entity(forEntityName entityName: String, in context: NSManagedObjectContext) -> NSEntityDescription? //通过实例名及上下文获取对应实例描述
NSFetchRequest:
@available(iOS 3.0, *)
open class NSFetchRequest<ResultType : NSFetchRequestResult> : NSPersistentStoreRequest, NSCoding
@available(iOS 4.0, *)
    public convenience init(entityName: String) //通过实例名创建抓取请求
    open var predicate: NSPredicate? //谓词, 用于查询过滤
    open var sortDescriptors: [NSSortDescriptor]? //进行排序
    open var propertiesToFetch: [Any]? //属性抓取
@available(iOS 3.0, *)
    open var resultType: NSFetchRequestResultType //返回的结果类型
简易 Core Data Stack
  //懒加载一个持久化容器属性 
  lazy var persistentContainer: NSPersistentContainer = {
    let container = NSPersistentContainer(name: "HitList") //根据名称取出对应的持久化容器
    container.loadPersistentStores(completionHandler: { (storeDescription, error) in //加载持久化存储
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    })
    return container
  }()
实现 Core Data Stack
class CoreDataStack { //创建CoreDataStack类以备复用

  private let modelName: String //CoreData模型名称, 对应.xcdatamodeld文件名

  init(modelName: String) {
    self.modelName = modelName //初始化方法并赋值
  }

  lazy var managedContext: NSManagedObjectContext = { //懒加载托管对象上下文
    return self.storeContainer.viewContext
  }()

  private lazy var storeContainer: NSPersistentContainer = { //懒加载持续化容器

    let container = NSPersistentContainer(name: self.modelName)
    container.loadPersistentStores { (storeDescription, error) in
      if let error = error as NSError? {
        print("Unresolved error \(error), \(error.userInfo)")
      }
    }
    return container
  }()

  func saveContext () { //保存托管对象上下文
    guard managedContext.hasChanges else { return } //校验是否上下文有所改变

    do {
      try managedContext.save() //进行保存
    } catch let error as NSError {
      print("Unresolved error \(error), \(error.userInfo)")
    }
  }
}
简单演示如何存储:
  var people: [NSManagedObject] = [] //创建一个people数组, 数组内部都是托管对象Person实例
  //读取数据
  let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person") //通过实例名字抓取数据
  do {
    people = try managedContext.fetch(fetchRequest) //通过托管对象上下文获取数据并赋值给people数组
  } catch let error as NSError {
    print("Could not fetch. \(error), \(error.userInfo)")
  }
  //存储数据
  let entity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)! //通过实例名及托管对象上下文获取对应实例描述
  let person = NSManagedObject(entity: entity, insertInto: managedContext) //通过实例及托管对象上下文创建托管对象
  person.setValue(name, forKeyPath: "name") //通过KVC将新值赋值给name属性
  do {
    try managedContext.save() //保存托管对象上下文
    people.append(person) // 将实例添加至实例数组
  } catch let error as NSError { //如果存储失败抛出异常
    print("Could not save. \(error), \(error.userInfo)")
  }

这样我们就通过读取和存储, 了解了Core Data的基本概念了, 比起写SQL还是更有爱一点~

Core Data 模型制作

上一个例子中, 我们通过KVC的方式取值和赋值, 但我们的托管对象不可能永远只有一个属性, 所以通过字符串Key来取值的方法往往不是最佳方案, 我们看看如何更好地面向对象, 我们先使用图形化界面生成Core Data模型.

Core Data 文件, 添加了很多属性, 其中Type为Transformable 是指可转变为其他类型

通过Editor 可以进行创建对应的swift文件映射, 和IB很类似

只需要class 类名匹配即可

我们可以如图自动创建文件, 但ObjC.io的建议是自行输入为上, 因为并不是很神奇的东西, 只需要class 类名匹配即可. 生成的文件可以自行输入, Bowtie是继承自NSManagedObject的托管对象.

extension Bowtie {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<Bowtie> {
        return NSFetchRequest<Bowtie>(entityName: "Bowtie");
    }

    @NSManaged public var isFavorite: Bool
    @NSManaged public var lastWorn: NSDate?
    @NSManaged public var name: String?
    @NSManaged public var photoData: NSData?
    @NSManaged public var searchKey: String?
    @NSManaged public var timesWorn: Int32
    @NSManaged public var tintColor: NSObject?
    @NSManaged public var rating: Double
}

我们可以看到自动生成了图形化工具中的全部属性, 并创建了抓取函数, 方便使用. 注意之前的Type为Transformable, 生成出来就变成了NSObject?, 可以分析得知, 可转变顾名思义, 所有类型都是继承自NSObject, 当然是对于Objetcive-C来说, 不过对于Foundation框架中的类, Swift也是如此.

Core Data 关系映射

学会了制作Core Data模型后, 我们就要来谈谈各个实例之间的关系了, 各个实例化之间的关系也可以通过图形化界面来实现:

Dog 实例中 与Walk实例的关系 Type 设置为 To Many 作为集合

Walk 实例中 与Dog实例的关系 Type 设置为 To One 作为属性

设置Inverse, 隐射两个实体之间的对应关系, 图示双向箭头表示集合嵌套

RelationShips Core Data 模型
extension Dog {

  @nonobjc public class func fetchRequest() -> NSFetchRequest<Dog> {
    return NSFetchRequest<Dog>(entityName: "Dog");
  }

  @NSManaged public var name: String?
  @NSManaged public var walks: NSOrderedSet? //这里不是数组类型, 而是特殊的集合类型
}

// MARK: Generated accessors for walks
extension Dog { //由于RelationShips关系映射会自动生成集合增删改查逻辑

  @objc(insertObject:inWalksAtIndex:)
  @NSManaged public func insertIntoWalks(_ value: Walk, at idx: Int)

  @objc(removeObjectFromWalksAtIndex:)
  @NSManaged public func removeFromWalks(at idx: Int)

  @objc(insertWalks:atIndexes:)
  @NSManaged public func insertIntoWalks(_ values: [Walk], at indexes: NSIndexSet)

  @objc(removeWalksAtIndexes:)
  @NSManaged public func removeFromWalks(at indexes: NSIndexSet)

  @objc(replaceObjectInWalksAtIndex:withObject:)
  @NSManaged public func replaceWalks(at idx: Int, with value: Walk)

  @objc(replaceWalksAtIndexes:withWalks:)
  @NSManaged public func replaceWalks(at indexes: NSIndexSet, with values: [Walk])

  @objc(addWalksObject:)
  @NSManaged public func addToWalks(_ value: Walk)

  @objc(removeWalksObject:)
  @NSManaged public func removeFromWalks(_ value: Walk)

  @objc(addWalks:)
  @NSManaged public func addToWalks(_ values: NSOrderedSet)

  @objc(removeWalks:)
  @NSManaged public func removeFromWalks(_ values: NSOrderedSet)
}
extension Walk {

  @nonobjc public class func fetchRequest() -> NSFetchRequest<Walk> {
    return NSFetchRequest<Walk>(entityName: "Walk");
  }

  @NSManaged public var date: NSDate?
  @NSManaged public var dog: Dog?
}

看到生成后的两个实体之间的关系也很好理解, 只是未显示NSOrderedSet中包含的实体类型, 这点在Swift4中已经有改善

Core Data 获取操作

之前使用数据库的时候, 我们都会使用SQL查询语句 , select 字段 from 表 where 筛选条件, 进行数据的过滤, 而我们使用Core Data的时候用什么方法呢? 首先我们可以先了解以下类:

NSPredicate 谓词用于筛选过滤
@available(iOS 3.0, *)
open class NSPredicate : NSObject, NSSecureCoding, NSCopying 
    public /*not inherited*/ init(format predicateFormat: String, argumentArray arguments: [Any]?) //谓词格式, 用以过滤信息
NSSortDescriptor 用于数据的排序
open class NSSortDescriptor : NSObject, NSSecureCoding, NSCopying 
    public init(key: String?, ascending: Bool) //通过key值, 进行升序或降序的排序
NSExpressionDescription 对没有属性名的数据操作
  func populateDealsCountLabel() {

    let fetchRequest = NSFetchRequest<NSDictionary>(entityName: "Venue")
    fetchRequest.resultType = .dictionaryResultType //将返回结果设置为字典类型

    let sumExpressionDesc = NSExpressionDescription() //由于是字典类型并没有再图形化界面中设置, 故需要使用NSExpressionDescription
    sumExpressionDesc.name = "sumDeals"

    let specialCountExp = NSExpression(forKeyPath: #keyPath(Venue.specialCount)) //根据KVC获取属性值
    sumExpressionDesc.expression = NSExpression(forFunction: "sum:", arguments: [specialCountExp])
    sumExpressionDesc.expressionResultType = .integer32AttributeType

    fetchRequest.propertiesToFetch = [sumExpressionDesc] //进行获取

    do {
      let results = try coreDataStack.managedContext.fetch(fetchRequest)
      let resultDict = results.first!
      let numDeals = resultDict["sumDeals"]!
      numDealsLabel.text = "\(numDeals) total deals"

    } catch let error as NSError {
      print("Count not fetch \(error), \(error.userInfo)")
    }
  }
NSBatchUpdateRequest & NSAsynchronousFetchRequest 批量更新和异步更新
  override func viewDidLoad() {
    super.viewDidLoad()

    let batchUpdate = NSBatchUpdateRequest(entityName: "Venue") //初始化批量更新
    batchUpdate.propertiesToUpdate = [#keyPath(Venue.favorite) : true] //更新的属性
    batchUpdate.affectedStores = coreDataStack.managedContext.persistentStoreCoordinator?.persistentStores //持久化存储
    batchUpdate.resultType = .updatedObjectsCountResultType //设置类型

    do {
      let batchResult = try coreDataStack.managedContext.execute(batchUpdate) as! NSBatchUpdateResult
      print("Records updated \(batchResult.result!)")
    } catch let error as NSError {
      print("Could not update \(error), \(error.userInfo)")
    }
    
    fetchRequest = Venue.fetchRequest()

    asyncFetchRequest = NSAsynchronousFetchRequest<Venue>(fetchRequest: fetchRequest) { [unowned self] (result: NSAsynchronousFetchResult) in //异步获取

      guard let venues = result.finalResult else {
        return
      }

      self.venues = venues
      self.tableView.reloadData()
    }

    do {
      try coreDataStack.managedContext.execute(asyncFetchRequest)
    } catch let error as NSError {
      print("Could not fetch \(error), \(error.userInfo)")
    }
  }

Core Data 与 TableView 协作

Core Data中有一个类是和列表视图度身订造的, 那就是NSFetchedResultsController, 可以将完美的与TableView进行结合, 我们先来看下常用的方法:

NSFetchedResultsController
@available(iOS 3.0, *)
open class NSFetchedResultsController<ResultType : NSFetchRequestResult> : NSObject 
  public init(fetchRequest: NSFetchRequest<ResultType>, managedObjectContext context: NSManagedObjectContext, sectionNameKeyPath: String?, cacheName name: String?) //初始化
  open func performFetch() throws //获取
  unowned(unsafe) open var delegate: NSFetchedResultsControllerDelegate? //代理
  open func object(at indexPath: IndexPath) -> ResultType //获取特定对象

public protocol NSFetchedResultsSectionInfo 
  public var numberOfObjects: Int { get } //获取对象数量

其中sectionNameKeyPath参数是传入根据什么字段进行分组, 而cacheName, 则是进行内存缓存.

简单演示如何操作:
  override func viewDidLoad() {
    super.viewDidLoad()

    let fetchRequest: NSFetchRequest<Team> = Team.fetchRequest()

    let zoneSort = NSSortDescriptor(key: #keyPath(Team.qualifyingZone), ascending: true)
    let scoreSort = NSSortDescriptor(key: #keyPath(Team.wins), ascending: false)
    let nameSort = NSSortDescriptor(key: #keyPath(Team.teamName), ascending: true)

    fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort] //进行三层排序

    fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
                                                          managedObjectContext: coreDataStack.managedContext,
                                                          sectionNameKeyPath: #keyPath(Team.qualifyingZone),
                                                          cacheName: "worldCup")

    fetchedResultsController.delegate = self //设置代理

    do {
      try fetchedResultsController.performFetch() //进行获取数据
    } catch let error as NSError {
      print("Fetching error: \(error), \(error.userInfo)")
    }
  }
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {

  func numberOfSections(in tableView: UITableView) -> Int {
    guard let sections = fetchedResultsController.sections else { //section的数量
      return 0
    }

    return sections.count
  }

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

    guard let sectionInfo = fetchedResultsController.sections?[section] else { //row的数量
      return 0
    }

    return sectionInfo.numberOfObjects
  }

  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    let cell = tableView.dequeueReusableCell(withIdentifier: teamCellIdentifier, for: indexPath)
    guard let cell = cell as? TeamCell else {
      return
    }

    let team = fetchedResultsController.object(at: indexPath) //拿到对应cell的数据
    cell.flagImageView.image = UIImage(named: team.imageName!)
    cell.teamLabel.text = team.teamName
    cell.scoreLabel.text = "Wins: \(team.wins)"
    return cell
  }

  func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    let sectionInfo = fetchedResultsController.sections?[section]
    return sectionInfo?.name //拿到对象的名字
  }
}

// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {

  func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    let team = fetchedResultsController.object(at: indexPath)
    team.wins = team.wins + 1 //修改数据
    coreDataStack.saveContext() //存入Core Data
  }
}

extension ViewController: NSFetchedResultsControllerDelegate {

  //当上下文更改开始前调用
  func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.beginUpdates() //reloadData自定义方法开始
  }

  //当托管对象进行改变时调用
  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

    switch type {
    case .insert:
      tableView.insertRows(at: [newIndexPath!], with: .automatic)
    case .delete:
      tableView.deleteRows(at: [indexPath!], with: .automatic)
    case .update:
      let cell = tableView.cellForRow(at: indexPath!) as! TeamCell
      configure(cell: cell, for: indexPath!)
    case .move:
      tableView.deleteRows(at: [indexPath!], with: .automatic)
      tableView.insertRows(at: [newIndexPath!], with: .automatic)
    }
  }
  
  //当上下文更改结束后调用
  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.endUpdates()  //reloadData自定义方法结束
  }

  //当组信息进行改变的时候调用
  func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {

    let indexSet = IndexSet(integer: sectionIndex)

    switch type {
    case .insert:
      tableView.insertSections(indexSet, with: .automatic)
    case .delete:
      tableView.deleteSections(indexSet, with: .automatic)
    default: break
    }
  }
}

以上就是Core Data的学习概要, 当然还有更重要的Core Data的迁移及父子上下文和线程并发问题等没有讲到, 当然这些深层的东西还需要慢慢的研究, 还是之前的一句话 – 一般覆盖80%的页面缓存, 个人愚见使用写.plist的方式最为简单有效.

🌟 想要关注作者 请点这里🌟 »> 喜欢的朋友请点喜欢 »> 下载源码的同学请送下小星星 »> 有闲钱的壕们可以进行打赏 »> 小弟会尽快推出更好的文章和大家分享 »> 你的激励就是我的动力!!

最近的文章

SceneKit 不会 Unity3D 的另一种选择

上周一, 相信很多人和我一样, 全程观看了WWDC2017的开发者大会, 其中虽然亮点平平但也能些许的看出苹果未来的战略, 虽然已经从先驱者变成跟随者, 但强者恒强的道理是亘古不变的真理, 而且在生态链的建设上也是无人能出其右, 虽然在消费者眼中最为关注的是HomePod和iPad Pro10.5, 而在开发者眼中为之眼前一亮的则是ARKit和Core ML.Core ML 刚发布的时候还以为是终于能用Swift进行模型的训练了, 终于不用学习缩进地狱的Python了, 然而这仅仅是一...…

• 移动开发继续阅读
更早的文章

SpriteKit 瓦片地图什么的还挺好玩儿

说实话这个2D游戏实战的入门看的我脑浆子都沸腾了, 好多新的概念涌入, 没做过游戏开发的我表示真的难以接受, 吸收效率与之前相比也下降好多, 不过越往后学, 就能够加深对之前知识的掌握, 这可能也是看书的好处吧, 今天我也把对瓦片地图的一些学习经验记录下来供大家探讨.代码见:github说实话, 我很推荐Ray家的资源, 由浅入深手把手的教学, 内容前后呼应, 看几本书就能涵盖国内4个月培训班的课程体系. 遵循本系列一贯的风格, 我们还是从基础的API开始看起, 对API掌握熟练的话,...…

• 移动开发继续阅读