数据持久化方案解析(六) —— 基于Realm的持久化存储(二)

版本记录

版本号 时间
V1.0 2018.12.21 星期五

前言

数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
3. 数据持久化方案解析(三) —— 基于NSCoding的持久化存储(一)
4. 数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二)
5. 数据持久化方案解析(五) —— 基于Realm的持久化存储(一)

开始

首先看下写作环境

Swift 4.2, iOS 12, Xcode 10

Realm是一个跨平台的移动数据库解决方案,专为移动应用程序而设计,可以与iOS项目集成。 与Core Data的封装不同,Realm不依赖于Core Data甚至是SQLite后端。

本教程将向您介绍iOS上Realm的基本功能。 在本教程结束时,您将了解如何链接Realm框架,创建模型,执行查询和更新记录。

这是一个场景:你已经接受了国家公园管理局的实习生职位。 您的工作是记录在美国最大的国家公园中发现的物种。

您需要助理来记录您的发现,但由于该机构没有预算来雇用新的,您决定为自己创建一个虚拟助手:一个名为Agents Partner的应用程序。

在Xcode中打开启动项目。 目前,该应用程序仅包含使用MapKit的地图功能,该功能已在项目中设置。

启动项目缺少Realm,所以是时候添加它了。

注意:本教程是针对Realm 3.11.1编写的。

安装Realm的有效方法是使用CocoaPods

在starter项目的根目录中,创建一个名为Podfile的新文件。 复制以下文本并将其粘贴到新创建的文件中:

platform :ios, '12.0'
use_frameworks!

target 'Agents Partner' do
  pod 'RealmSwift'
end

保存并关闭文件。

在终端和项目的根目录中,运行以下命令:

pod install

这告诉CocoaPods扫描你的Podfile并安装你在文件中列出的任何pod。 很简约!

Realm可能需要安装一会,所以请密切关注您的终端。 一旦完成,您将看到底部附近的一条线,Pod installation complete!

在Finder中,打开starter项目的根目录。 请注意CocoaPods添加的文件夹以及Agents Partner.xcworkspace

如果您在Xcode中打开了起始项目,请立即关闭它并通过双击该文件打开.xcworkspace。 当您想要处理项目时,需要打开此文件。

您只需使用CocoaPods设置Realm。 构建并运行项目以确保编译所有内容。 如果一切按预期进行,你会看到:


Concepts and Classes Overview - 概念和类概览

为了更好地理解Realm的作用,下面是一些关于您将在本教程中使用的类的概念和信息:

  • Realm:Realm实例是框架的核心。它是您的基础数据库的访问点,如Core Data managed object context。您可以使用Realm()初始值设定项创建实例。
  • Object:这是您的Realm模型。创建模型的行为定义了数据库的schema。要创建模型,请将Object子类化并定义要作为属性存储的字段。
  • Relationships:通过声明要引用的Object类型的属性,可以在对象之间创建一对多关系。您可以通过List类型的属性创建多对一和多对多关系。
  • Write Transactions:数据库中的任何操作,如创建,编辑或删除对象,都必须通过在Realm实例上调用write(_ :)来在写入中执行。
  • Queries:要从数据库中检索对象,请使用查询。最简单的查询形式是在Realm实例上调用objects()并传入您正在寻找的Object的类。如果您的数据检索需求更加复杂,您可以使用谓词,链接查询并对结果进行排序。
  • ResultsResults是您从对象查询返回的自动更新容器类型。它们与常规数组Arrays有许多相似之处,包括下标语法。

通过对Realm的简要介绍,是时候让你构建项目的其余部分。


Your First Model

Models组打开Specimen.swift并添加以下实现:

import Foundation
import RealmSwift

class Specimen: Object {
  @objc dynamic var name = ""
  @objc dynamic var specimenDescription = ""
  @objc dynamic var latitude = 0.0
  @objc dynamic var longitude = 0.0
  @objc dynamic var created = Date()
}

上面的代码添加了一些属性:

namesamplesDescription存储样本的名称和描述。 Realm中的特定数据类型(如字符串)必须使用值初始化。 在这种情况下,您使用空字符串初始化它们。

latitudelongitude存储样本的坐标。 在这里,您将类型设置为Double并使用0.0初始化它们。

created存储样本的创建日期。 Date()返回当前日期,以便使用该值初始化该属性。

在Realm中创建第一个模型后,您是否准备好在一个小挑战中使用这些知识?

标本应分为不同的类别。 挑战在于自己创建一个Category模型。 将文件命名为Category.swift,并为新模型提供名为name的单个String属性。

如果您想检查您的工作,解决方案如下:

Category.swift看起来类似如下

import Foundation
import RealmSwift

class Category: Object {
  @objc dynamic var name = "" 
}

您有一个Category模型,您需要以某种方式与Specimen模型相关联。

回想一下上面的说明,声明您可以通过声明具有要链接的适当模型的属性来创建模型之间的关系。

打开Specimen.swift并在其他属性下面添加以下声明:

@objc dynamic var category: Category!

这在SpecimenCategory之间建立了一对多的关系。 这意味着每个Specimen只能属于一个Category,但每个Category可以有许多Specimens

您现在已经拥有了基本数据模型。 是时候将一些记录添加到您的数据库了!


Adding Records - 添加记录

当用户添加新样本时,他们可以输入样本名称并选择一个类别。 打开CategoriesTableViewController.swift。 此视图控制器在table view中显示类别列表,以便用户可以选择一个。

在开始编写代码以集成Realm之前,您需要导入RealmSwift框架。 将以下行添加到文件的顶部,在import UIKit下面:

import RealmSwift

您将使用一些默认categories填充此table view。 这些Category实例可以存储在Results的实例中。

CategoriesTableViewController现在有一个categories数组作为占位符。 在类定义的顶部找到以下代码:

var categories: [Any] = []

用以下内容替换该代码:

let realm = try! Realm()
lazy var categories: Results = { self.realm.objects(Category.self) }()

如果要获取对象,则始终可以定义所需的模型。 在上面的代码中,首先创建一个Realm实例,然后通过调用objects(_:)来填充categories,并传入所需模型类型的类名。

注意:为了简化本教程中所需的代码,您正在使用try!调用抛出错误的Realm方法时。 在您自己的代码中,您应该使用trydo / catch来捕获和处理错误。

您希望为用户提供一些默认类别,以便在应用首次运行时进行选择。

将以下辅助方法添加到类定义中:

private func populateDefaultCategories() {
  if categories.count == 0 { // 1
    try! realm.write() { // 2
      let defaultCategories =
        ["Birds", "Mammals", "Flora", "Reptiles", "Arachnids" ] // 3
      
      for category in defaultCategories { // 4
        let newCategory = Category()
        newCategory.name = category
        
        realm.add(newCategory)
      }
    }
    
    categories = realm.objects(Category.self) // 5
  }
}

以下是每个编号行的内容:

  • 1) 如果count等于0,则表示数据库没有Category记录。 这是第一次运行应用程序时的情况。
  • 2) 这将在realm上启动事务,您现在可以将一些记录添加到数据库中。
  • 3) 在这里,您创建默认类别名称列表,然后迭代它们。
  • 4) 对于每个类别名称,您可以创建一个新的Category实例,填充name并将该对象添加到realm
  • 5) 您获取您创建的所有类别并将其存储在categories中。

将以下行添加到viewDidLoad()的末尾:

populateDefaultCategories()

这会调用辅助方法在视图加载时填充测试类别。

现在您有了一些数据,您将更新table view数据源方法以显示类别。 查找tableView(_:cellForRowAt :)并在return cell之前添加以下内容:

let category = categories[indexPath.row]
cell.textLabel?.text = category.name

此实现基于index pathcategories中检索类别。 然后,它设置单元格的文本标签以显示类别的name

接下来,在您添加到CategoriesTableViewController的其他属性下面添加此属性:

var selectedCategory: Category!

您将使用此属性存储当前选定的Category

找到tableView(_:willSelectRowAtIndexPath :)并在return indexPath之前添加以下内容:

selectedCategory = categories[indexPath.row]

这会将用户的选择存储在您在上面声明的属性selectedCategory中。

构建并运行您的应用程序。

将地图缩放并平移到有趣的地方,并通过点击右上角的+按钮创建新的注释。 点击地图图钉将其选中,然后点击注释数据以编辑详细信息。 最后,点击Categorytext field以查看类别列表,如下所示:

您可以选择一个类别,但只将其保存到属性中,而不是保存在数据库中的任何其他位置。 很高兴看到应用程序中显示了类别,但是在数据库中查看记录总是令人放心。 您可以通过Realm Browser执行此操作。


Introducing the Realm Browser

Realm包含用于读取和编辑数据库的Realm Browser。 Realm数据库格式是专有的,不是人类可读的。

您可以在此处here下载Realm Browser


Working With Realm Browser

在开发应用程序时了解Realm数据库的存储位置非常重要 - 您可以使用一个巧妙的技巧来找出它的位置。

打开MapViewController.swift并将以下行添加到现有import语句下面的文件顶部:

import RealmSwift

在调用super.viewDidLoad()之后,将以下行添加到viewDidLoad()

print(Realm.Configuration.defaultConfiguration.fileURL!)

此行将数据库位置打印到调试控制台。 这是以后使用Realm Browser浏览数据库的一步。

构建并运行您的应用程序,您将看到它在Xcode控制台中报告数据库的位置。

转到数据库位置的最简单方法是打开Finder,按Shift-Command-G并粘贴应用报告的路径。

在Finder中打开文件夹后,您可能会看到一个或两个文件。 其中一个是default.realm,它是您的数据库文件。 第二个文件可能存在也可能不存在,是default.realm.lock。 锁定文件可防止在使用数据库时修改其他应用程序。

如果您尚未下载Realm Browser,请从App Store下载。 双击default.realm以使用Realm Browser打开它:

Realm Browser中打开数据库后,您会看到Category旁边有一个5。 这意味着该类包含五个记录。 单击一个类以检查其中包含的各个字段。


Adding Categories

您现在可以设置逻辑来设置Specimencategory

打开AddNewEntryController.swift并在现有import语句下面导入RealmSwift framework

import RealmSwift

将以下属性添加到类中:

var selectedCategory: Category!

您将使用它来存储选定的Category

接下来,找到unwindFromCategories(segue :)并在其中添加以下实现:

if segue.identifier == "CategorySelectedSegue" {
  let categoriesController = segue.source as! CategoriesTableViewController
  selectedCategory = categoriesController.selectedCategory
  categoryTextField.text = selectedCategory.name
}

当用户从您在上一步中设置的CategoriesTableViewController中选择一个类别时,将调用unwindFromCategories(segue :)。 在这里,您检索所选类别,将其存储在selectedCategory中,并使用类别名称填写text field

您可以继续创建您的第一个Specimen


Adding Specimens

仍然在AddNewEntryController.swift中,再向该类添加一个属性:

var specimen: Specimen!

此属性存储新的specimen对象。

接下来,将此辅助方法添加到类中:

func addNewSpecimen() {
  let realm = try! Realm() // 1
    
  try! realm.write { // 2
    let newSpecimen = Specimen() // 3
      
    newSpecimen.name = nameTextField.text! // 4
    newSpecimen.category = selectedCategory
    newSpecimen.specimenDescription = descriptionTextField.text
    newSpecimen.latitude = selectedAnnotation.coordinate.latitude
    newSpecimen.longitude = selectedAnnotation.coordinate.longitude
      
    realm.add(newSpecimen) // 5
    specimen = newSpecimen // 6
  }
}

以下是上面代码的作用:

  • 1) 首先,像以前一样获取Realm实例。
  • 2) 启动写入事务以添加新的Specimen
  • 3) 创建一个新的Specimen实例。
  • 4) 分配Specimen值。 值来自用户界面中的输入文本字段,选定的类别和地图注释中的坐标。
  • 5) 将新Specimen添加到领域。
  • 6) 将新Specimen分配给Specimen属性。

您需要某种验证器来确保所有字段都填充在您的Specimen中。 存在AddNewEntryViewController中的validateFields()以检查Specimen名称和描述。 由于您已添加了为样本分配类别的功能,因此您也将检查该字段。

validateFields()中找到如下所示的行:

if nameTextField.text!.isEmpty || descriptionTextField.text!.isEmpty {

修改这一行,如下所示:

if 
  nameTextField.text!.isEmpty || 
  descriptionTextField.text!.isEmpty || 
  selectedCategory == nil {

这将验证是否已填充所有字段以及您是否已选择类别。

接下来,将以下方法添加到类中:

override func shouldPerformSegue(
  withIdentifier identifier: String, 
  sender: Any?
  ) -> Bool {
    if validateFields() {
      addNewSpecimen()
        
      return true
    } else {
      return false
    }
}

在上面的代码中,您调用方法来验证字段。 如果所有内容都已填写,则添加新样本并返回true;否则,你返回false

建立并运行。 点击+按钮创建一个新样本。 填写名称和说明,选择一个类别,然后点击Confirm将您的Specimen添加到数据库。

视图控制器dismiss,但似乎没有任何事情发生。 这是怎么回事?

您已将记录发布到您的realm,但您尚未使用新样本填充地图。


Retrieving Records

您已将样本添加到要在地图上显示的数据库中。

首先看一下Realm Browser中更新的数据库:

您将看到填充了其字段的单个样本,以及MKAnnotation中的纬度和经度。 您还可以看到标本类别的链接;这意味着您的一对多Category关系正在按预期工作。

单击Specimen记录中的Category以查看Category记录本身。

接下来,您将在应用程序中填充地图。

打开SpecimenAnnotation.swift并向该类添加属性:

var specimen: Specimen?

这保存了注释的Specimen

接下来,使用以下内容替换初始化程序:

init(
  coordinate: CLLocationCoordinate2D, 
  title: String, 
  subtitle: String, 
  specimen: Specimen? = nil
  ) {
    self.coordinate = coordinate
    self.title = title
    self.subtitle = subtitle
    self.specimen = specimen
}

这里的更改是添加一个传递Specimen的选项。 样本的默认值为nil,这意味着如果您愿意,可以省略该参数。 如果没有标本,应用程序的其余部分仍然可以使用前三个参数调用初始化程序。

打开MapViewController.swift并向该类添加一个新属性:

var specimens = try! Realm().objects(Specimen.self)

由于您希望在此属性中存储标本集合,因此请向Realm实例询问Specimen类型的所有对象。

现在,将以下方法添加到类中:

func populateMap() {
  mapView.removeAnnotations(mapView.annotations) // 1

  specimens = try! Realm().objects(Specimen.self) // 2

  // Create annotations for each one
  for specimen in specimens { // 3
    let coord = CLLocationCoordinate2D(
      latitude: specimen.latitude, 
      longitude: specimen.longitude);
    let specimenAnnotation = SpecimenAnnotation(
      coordinate: coord,
      title: specimen.name,
      subtitle: specimen.category.name,
      specimen: specimen)
    mapView.addAnnotation(specimenAnnotation) // 4
  }
}

下面进行细分:

  • 1) 清除地图上的所有现有注释以重新开始。
  • 2) 刷新您的specimens属性。
  • 3) 遍历specimens并使用样本的坐标以及其namecategory创建SpecimenAnnotation
  • 4) 将每个samplesAnnotation添加到MKMapView

你需要从某个地方调用这个方法。 找到viewDidLoad()并将此行添加到其实现的末尾:

populateMap()

这可确保地图在视图控制器加载时显示样本。

现在,您将更改注释以包含样本名称和类别。 找到unwindFromAddNewEntry(segue :)并用以下实现替换该方法:

@IBAction func unwindFromAddNewEntry(segue: UIStoryboardSegue) {
  let addNewEntryController = segue.source as! AddNewEntryViewController
  let addedSpecimen = addNewEntryController.specimen!
  let addedSpecimenCoordinate = CLLocationCoordinate2D(
    latitude: addedSpecimen.latitude,
    longitude: addedSpecimen.longitude)
    
  if let lastAnnotation = lastAnnotation {
    mapView.removeAnnotation(lastAnnotation)
  } else {
    for annotation in mapView.annotations {
      if let currentAnnotation = annotation as? SpecimenAnnotation {
        if currentAnnotation.coordinate.latitude == addedSpecimenCoordinate.latitude &&
          currentAnnotation.coordinate.longitude == addedSpecimenCoordinate.longitude {
            mapView.removeAnnotation(currentAnnotation)
            break
        }
      }
    }
  }
    
  let annotation = SpecimenAnnotation(
    coordinate: addedSpecimenCoordinate,
    title: addedSpecimen.name,
    subtitle: addedSpecimen.category.name,
    specimen: addedSpecimen)
    
  mapView.addAnnotation(annotation)
  lastAnnotation = nil;
}

一旦从AddNewEntryController返回并且有一个新的样本要添加到地图中,系统将调用此方法。 将新样本添加到地图时,会获得通用注释图标。 对于您的类别,您希望将该图标更改为特定于类别的图标。

在这里,您删除添加到地图的最后一个注释,并将其替换为显示样本名称和类别的注释。

建立并运行。 创建一些不同类别的新标本,并查看地图如何更新:

1. A Different View

您可能已经注意到地图视图左上角的Log按钮。 除了地图之外,该应用程序还有一个基于文本的table view,列出了所有注释,称为Log View。 接下来,您将使用一些数据填充此表视图。

打开LogViewController.swift并导入RealmSwift

import RealmSwift

然后,用以下内容替换specimens属性:

var specimens = try! Realm().objects(Specimen.self)
  .sorted(byKeyPath: "name", ascending: true)

在上面的代码中,您将占位符数组替换为包含SpecimensResults,就像在MapViewController中一样。 它们将按name排序。

接下来,在return cell之前将以下内容添加到tableView(_:cellForRowAt :)

let specimen = specimens[indexPath.row]

cell.titleLabel.text = specimen.name
cell.subtitleLabel.text = specimen.category.name

switch specimen.category.name {
case "Uncategorized":
  cell.iconImageView.image = UIImage(named: "IconUncategorized")
case "Reptiles":
  cell.iconImageView.image = UIImage(named: "IconReptile")
case "Flora":
  cell.iconImageView.image = UIImage(named: "IconFlora")
case "Birds":
  cell.iconImageView.image = UIImage(named: "IconBird")
case "Arachnid":
  cell.iconImageView.image = UIImage(named: "IconArachnid")
case "Mammals":
  cell.iconImageView.image = UIImage(named: "IconMammal")
default:
  cell.iconImageView.image = UIImage(named: "IconUncategorized")
}

此方法使用样本的namecategory填充单元格。

构建并运行您的应用程序。 点击Log,您将在table view中看到所有输入的样本,如下所示:

2. Fetches With Predicates

您希望自己的应用有一个方便的搜索功能。 您的starter项目包含一个UISearchController实例;您将添加一些特定于您的应用的修改,以使其与Realm一起使用。

LogViewController.swift中,将searchResults属性替换为以下内容:

var searchResults = try! Realm().objects(Specimen.self)

向类中添加这个方法

func filterResultsWithSearchString(searchString: String) {
  let predicate = NSPredicate(format: "name BEGINSWITH [c]%@", searchString) // 1
  let scopeIndex = searchController.searchBar.selectedScopeButtonIndex // 2
  let realm = try! Realm()
    
  switch scopeIndex {
  case 0:
    searchResults = realm.objects(Specimen.self)
      .filter(predicate).sorted(byKeyPath: "name", ascending: true) // 3
  case 1:
    searchResults = realm.objects(Specimen.self).filter(predicate)
      .sorted(byKeyPath: "created", ascending: true) // 4
  default:
    searchResults = realm.objects(Specimen.self).filter(predicate) // 5
  }
}

以下是上述函数的作用:

  • 1) 首先,创建一个谓词,用于搜索以searchString开头的nameBEGINSWITH之后的[c]表示不区分大小写的搜索。
  • 2) 然后,从搜索栏中获取对当前所选范围索引的引用。
  • 3) 如果选择了第一个分段按钮,则按名称升序对结果进行排序。
  • 4) 如果选择了第二个按钮,则按创建日期升序对结果进行排序。
  • 5) 如果未选择任何按钮,请不要对结果进行排序,按照从数据库返回的顺序进行排序。

现在,当用户与search field交互时,您实际上将执行过滤。 在updateSearchResults(for :)中,在方法的开头添加以下两行:

let searchString = searchController.searchBar.text!
filterResultsWithSearchString(searchString: searchString)

由于搜索结果table view调用相同的数据源方法,因此您需要对tableView(_:cellForRowAt :)进行一些小的更改,以处理主日志表视图和搜索结果。 在该方法中,找到分配给specimen的那一行:

let specimen = specimens[indexPath.row]

删除它并用以下内容替换它:

let specimen = searchController.isActive ?
  searchResults[indexPath.row] : specimens[indexPath.row]

此代码检查searchController是否处于活动状态。 如果是这样,它将从searchResults中检索样本。 如果没有,它会从specimens中取回样本。

接下来,当用户点击范围栏中的按钮时,您将添加一个函数来对返回的结果进行排序。

将以下实现添加到scopeChanged(sender :)

let scopeBar = sender as! UISegmentedControl
let realm = try! Realm()
  
switch scopeBar.selectedSegmentIndex {
case 1:
  specimens = realm.objects(Specimen.self)
    .sorted(byKeyPath: "created", ascending: true)
default:
  specimens = realm.objects(Specimen.self)
    .sorted(byKeyPath: "name", ascending: true)
}
  
tableView.reloadData()

在这里,您可以检查按下的范围按钮(A-ZDate Added)并进行排序。 默认情况下,列表将按name排序。

构建并运行您的应用程序。 尝试一些不同的搜索,看看你得到的结果。


Updating Records

您已经介绍了添加记录,但是当您想要更新记录时呢?

如果您点击LogViewController中的单元格,您将转到AddNewEntryViewController,但字段为空。 让用户编辑字段的第一步是显示现有数据。

打开AddNewEntryViewController.swift并将以下辅助方法添加到类中:

func fillTextFields() {
  nameTextField.text = specimen.name
  categoryTextField.text = specimen.category.name
  descriptionTextField.text = specimen.specimenDescription

  selectedCategory = specimen.category
}

该方法用样本数据填充用户界面。 请记住,到目前为止,AddNewEntryViewController仅用于新标本,因此这些字段始终为空。

接下来,将以下行添加到viewDidLoad()的末尾:

if let specimen = specimen {
  title = "Edit \(specimen.name)"
      
  fillTextFields()
} else {
  title = "Add New Specimen"
}

此代码设置导航标题以指示用户是否正在添加或更新样本。 如果它是现有样本,您还可以调用辅助方法来填充字段。

您需要一种方法来更新样本记录和用户的更改。 添加以下方法:

func updateSpecimen() {
  let realm = try! Realm()
    
  try! realm.write {
    specimen.name = nameTextField.text!
    specimen.category = selectedCategory
    specimen.specimenDescription = descriptionTextField.text
  }
}

像往常一样,该方法从获取Realm实例开始,然后其余部分封装在write()事务中。 在事务内部,您更新数据字段。

只需要六行代码来更新Specimen记录!

接下来,当用户点击Confirm时,您将调用上述方法。 找到shouldPerformSegue(withIdentifier:sender :)并将addNewSpecimen()的调用替换为以下内容:

if specimen != nil {
  updateSpecimen()
} else {
  addNewSpecimen()
}

这会调用您的方法在适当时更新数据。

打开LogViewController.swift并为prepare(for:sender :)添加以下实现:

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  if (segue.identifier == "Edit") {
    let controller = segue.destination as! AddNewEntryViewController
    var selectedSpecimen: Specimen!
    let indexPath = tableView.indexPathForSelectedRow
      
    if searchController.isActive {
      let searchResultsController =
        searchController.searchResultsController as! UITableViewController
      let indexPathSearch = searchResultsController.tableView.indexPathForSelectedRow
        
      selectedSpecimen = searchResults[indexPathSearch!.row]
    } else {
      selectedSpecimen = specimens[indexPath!.row]
    }
      
    controller.specimen = selectedSpecimen
  }
}

您将选定的样本传递给AddNewEntryController实例。 if / else的复杂性是因为取决于用户是否正在查看搜索结果而使所选样本不同。

构建并运行您的应用程序。 打开日志视图,然后点击Specimen。 您将看到填充了所有字段并准备编辑的详细信息。

后记

本篇主要讲述了基于Realm的持久化存储,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(数据持久化方案解析(六) —— 基于Realm的持久化存储(二))