跳转至

2.NSManagedObject子类

在第1章中,您已经熟悉了一个简单的Core Data应用程序;现在是时候探索更多的核心数据所提供的!

本章的核心是对NSManagedObject进行子类化,以便为每个数据实体创建自己的类。 这将在数据模型编辑器中的实体和代码中的类之间创建直接的一对一映射。 这意味着在代码的某些部分,您可以使用对象和属性,而无需过多担心核心数据方面的事情。

在此过程中,您将了解Core Data实体中可用的所有数据类型,包括一些常见的字符串和数字类型之外的数据类型。 通过所有可用的数据类型选项,您还将了解验证数据以在保存前自动检查值。

入门

转到本书附带的文件,并在starter文件夹中打开名为 BowTies 的示例项目。 和 HitList 一样,这个项目使用了XcodeCore Data模板。 像以前一样,这意味着Xcode生成了自己的现成的Core Data堆栈,位于 AppDelegate.swift 中。

打开 Main.storyboard。 在这里,您将找到示例项目的单页UI

img

正如您可能猜到的,BowTies是一个轻量级的领结管理应用程序。 你可以在你拥有的不同颜色的领结之间切换--应用程序假设每种颜色都有一个--使用最上面的分段控件。 点击“R”代表红色,“O”代表橙子,依此类推。

点击特定的颜色会弹出领带的图像,并在屏幕上填充几个标签,其中包含有关领带的特定信息。 这包括:

  • 领结的名称(这样你就可以区分颜色相似的领结)-你戴领结的次数-你最后一次戴领结的日期-这条领带是否是你的最爱

左下角的 Wear 按钮会增加您佩戴该领带的次数,并将“最后一次佩戴”日期设置为今天。

橙子不适合你吗 不用担心 右下角的 Rate 按钮可以更改领结的评级。 该特定评级系统使用从0到5的标度,允许十进制值。

这就是应用程序在其最终状态下应该做的事情。 打开 ViewController.swift,看看它现在做什么:

import UIKit

class ViewController: UIViewController {

  // MARK: - IBOutlets
  @IBOutlet weak var segmentedControl: UISegmentedControl!
  @IBOutlet weak var imageView: UIImageView!
  @IBOutlet weak var nameLabel: UILabel!
  @IBOutlet weak var ratingLabel: UILabel!
  @IBOutlet weak var timesWornLabel: UILabel!
  @IBOutlet weak var lastWornLabel: UILabel!
  @IBOutlet weak var favoriteLabel: UILabel!
  @IBOutlet weak var wearButton: UIButton!
  @IBOutlet weak var rateButton: UIButton!

  // MARK: - View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()
  }

  // MARK: - IBActions
  @IBAction func segmentedControl(
    _ sender: UISegmentedControl) {

  }

  @IBAction func wear(_ sender: UIButton) {

  }

  @IBAction func rate(_ sender: UIButton) {

  }
}

坏消息是在目前的状态下,领结什么也不做。 好消息是您不需要执行任何Ctrl拖动!

用户界面上的分段控件和所有标签都已经在代码中连接到IBOutlets。 另外,分段控件、WearRate按钮都有对应的IBActions

看起来你已经拥有了开始添加一些核心数据所需的一切-但是等等,你要在屏幕上显示什么? 没有输入法可言,所以应用程序必须附带样本数据。 完全正确。 BowTies包含一个名为 SampleData.plist 的属性列表,其中包含七个样本领带的信息,彩虹的每种颜色对应一个领带。 img

此外,应用程序的资产目录 Assets.xcassets 包含与 SampleData.plist 中的七个领结对应的七个图像。

您现在要做的是获取此示例数据,将其存储在Core Data中,并使用它来实现领结管理功能。

数据建模

在上一章中,您了解了在开始一个新的Core Data项目时必须做的第一件事之一是创建数据模型。

打开 BowTies.xcdatamodeld,点击左下方【添加实体】,新建实体。 双击新实体并将其名称更改为 BowTie,如下所示: img

在上一章中,您创建了一个简单的Person实体,它具有一个字符串属性来保存人名。 Core Data支持其他几种数据类型,您将在新的BowTie实体中使用其中的大多数。

属性的数据类型决定了您可以在其中存储什么类型的数据以及它将占用磁盘上的多少空间。 在Core Data中,属性的数据类型以Undefined开头,因此您必须将其更改为其他类型。

如果您还记得 SampleData.plist 中的内容,每个领结都有十条相关的信息。 这意味着BowTie实体将在模型编辑器中至少有十个属性。

选择左侧的BowTie,然后单击 属性 下的加号(+)。 将新属性的名称更改为 name,类型设置为 Stringimg

再重复此过程七次,以添加以下属性:

  • 名为 isFavorite Boolean
  • 名为 lastWorn Date
  • 一个 Double 命名的 rating
  • 一个名为 searchKeyString
  • 名为 timesWornInteger 32
  • 名为 id UUID
  • 名为 url URI

这些数据类型中的大多数在日常编程中很常见。 如果您以前没有听说过 UUID,它是 universally unique identifier 的缩写,通常用于唯一标识信息。

URI 代表 统一资源标识符 ,用于命名和标识文件、网页等不同资源。 事实上,所有的URL都是URI

完成后,Attributes部分应类似于以下内容:

img

不要担心属性的顺序是否不同-重要的是属性名称和类型是否正确。

Note

您可能已经注意到,timesWorn integer属性有三个选项: Integer 16 Integer 32 Integer 64。 16、32和64是指表示整数的位数。 这一点很重要,原因有二:位的数量反映了一个整数在磁盘上占据了多少空间,以及它可以表示多少值,也称为它的range。 下面是三种类型的整数的范围: 16位整数的范围:-32768至32767 32位整数的范围:-2147483648至2147483647 64位整数的范围:-9223372036854775808至9223372036854775807 你怎么选择? 数据的来源将决定最佳的整数类型。 你假设你的用户真的很喜欢领结,所以一个32位的整数应该提供足够的存储空间,可以让你一生都戴着领结。

每个领结都有一个相关的图像。 您将如何将其存储在核心数据中? 在BowTie实体中增加一个属性,命名为 photoData,数据类型改为 Binary Data

img

Core Data提供了将任意二进制数据blob直接存储在数据模型中的选项。 这些可以是任何东西,从图像到PDF文件,再到任何可以序列化为01的东西。

正如你所能想象的,这种便利可能会付出高昂的代价。 将大量二进制数据存储在与其他属性相同的SQLite数据库中可能会影响应用的性能。 这意味着每次访问一个实体时,一个巨大的二进制blob将被加载到内存中,即使您只需要访问它的名称!

幸运的是,Core Data预见到了这个问题。 选择photoData属性,打开属性检查,勾选允许外存储选项。

img

当您启用 Allows External Storage 时,Core Data会根据每个值试探性地决定是将数据直接保存在数据库中,还是存储指向单独文件的URI

Note

Allows External Storage 选项仅对二进制数据属性类型可用。 此外,如果启用此属性,则无法使用此属性查询Core Data。

总之,除了字符串,整数,双精度,布尔值和日期之外,Core Data还可以保存二进制数据,并且可以高效和智能地保存。

Core Data中存储非标准数据类型

不过,还有许多其他类型的数据,你可能要保存。 例如,如果你必须存储一个UIColor的实例,你会怎么做?

使用到目前为止提供的选项,您必须将颜色分解为单个组件并将其保存为整数(例如,red: 255, green: 101, blue: 155)。 然后,在获取这些组件之后,您必须在运行时重新构建颜色。

或者,您可以将UIColor实例序列化为Data并将其保存为二进制数据。 然后,您还必须“加水”,以便将二进制数据重新构造回您最初想要的UIColor对象。

再一次,核心数据支持你。 如果您仔细查看 SampleData.plist ,您可能会注意到每个领结都有一个相关的颜色。 在模型编辑器中选择BowTie实体,添加一个新属性,名为 tintColor,类型为 Transformable

img

可转换属性持久化未在Xcode的数据模型检查器中列出的数据类型。 这些类型包括Apple在其框架中提供的类型,如UIColorCLLocationCoordinate 2D以及您自己的类型。

可转换属性非常强大和灵活,但你需要做一些工作,告诉iOS如何将这些类型转换为Data。 要使属性可变换,必须满足三个要求:

  1. NSSecureCoding协议一致性添加到后台数据类型。
  2. 创建并注册NSSecureUnarchiveFromDataTransformer子类。
  3. 将自定义数据转换器子类与数据模型编辑器中的Transformable属性相关联。

由于您正在处理UIColor,好消息是它已经符合NSSecureCodingApple框架中的大多数数据类型都是如此。 万岁!

要满足第二个要求,请单击 File\New\File... 并从可可Touch模板创建一个文件。 将文件命名为ColorAttributeTransformer,并使其成为NSSecureUnarchiveFromDataTransformer的子类。 单击【下一步】,将文件保存到项目中。

img

接下来,用下面的实现替换新文件的内容。

import UIKit

class ColorAttributeTransformer:
  NSSecureUnarchiveFromDataTransformer {

  //1
  override static var allowedTopLevelClasses: [AnyClass] {
    [UIColor.self]
  }

  //2
  static func register() {
    let className =
      String(describing: ColorAttributeTransformer.self)
    let name = NSValueTransformerName(className)

    let transformer = ColorAttributeTransformer()
    ValueTransformer.setValueTransformer(
      transformer, forName: name)
  }
}

下面是这段代码的作用:

  1. 重载allowedTopLevelClasses返回此数据转换器可以解码的类列表。 我们希望持久化并检索UIColor的实例,因此在这里返回一个仅包含该类的数组。
  2. 顾名思义,静态函数register()可以帮助您使用ValueTransformer注册子类。 但你为什么要这么做 ValueTransformer维护了一个键值映射,其中key是您使用NSValueTransformerName提供的名称,value是对应transformer的实例。 稍后在数据模型编辑器中将需要此映射。

接下来打开 AppDelegate.swift,将application(_:didFinishLaunchingWithOptions:)替换为如下实现:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions
  launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
                 -> Bool {

  ColorAttributeTransformer.register()

  return true
}

在这里,您使用前面实现的静态方法注册数据转换器。 注册可以在应用程序设置Core Data堆栈之前的任何时候进行。

接下来,返回 BowTies.xcdatamodeld,选择 tintColor 属性,打开数据模型检查器。

Transformer的值更改为 ColorAttributeTransformer,将Custom Class设置为 UIColor

img

您的数据模型现在已经完成。 BowTie实体有十个属性,它需要将所有信息存储在 SampleData.plist 中。

托管对象子类

在上一章的示例项目中,您使用了键-值编码来访问Person实体的属性。 它看起来类似于以下内容:

// Set the name
person.setValue(aName, forKeyPath: "name")

// Get the name
let name = person.value(forKeyPath: "name")

即使您可以使用键-值编码直接在NSManagedObject上执行所有操作,但这并不意味着您应该这样做!

键值编码的最大问题是使用字符串而不是强类型类访问数据。 这通常被戏称为编写stringly typed代码。

你可能从经验中知道,字符串类型的代码很容易出现愚蠢的人为错误,比如输入错误和拼写错误。 键值编码也没有充分利用Swift的类型检查和Xcode的自动完成。 “一定还有别的办法!” 你可能在想,你是对的

键值编码的最佳替代方法是为数据模型中的每个实体创建NSManagedObject子类。 这意味着将有一个BowTie类,每个属性都有正确的类型。

Xcode可以手动或自动为您生成子类。 为什么你想让Xcode为你做这件事? 如果您从来不需要查看或更改这些子类文件,那么生成这些子类文件并使它们在您的项目中变得杂乱可能会有点麻烦。 从Xcode 8开始,您可以选择在每个实体的基础上让Xcode自动生成和更新这些文件,并将它们存储在项目的派生数据文件夹中。

使用模型编辑器时,此设置位于 Data Model 检查器的 Codegen 字段中。 因为您在本书中学习的是Core Data,所以您不会使用自动代码生成,因为它可以帮助您轻松地查看已为您生成的文件。

确保 BowTies.xcdatamodeld 仍然打开,选择BowTie实体并打开数据模型检查器。 设置 Codegen 下拉菜单为 Manual/None ,如下图所示:

img

Note

请确保在第一次编译之前,在将BowTie实体添加到模型之后,更改此代码生成设置。如果你在第一次编译后设置代码生成设置,你将有两个版本的托管对象子类:一个在派生数据中,另一个在源代码中。 如果发生这种情况,当您尝试再次编译时,您将遇到问题。

接下来,转到 Editor\Create NSManagedObject Subclass...。 选择数据模型,然后在接下来的两个对话框中选择BowTie实体。 单击 Create 按钮,保存文件。

Xcode生成了两个Swift文件,一个名为 BowTie+CoreDataClass.swift ,另一个名为 BowTie+CoreDataProperties.swift。 打开 BowTie+CoreDataClass.swift。 它应该类似于以下内容:

import Foundation
import CoreData

@objc(BowTie)
public class BowTie: NSManagedObject {

}

接下来打开 BowTie+CoreDataProperties.swift。 生成的属性可能与此处显示的顺序不同,但文件应类似于以下内容:

import Foundation
import CoreData

extension BowTie {

  @nonobjc public class func fetchRequest() 
    -> NSFetchRequest<BowTie> {

    return NSFetchRequest<BowTie>(entityName: "BowTie")
  }

  @NSManaged public var name: String?
  @NSManaged public var isFavorite: Bool
  @NSManaged public var lastWorn: Date?
  @NSManaged public var rating: Double
  @NSManaged public var searchKey: String?
  @NSManaged public var timesWorn: Int32
  @NSManaged public var id: UUID?
  @NSManaged public var url: URL?
  @NSManaged public var photoData: Data?
  @NSManaged public var tintColor: UIColor?
}

extension BowTie : Identifiable {

}

在面向对象的说法中,对象是一组 values 沿着在这些值上定义的一组 operations。 在这种情况下,Xcode将这两个东西分成两个单独的文件。 值(即对应于数据模型中BowTie属性的属性)在 BowTie+CoreDataProperties.swift 中,而操作在当前为空的 BowTie+CoreDataClass.swift 中。

Note

您可能想知道为什么会生成两个单独的文件。 在NSManagedObject子类中添加自定义代码是很常见的。 如果您在模型编辑器中更新了BowTie实体并再次转到 Editor\Create NSManagedObject Subclass... ,您不会希望丢失这些代码。 代码创建是智能的-如果 BowTie+CoreDataClass.swift 已经存在,它只会创建 BowTie+CoreDataProperties.swift,保留您所有的自定义代码。 这就是Core Data生成两个文件的主要原因,而不是像以前版本的Xcode那样生成一个文件。

Xcode已经为数据模型中的每个属性创建了一个类。 由于它已经创建了UIColor的属性,因此您需要在import CoreData下面添加以下import UIKit来修复错误。

模型编辑器中的每个属性类型在FoundationSwift标准库中都有对应的类。 下面是属性类型到运行时类的完整映射:

  • String maps to String?
  • Integer 16 maps to Int16
  • Integer 32 maps to Int32
  • Integer 64 maps to Int64
  • Float maps to Float
  • Double maps to Double
  • Boolean maps to Bool
  • Decimal maps to NSDecimalNumber?
  • Date maps to Date?
  • URI maps to URL?
  • UUID maps to UUID?
  • Binary data maps to Data?
  • Transformable maps to NSObject?

Note

类似于Objective-C中的@dynamic@NSCmanaged属性通知Swift编译器,属性的后备存储和实现将在运行时而不是编译时提供。正常模式是由内存中的实例变量支持属性。 托管对象的属性不同:它由托管对象上下文支持,因此在编译时不知道数据的源。请注意,由于您为tintColor属性提供了UIColorCustom Class,因此Core Data已使用UIColor?生成该属性 而不是NSObject?

恭喜你,你已经在Swift中创建了你的第一个托管对象子类!

与键-值编码相比,这是一种更好的处理核心数据实体的方法,有两个主要好处:

  1. 托管对象子类释放了Swift属性的语法能力。 通过使用properties而不是key-value编码来访问属性,您可以与Xcode和编译器友好相处。
  2. 您可以重写现有方法或添加自己的方法。 请注意,有一些NSManagedObject方法您永远不能覆盖。 查看AppleNSManagedObject文档以获取完整列表。

为了确保数据模型和新的托管对象子类之间的一切都正确连接,您将执行一个小测试。

打开 AppDelegate.swift,将application(_:didFinishLaunchingWithOptions:)替换为如下实现:

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions
  launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
                 -> Bool {

  ColorAttributeTransformer.register()

  // Save test bow tie
  let bowtie = NSEntityDescription.insertNewObject(
    forEntityName: "BowTie",
    into: self.persistentContainer.viewContext) as! BowTie
  bowtie.name = "My bow tie"
  bowtie.lastWorn = Date()
  saveContext()

  // Retrieve test bow tie
  let request: NSFetchRequest<BowTie> = BowTie.fetchRequest()

  if let ties =
    try? self.persistentContainer.viewContext.fetch(request),
    let testName = ties.first?.name,
    let testLastWorn = ties.first?.lastWorn {
    print("Name: \(testName), Worn: \(testLastWorn)")
  } else {
    print("Test failed.")
  }

  return true
}

在应用程序启动时,此测试将创建一个领结,并在保存托管对象上下文之前设置其namelastWorn属性。紧接着,它获取所有的BowTie实体,并将第一个实体的名称和lastWorn日期打印到控制台;现在应该只有一个构建并运行应用程序,并密切关注控制台:

Name: My bow tie, Worn: 2020-09-02 15:05:13 +0000

如果您已经仔细地沿着了以下操作,那么namelastWorn将按预期打印到控制台。 这意味着您能够成功地保存和获取BowTie托管对象子类。 掌握了这些新知识之后,就可以实现整个示例应用程序了。

传播托管上下文

打开 ViewController.swift,在import UIKit下面添加如下内容:

import CoreData

接下来,在最后一个IBOutlet属性下面添加以下内容:

// MARK: - Properties
var managedContext: NSManagedObjectContext!

重申一下,在您可以在Core Data中执行任何操作之前,您首先必须获得要使用的NSManagedObjectContext。 了解如何将托管对象上下文 propagate 到应用的不同部分是Core Data编程的一个重要方面。

打开 AppDelegate.swift,将当前包含测试代码的application(_:didFinishLaunchingWithOptions:)替换为之前的实现:

func application(_ application: UIApplication,
                  didFinishLaunchingWithOptions
  launchOptions: [UIApplication.LaunchOptionsKey: Any]?)
  -> Bool {
  ColorAttributeTransformer.register()
  return true
}

你有七个领结等着进入你的核心数据库。 打开 ViewController.swift,在rate(_:)下面添加如下方法:

// Insert sample data
func insertSampleData() {

  let fetch: NSFetchRequest<BowTie> = BowTie.fetchRequest()
  fetch.predicate = NSPredicate(format: "searchKey != nil")

  let tieCount = (try? managedContext.count(for: fetch)) ?? 0

  if tieCount > 0 {
    // SampleData.plist data already in Core Data
    return
  }

  let path = Bundle.main.path(forResource: "SampleData",
                              ofType: "plist")
  let dataArray = NSArray(contentsOfFile: path!)!

  for dict in dataArray {
    let entity = NSEntityDescription.entity(
      forEntityName: "BowTie",
      in: managedContext)!
    let bowtie = BowTie(entity: entity,
                        insertInto: managedContext)
    let btDict = dict as! [String: Any]

    bowtie.id = UUID(uuidString: btDict["id"] as! String)
    bowtie.name = btDict["name"] as? String
    bowtie.searchKey = btDict["searchKey"] as? String
    bowtie.rating = btDict["rating"] as! Double
    let colorDict = btDict["tintColor"] as! [String: Any]
    bowtie.tintColor = UIColor.color(dict: colorDict)

    let imageName = btDict["imageName"] as? String
    let image = UIImage(named: imageName!)
    bowtie.photoData = image?.pngData()
    bowtie.lastWorn = btDict["lastWorn"] as? Date

    let timesNumber = btDict["timesWorn"] as! NSNumber
    bowtie.timesWorn = timesNumber.int32Value
    bowtie.isFavorite = btDict["isFavorite"] as! Bool
    bowtie.url = URL(string: btDict["url"] as! String)
  }
  try? managedContext.save()
}

Xcode将报告UIColor上缺少方法声明。 要解决此问题,请将以下私有UIColor扩展名添加到文件末尾最后一个大括号下方。

private extension UIColor {

  static func color(dict: [String: Any]) -> UIColor? {
    guard 
      let red = dict["red"] as? NSNumber,
      let green = dict["green"] as? NSNumber,
      let blue = dict["blue"] as? NSNumber else {
        return nil
    }

    return UIColor(
      red: CGFloat(truncating: red) / 255.0,
      green: CGFloat(truncating: green) / 255.0,
      blue: CGFloat(truncating: blue) / 255.0,
      alpha: 1)
  }
}

这是相当多的代码,但它都是相对简单的。 第一个方法,insertSampleData,检查是否有蝴蝶结; 你以后会知道这是怎么回事 如果不存在,它会抓取 SampleData.plist 中的领结信息,遍历每个领结字典,并将新的BowTie实体插入到Core Data存储中。 在此迭代结束时,它保存托管对象上下文属性以将这些更改提交到磁盘。

您通过私有扩展添加到UIColorcolor(dict:)方法也很简单。 SampleData.plist 将颜色存储在一个包含三个键的字典中:redgreenblue。 这个静态方法接受这个字典并返回一个真正的UIColor

这里有两件事需要特别注意:

  1. 您在Core Data中存储图像的方式。 特性列表包含每个领结的文件名,而不是文件图像-实际图像位于项目的资源目录中。 使用此文件名,您将实例化UIImage,并立即通过pngData()将其转换为Data,然后将其存储在imageData属性中。
  2. 你储存颜色的方式 即使颜色存储在可转换属性中,在将其存储在tintColor中之前,也不需要任何特殊处理。 您只需设置属性即可。

前面的方法将 SampleData.plist 中的所有领结数据插入到Core Data中。 现在您需要从某个地方访问数据!

接下来,将viewDidLoad()替换为以下实现:

// MARK: - View Life Cycle
override func viewDidLoad() {
  super.viewDidLoad()

  let appDelegate = 
    UIApplication.shared.delegate as? AppDelegate
  managedContext = appDelegate?.persistentContainer.viewContext

  //1
  insertSampleData()

  //2
  let request: NSFetchRequest<BowTie> = BowTie.fetchRequest()
  let firstTitle = segmentedControl.titleForSegment(at: 0) ?? ""
  request.predicate = NSPredicate(
    format: "%K = %@",
    argumentArray: [#keyPath(BowTie.searchKey), firstTitle])

  do {
    //3
    let results = try managedContext.fetch(request)

    //4
    if let tie = results.first {
      populate(bowtie: tie)
    }
  } catch let error as NSError {
    print("Could not fetch \(error), \(error.userInfo)")
  }
}

这是您从Core Data获取领结并填充UI的地方。

一步一步来,下面是你对这段代码所做的:

  1. 调用前面实现的insertSampleData()。 由于每次启动应用程序时都可以调用viewDidLoad(),因此insertSampleData()会执行一次提取,以确保不会多次将样本数据插入Core Data
  2. 您创建一个获取请求,用于获取新插入的BowTie实体。 分段控件具有按颜色进行筛选的选项卡,因此谓词添加条件以查找与所选颜色匹配的领结。谓词既非常灵活又非常强大--您将在第4章“中间获取”中阅读更多关于谓词的内容。 现在,我们知道这个特定的谓词正在查找领结,其searchKey属性设置为分段控件的第一个按钮标题:在本例中为 R
  3. 与往常一样,托管对象上下文为您完成了繁重的工作。 它执行您刚才创建的fetch请求,并返回一个BowTie对象数组。
  4. 您使用results数组中的第一个领结填充用户界面。 如果出现错误,则将错误打印到控制台。

你还没有定义populate方法,所以Xcode抛出了一个警告。 在insertSampleData()下面添加如下实现:

func populate(bowtie: BowTie) {

  guard let imageData = bowtie.photoData as Data?,
    let lastWorn = bowtie.lastWorn as Date?,
    let tintColor = bowtie.tintColor else {
      return
  }

  imageView.image = UIImage(data: imageData)
  nameLabel.text = bowtie.name
  ratingLabel.text = "Rating: \(bowtie.rating)/5"

  timesWornLabel.text = "# times worn: \(bowtie.timesWorn)"

  let dateFormatter = DateFormatter()
  dateFormatter.dateStyle = .short
  dateFormatter.timeStyle = .none

  lastWornLabel.text =
    "Last worn: " + dateFormatter.string(from: lastWorn)

  favoriteLabel.isHidden = !bowtie.isFavorite
  view.tintColor = tintColor
}

在领结中定义的大多数属性都有一个UI元素。 由于Core Data只将图像存储为二进制数据的blob,因此您的工作是将其重新构造回图像,以便视图控制器的图像视图可以使用它。

同样,您不能直接使用lastWorndate属性。 您首先需要创建一个日期格式化程序,将日期转换为人们可以理解的字符串。

最后,存储领结颜色的tintColor可转换属性改变屏幕上所有元素的颜色,而不是一个元素的颜色。 只需在视图控制器的视图上设置色彩,然后就可以了! 现在一切都是相同的颜色。

Note

Xcode生成一些NSManagedObject子类属性作为可选类型。 这就是为什么在populate方法中,您在方法的开头使用guard语句来展开BowTie上的一些Core Data属性。

构建并运行应用程序。红色领结出现在屏幕上,如下所示:

img

WearRate 按钮目前没有任何功能。 轻敲分段控件的不同部分也不会执行任何操作。 你还有工作要做!

首先,您需要跟踪当前选择的领结,以便您可以在课堂上的任何地方引用它。 还是在 ViewController.swift 中,在managedContext下面添加如下属性:

var currentBowTie: BowTie!

接下来,在viewDidLoad()中的do-catch语句中找到populate(bowtie:)的调用,并在其上方添加以下行,以设置currentBowTie的初始值:

currentBowTie = tie

要实现 WearRate 按钮,必须跟踪当前选择的领结,因为这些操作仅影响当前领结。

用户每次点击Wear按钮,按钮都会执行wear(_:)操作方法。 但wear(_:)目前为空。 将wear(_:)实现替换为以下内容:

@IBAction func wear(_ sender: UIButton) {  
  currentBowTie.timesWorn += 1
  currentBowTie.lastWorn = Date()

  do {
    try managedContext.save()
    populate(bowtie: currentBowTie)    
  } catch let error as NSError {    
    print("Could not fetch \(error), \(error.userInfo)")
  }
}

此方法采用当前选定的领结,并将其timesWorn属性加1。 接下来,将lastWorn日期更改为今天,并保存托管对象上下文以将这些更改提交到磁盘。 最后,填充用户界面以可视化这些更改。

构建并运行应用程序,然后点击 Wear,次数不限。 它看起来像你彻底享受永恒的优雅的红色领结!

img

同样的,每次用户点击 Rate,都会执行代码中的rate(_:)action方法。 rate(_:)当前为空。 将rate(_:)的实现替换为:

@IBAction func rate(_ sender: UIButton) {

  let alert = UIAlertController(title: "New Rating",
                                message: "Rate this bow tie",
                                preferredStyle: .alert)

  alert.addTextField { textField in
    textField.keyboardType = .decimalPad
  }

  let cancelAction = UIAlertAction(title: "Cancel",
                                   style: .cancel)

  let saveAction = UIAlertAction(
    title: "Save",
    style: .default
    ) { [unowned self] _ in
      if let textField = alert.textFields?.first {
        self.update(rating: textField.text)
      }
    }

  alert.addAction(cancelAction)
  alert.addAction(saveAction)

  present(alert, animated: true)
}

点击 Rate 现在会显示一个带有单个文本字段、取消按钮和保存按钮的警报视图控制器。 点击保存按钮调用update(rating:),这...

哎呀,你还没有定义这个方法。 通过在populate(bowtie:)下面添加以下实现来满足Xcode

func update(rating: String?) {

  guard let ratingString = rating,
    let rating = Double(ratingString) else {
      return
  }

  do {
    currentBowTie.rating = rating
    try managedContext.save()
    populate(bowtie: currentBowTie)
  } catch let error as NSError {    
    print("Could not save \(error), \(error.userInfo)")
  }
}

您可以将警报视图的文本字段中的文本转换为Double,并使用它来更新当前领结rating属性。 最后,您可以像往常一样提交更改,方法是保存托管对象上下文并刷新UI以真实的查看更改。

试试看。 构建并运行应用程序,点击 Rate

img

输入05之间的任意小数,点击Save。 如您所料,分级标签将更新为您输入的新值。 现在再点击一次Rate。 还记得红色领结的永恒优雅吗? 假设你非常喜欢它,你决定给它打6分。 点击Save,刷新用户界面:

img

虽然你可能绝对喜欢红色,但现在既不是夸张的时候,也不是夸张的地方。 您的应用程序允许您将6保存为仅应最高为5的值。 你手上有无效数据。

核心数据中的数据校验

您的第一直觉可能是编写客户端验证-类似于“仅在值大于0且小于5时保存新评级” 幸运的是,您不必自己编写这些代码。 Core Data支持对大多数属性类型进行开箱即用的验证。

打开 BowTies.xcdatamodeld,选择 rating 属性,打开数据模型检查器。

img

Validation 旁边,键入 0 表示最小值,键入 5 表示最大值。 就是这样! 不需要写任何Swift来拒绝无效数据。

Note

通常情况下,如果你想在发布应用后更改数据模型,你必须对数据模型进行 version。你将在第6章“版本化和迁移”中了解更多。属性验证是少数例外之一。 如果您在交付后将其添加到应用,则无需对数据模型进行版本控制。 你真幸运!

但这到底是做什么的?

在托管对象上下文中调用save()后,验证将立即启动。 托管对象上下文检查模型,以查看是否有任何新值与您已设置的验证规则冲突。

如果存在验证错误,则保存失败。 还记得do-catch块中包装保存方法的NSError吗? 到目前为止,如果出现错误,除了将其记录到控制台之外,您没有理由做任何特别的事情。 验证改变了这一点。

再次构建并运行应用程序。 给予红色领结的评分为6分(满分5分)并保存。 一个相当隐晦的错误消息将溢出到您的控制台上:

Could not save Error Domain=NSCocoaErrorDomain Code=1610 "rating is too large." UserInfo={NSValidationErrorObject=<BowTie: 0x600002b8ab20> (entity: BowTie; id: 0xcef31f910384f2ad <x-coredata://A64812B6-5D4D-4934-805C-72F6A345EC7B/BowTie/p5>; data: {
    id = "800C3526-E83A-44AC-B718-D36934708921";
    isFavorite = 0;
    lastWorn = "2019-07-28 04:08:02 +0000";
    name = "Red Bow Tie";
    photoData = "{length = 50, bytes = 0x89504e47 0d0a1a0a 0000000d 49484452 ... aece1ce9 00000078 }";
    rating = 6;
    searchKey = R;
    timesWorn = 28;
    tintColor = "UIExtendedSRGBColorSpace 0.937255 0.188235 0.141176 1";
    url = "https://en.wikipedia.org/wiki/Bow_tie";
}), NSLocalizedDescription=rating is too large., NSValidationErrorKey=rating, NSValidationErrorValue=6}, ["NSValidationErrorKey": rating, "NSLocalizedDescription": rating is too large., "NSValidationErrorValue": 6, "NSValidationErrorObject": <BowTie: 0x600002b8ab20> (entity: BowTie; id: 0xcef31f910384f2ad <x-coredata://A64812B6-5D4D-4934-805C-72F6A345EC7B/BowTie/p5>; data: {
    id = "800C3526-E83A-44AC-B718-D36934708921";
    isFavorite = 0;
    lastWorn = "2019-07-28 04:08:02 +0000";
    name = "Red Bow Tie";
    photoData = "{length = 50, bytes = 0x89504e47 0d0a1a0a 0000000d 49484452 ... aece1ce9 00000078 }";
    rating = 6;
    searchKey = R;
    timesWorn = 28;
    tintColor = "UIExtendedSRGBColorSpace 0.937255 0.188235 0.141176 1";
    url = "https://en.wikipedia.org/wiki/Bow_tie";
}), "NSValidationErrorValue": 8, "NSValidationErrorKey": rating, "NSLocalizedDescription": rating is too large.]

错误附带的userInfo字典包含有关Core Data中止保存操作的原因的各种有用信息。 它甚至有一个本地化的错误消息,您可以在NSLocalizedDescription键下向用户显示:rating is too large.。

但是,如何处理这个错误完全取决于您。 打开 ViewController.swift,将update(rating:)替换为以下内容,正确处理错误:

func update(rating: String?) {

  guard let ratingString = rating,
    let rating = Double(ratingString) else {
      return
  }

  do {

    currentBowTie.rating = rating
    try managedContext.save()
    populate(bowtie: currentBowTie)

  } catch let error as NSError {

    if error.domain == NSCocoaErrorDomain &&
      (error.code == NSValidationNumberTooLargeError ||
        error.code == NSValidationNumberTooSmallError) {
      rate(rateButton)
    } else {
      print("Could not save \(error), \(error.userInfo)")
    }
  }
}

如果由于新评级太大或太小而出现错误,则再次显示警报视图。

否则,您将像以前一样使用新评级填充用户界面。

但是等等...NSValidationNumberTooLargeErrorNSValidationNumberTooSmallError从何而来? 回到前面的控制台阅读,仔细看第一行:

Could not save Error Domain=NSCocoaErrorDomain Code=1610 "rating is too large."

NSValidationNumberTooLargeError是映射到整数1610的错误代码。

有关Core Data错误和代码定义的完整列表,您可以通过单击NSValidationNumberTooLargeError查看Xcode中的 CoreDataErrors.h

Note

当涉及到NSError时,标准的做法是检查域和错误代码,以确定出了什么问题。 你可以在AppleError Handling Programming Guide中阅读更多关于这一点的信息。

构建并运行应用程序。通过再次向红领带展示一些爱来验证新的验证规则是否正常工作。

img

如果您输入任何高于5的值并尝试保存,应用程序将拒绝您的评级,并要求您使用新的警报视图重试。成功了!

把一切绑起来

WearRate按钮正常,但应用只能显示一条领带。 在分段控件上点击不同的值应该是为了切换关系。 您将通过实现该特性来完成这个示例项目。

每次用户点击分段控件时,它都会执行代码中的segmentedControl(_:)操作方法。 将segmentedControl(_:)的实现替换为以下内容:

@IBAction func segmentedControl(_ sender: UISegmentedControl) {
  guard let selectedValue = sender.titleForSegment(
    at: sender.selectedSegmentIndex) else {
      return
  }

  let request: NSFetchRequest<BowTie> = BowTie.fetchRequest()
  request.predicate = NSPredicate(
    format: "%K = %@",
    argumentArray: [#keyPath(BowTie.searchKey), selectedValue])

  do {
    let results = try managedContext.fetch(request)
    currentBowTie = results.first
    populate(bowtie: currentBowTie)
  } catch let error as NSError {
    print("Could not fetch \(error), \(error.userInfo)")
  }
}

分段控件中每个分段的标题对应于特定关系的searchKey属性。 抓取当前所选片段的标题,并使用精心制作的NSPerdicate获取适当的领结。

然后,使用结果数组中的第一个蝴蝶结(每个searchKey应该只有一个)来填充用户界面。

构建并运行应用程序。点击分段控件上的不同字母,享受迷幻享受。

img

你做到了! 有了这个领结应用程序在你的腰带,你很好地在你的方式成为一个核心数据的主人。

关键点

  • Core Data支持不同的 attribute data types,这决定了您可以在实体中存储的数据类型以及它们在磁盘上占用的空间。 常见的属性数据类型有StringDateDouble
  • Binary Data 属性数据类型允许您在数据模型中存储任意数量的二进制数据。
  • Transformable 属性数据类型允许您在数据模型中存储任何符合NSSecureCoding的对象。
  • 使用NSManagedObject subclass 是处理Core Data实体的更好方法。 您可以手动生成子类,也可以让Xcode自动生成。
  • 您可以使用NSPerdicateNSFetchRequest获取的集合实体进行 automatically
  • 大多数属性数据类型都可以直接在数据模型编辑器中设置 validation rules(如最大值、最小值)。 如果尝试保存无效数据,托管对象上下文将引发错误。