Core Data详细解析(四) —— 一个简单的入门示例(二)

版本记录

版本号 时间
V1.0 2018.09.26 星期三

前言

数据是移动端的重点关注对象,其中有一条就是数据存储。CoreData是苹果出的数据存储和持久化技术,面向对象进行数据相关存储。感兴趣的可以看下面几篇文章。
1. iOS CoreData(一)
2. iOS CoreData实现数据存储(二)
3. Core Data详细解析(三) —— 一个简单的入门示例(一)

Modeling Your Data - 建模数据

现在您知道如何检查持久性,您可以深入了解Core Data。 您对HitList应用程序的目标很简单:保留您输入的名称,以便在新应用程序启动后可以查看这些names

到目前为止,您一直使用普通的旧Swift字符串将names存储在内存中。 在本节中,您将使用Core Data对象替换这些字符串。

第一步是创建一个managed object model,该模型描述了Core Data在磁盘上表示数据的方式。

默认情况下,Core Data使用SQLite数据库作为持久性存储,因此您可以将数据模型视为数据库模式。

注意:在处理Core Data时,您会遇到相当多的“managed”。 如果您在类的名称中看到“managed”,例如在NSManagedObjectContext中,则可能正在处理Core Data类。 “Managed”是指Core Data对Core Data对象生命周期的管理。但是,不要假设所有Core Data类都包含“managed”一词。 大多数没有。 有关Core Data类的完整列表,请查看文档浏览器中的Core Data框架参考。

由于您已选择使用Core Data,因此Xcode会自动为您创建一个数据模型文件,并将其命名为HitList.xcdatamodeld

打开HitList.xcdatamodeld。 如您所见,Xcode拥有强大的数据模型编辑器:

数据模型编辑器具有许多您可以在以后探索的功能。 现在,让我们专注于创建一个Core Data实体。

单击左下方的Add Entity以创建新实体。 双击新实体并将其名称更改为Person,如下所示:

您可能想知道为什么模型编辑器使用术语Entity。你不是只是简单地定义一个新类吗?正如您将很快看到的,Core Data带有自己的词汇。以下是您将经常遇到的一些术语的快速概述:

  • entity是Core Data中的类定义。典型的例子是EmployeeCompany。在关系数据库中,实体对应于表。
  • attribute是附加到特定entity的一条信息。例如,Employee实体可以具有员工namepositionsalary`的属性。在数据库中,属性对应于表中的特定字段。
  • relationship是多个实体之间的链接。在Core Data中,两个实体之间的关系称为to-one relationships,而一个和多个实体之间的关系称为to-many relationships。例如,经理可以与一组员工建立一对多(to-many relationship)的关系,而个人员工通常与他的经理有一对一( to-one relationship)的关系。

注意:您可能已经注意到实体听起来很像类。 同样,属性和关系听起来很像属性。 有什么不同? 您可以将Core Data实体视为类定义,将managed object视为该类的实例。

现在您知道属性是什么,您可以向之前创建的Person对象添加属性。 仍然在HitList.xcdatamodeld中,选择左侧的Person并单击Attributes下的加号(+)

将新属性的名称设置为,比如说,name并将其类型更改为String

Core Data中,属性可以是多种数据类型之一。


Saving to Core Data - 保存到Core Data

打开ViewController.swift,在UIKit导入下面添加以下Core Data模块导入:

import CoreData

要在代码中开始使用Core Data API,只需导入即可。

接下来,使用以下内容替换names属性定义:

var people: [NSManagedObject] = []

您将存储Person实体而不是字符串名称,因此您将用作table view的数据模型的数组重命名为people。 它现在包含NSManagedObject的实例而不是简单的字符串。

NSManagedObject表示存储在Core Data中的单个对象; 您必须使用它来创建,编辑,保存和删除Core Data持久性存储。 正如您将很快看到的,NSManagedObject是一个形状移位器。 它可以采用数据模型中任何实体的形式,占用您定义的任何属性和关系。

由于您要更改table view的模型,因此还必须替换之前实现的两种数据源方法。 用以下内容替换您的UITableViewDataSource扩展:

// MARK: - UITableViewDataSource

extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView,
                 numberOfRowsInSection section: Int) -> Int {
    return people.count
  }

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

    let person = people[indexPath.row]
    let cell =
      tableView.dequeueReusableCell(withIdentifier: "Cell",
                                    for: indexPath)
    cell.textLabel?.text =
      person.value(forKeyPath: "name") as? String
    return cell
  }
}

这些方法最重要的变化发生在tableView(_:cellForRowAt :)中。 现在,您不是将单元格与模型数组中的相应字符串进行匹配,而是将单元格与相应的NSManagedObject匹配。

请注意如何从NSManagedObject中获取name属性。 如下所示:

cell.textLabel?.text =
  person.value(forKeyPath: "name") as? String

你为什么要这样做? 事实证明,NSManagedObject不知道您在数据模型中定义的name属性,因此无法直接使用属性访问它。 Core Data提供读取值的唯一方法是键值编码,通常称为KVC。

注意:KVC是Foundation中用于间接使用字符串访问对象属性的机制。 在这种情况下,KVC使NSMangedObject在运行时的行为有点像字典。键值编码可用于从NSObject继承的所有类,包括NSManagedObject。 您不能在不继承自NSObject的Swift对象上使用KVC访问属性。

接下来,找到addName(_ :)并用以下内容替换save UIAlertAction

let saveAction = UIAlertAction(title: "Save", style: .default) {
  [unowned self] action in
  
  guard let textField = alert.textFields?.first,
    let nameToSave = textField.text else {
      return
  }
  
  self.save(name: nameToSave)
  self.tableView.reloadData()
}

这将获取text field中的文本并将其传递给名为save(name :)的新方法。 Xcode报错因为save(name :)尚不存在。 在addName(_ :)下面添加它:

func save(name: String) {
  
  guard let appDelegate =
    UIApplication.shared.delegate as? AppDelegate else {
    return
  }
  
  // 1
  let managedContext =
    appDelegate.persistentContainer.viewContext
  
  // 2
  let entity =
    NSEntityDescription.entity(forEntityName: "Person",
                               in: managedContext)!
  
  let person = NSManagedObject(entity: entity,
                               insertInto: managedContext)
  
  // 3
  person.setValue(name, forKeyPath: "name")
  
  // 4
  do {
    try managedContext.save()
    people.append(person)
  } catch let error as NSError {
    print("Could not save. \(error), \(error.userInfo)")
  }
}

这就是Core Data的用武之地! 这是代码的作用:

  • 1)在从Core Data存储中保存或检索任何内容之前,首先需要开始使用NSManagedObjectContext。 您可以将managed object context视为内存中的“暂存器”,以便使用managed objects

考虑将新的managed object保存到Core Data,这是一个两步过程:首先,将新的managed object插入到managed object context; 一旦您满意,您可以“提交”managed object context中的更改以将其保存到磁盘。

Xcode已经生成了一个managed object context,作为新项目模板的一部分。 请记住,只有在您选中开始时Use Core Data复选框时才会发生这种情况。 此缺省managed object context作为应用程序委托中NSPersistentContainer的属性。 要访问它,首先要获得对app delegate的引用。

  • 2) 您创建一个新的managed object并将其插入到managed object context中。 您可以使用NSManagedObject的静态方法一步完成此操作:entity(forEntityName:in :)

您可能想知道NSEntityDescription是什么。 回想一下,NSManagedObject被称为shape-shifter类,因为它可以表示任何实体。 实体描述是在运行时将数据模型中的实体定义与NSManagedObject实例相链接的部分。

  • 3) 使用NSManagedObject,您可以使用键值编码设置name属性。 您必须完全按照数据模型中显示的方式拼写KVC键(在本例中为name),否则,您的应用程序将在运行时崩溃。

  • 4) 通过在managed object context中调用save,可以将更改提交给person并保存到磁盘。 注意save可能会抛出错误,这就是你在do-catch块中使用try关键字调用它的原因。 最后,将新的managed object插入到people数组中,以便在table view重新加载时显示。

这比使用字符串数组要复杂一点,但也不算太糟糕。 这里的一些代码,例如获取managed object contextentity,可以在您自己的init()viewDidLoad()中完成一次,然后再重复使用。 为简单起见,您可以使用相同的方法完成所有操作。

构建并运行应用程序,并在table view中添加一些名称:

如果names实际存储在Core Data中,则HitList应用程序应通过持久性测试。 将应用程序置于前台,转到快速应用程序切换器,然后终止它。

Springboard,点击HitList应用程序以触发新的启动。 等等,发生了什么? table view为空:

您保存到Core Data,但在新应用程序启动后,people数组是空的! 那是因为数据正在你的磁盘上,但你还没有显示它。


Fetching from Core Data - 从Core Data中获取

要将持久存储中的数据导入到managed object context,您必须fetch它。 打开ViewController.swift并在viewDidLoad()下面添加以下内容:

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  
  //1
  guard let appDelegate =
    UIApplication.shared.delegate as? AppDelegate else {
      return
  }
  
  let managedContext =
    appDelegate.persistentContainer.viewContext
  
  //2
  let fetchRequest =
    NSFetchRequest<NSManagedObject>(entityName: "Person")
  
  //3
  do {
    people = try managedContext.fetch(fetchRequest)
  } catch let error as NSError {
    print("Could not fetch. \(error), \(error.userInfo)")
  }
}

下面一步一步看代码的作用:

  • 1) 在使用Core Data执行任何操作之前,您需要一个managed object contextFetching没有什么不同! 像以前一样,您启动应用程序委托并获取对其持久容器的引用以获取其NSManagedObjectContext

  • 2) 顾名思义,NSFetchRequest是负责从Core Data获取数据的类。 获取请求既强大又灵活。 您可以使用获取请求来获取符合所提供标准的一组对象(即,让我所有员工都住在威斯康星州并且已经在公司工作至少三年),个人价值(即在数据库中给我最长的名字)和 更多。

Fetch请求有几个限定符用于优化返回的结果集。 现在,你应该知道NSEntityDescription是这些必需的限定符之一。

设置获取请求的entity属性,或者使用init(entityName :)初始化它,获取特定实体的所有对象。 这是你在这里获取所有Person实体的方法。 另请注意,NSFetchRequest是泛型类型。 泛型的使用指定了获取请求的预期返回类型,在本例中为NSManagedObject

  • 3) 您将fetch请求移交给managed object context以执行繁重的工作。 fetch(_ :)返回符合获取请求指定条件的managed object数组。

注意:与save()一样,fetch(_ :)也会抛出错误,因此您必须在do block中使用它。 如果在fetch期间发生错误,您可以检查catch block内的错误并进行适当的响应。

构建并运行应用程序。 您应该立即看到之前添加的names列表:

在列表中添加更多names并重新启动应用程序以验证保存和提取是否正常。 如果没有删除应用程序,重置模拟器或将手机从高层建筑物中移开,则无论如何都会在table view中显示名称。

注意:此示例应用程序中有一些粗略的边缘:您必须每次从应用程序委托获取managed object context,并且您使用KVC访问实体的属性而不是更自然的对象样式person.name


Key Points - 关键点

  • Core Data提供磁盘持久性(on-disk persistence),这意味着即使在终止应用程序或关闭设备后,您的数据也可以访问。 这与内存中持久性不同,内存持久性只会在应用程序位于内存中时保存您的数据,无论是在前台还是在后台。
  • Xcode附带了一个功能强大的数据模型编辑器(Data Model editor),您可以使用它来创建managed object model
  • managed object模型由entitiesattributesrelationships组成
  • 实体entitiesCore Data中的类定义。
  • 属性attributes是附加到实体的一条信息。
  • 关系relationships是多个实体之间的链接。
  • NSManagedObject是Core Data实体的运行时表示。 您可以使用键值编码来读取和写入其属性。
  • 您需要NSManagedObjectContext来从或者向Core Data save()fetch(_:)读取和写入数据。

源码

下面看一下Swift源码。

1. ViewController.swift
import UIKit
import CoreData

class ViewController: UIViewController {

  @IBOutlet weak var tableView: UITableView!
  var people: [NSManagedObject] = []

  override func viewDidLoad() {
    super.viewDidLoad()

    title = "The List"
    tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
  }

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
      return
    }

    let managedContext = appDelegate.persistentContainer.viewContext
    let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Person")

    do {
      people = try managedContext.fetch(fetchRequest)
    } catch let error as NSError {
      print("Could not fetch. \(error), \(error.userInfo)")
    }
  }

  @IBAction func addName(_ sender: UIBarButtonItem) {
    let alert = UIAlertController(title: "New Name", message: "Add a new name", preferredStyle: .alert)

    let saveAction = UIAlertAction(title: "Save", style: .default) { [unowned self] action in

      guard let textField = alert.textFields?.first,
        let nameToSave = textField.text else {
          return
      }

      self.save(name: nameToSave)
      self.tableView.reloadData()
    }

    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel)
    alert.addTextField()
    alert.addAction(saveAction)
    alert.addAction(cancelAction)

    present(alert, animated: true)
  }

  func save(name: String) {
    guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
      return
    }

    let managedContext = appDelegate.persistentContainer.viewContext
    let entity = NSEntityDescription.entity(forEntityName: "Person", in: managedContext)!
    let person = NSManagedObject(entity: entity, insertInto: managedContext)
    person.setValue(name, forKeyPath: "name")

    do {
      try managedContext.save()
      people.append(person)
    } catch let error as NSError {
      print("Could not save. \(error), \(error.userInfo)")
    }
  }
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {

  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return people.count
  }

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

    let person = people[indexPath.row]
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    cell.textLabel?.text = person.value(forKeyPath: "name") as? String
    return cell
  }
}

下面看一下效果展示

后记

好几天没有更新了,因为这几天做封闭区块链开发,今天有时间就更新了下,希望大家能喜欢,给个赞或者关注,祝大家中秋快乐(迟到的祝福!)。

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

推荐阅读更多精彩内容