版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.08.19 星期三 |
前言
数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说
plist
文件(属性列表)、preference
(偏好设置)、NSKeyedArchiver
(归档)、SQLite 3
、CoreData
,这里基本上我们都用过。这几种方案各有优缺点,其中,CoreData是苹果极力推荐我们使用的一种方式,我已经将它分离出去一个专题进行说明讲解。这个专题主要就是针对另外几种数据持久化存储方案而设立。
1. 数据持久化方案解析(一) —— 一个简单的基于SQLite持久化方案示例(一)
2. 数据持久化方案解析(二) —— 一个简单的基于SQLite持久化方案示例(二)
3. 数据持久化方案解析(三) —— 基于NSCoding的持久化存储(一)
4. 数据持久化方案解析(四) —— 基于NSCoding的持久化存储(二)
5. 数据持久化方案解析(五) —— 基于Realm的持久化存储(一)
6. 数据持久化方案解析(六) —— 基于Realm的持久化存储(二)
7. 数据持久化方案解析(七) —— 基于Realm的持久化存储(三)
8. 数据持久化方案解析(八) —— UIDocument的数据存储(一)
9. 数据持久化方案解析(九) —— UIDocument的数据存储(二)
10. 数据持久化方案解析(十) —— UIDocument的数据存储(三)
11. 数据持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的数据存储示例(一)
12. 数据持久化方案解析(十二) —— 基于Core Data 和 SwiftUI的数据存储示例(二)
13. 数据持久化方案解析(十三) —— 基于Unit Testing的Core Data测试(一)
源码
1. Swift
首先看下工程组织结构
接着看一下sb中的内容
下面就是源码了
1. PandemicReport+CoreDataProperties.swift
import Foundation
import CoreData
extension PandemicReport {
@nonobjc
public class func fetchRequest() -> NSFetchRequest {
return NSFetchRequest(entityName: "PandemicReport")
}
@NSManaged public var id: UUID?
@NSManaged public var location: String?
@NSManaged public var numberTested: Int32
@NSManaged public var numberPositive: Int32
@NSManaged public var numberNegative: Int32
@NSManaged public var dateReported: Date?
}
2. PandemicReport+CoreDataClass.swift
import Foundation
import CoreData
@objc(PandemicReport)
public class PandemicReport: NSManagedObject {
}
3. CoreDataStack.swift
import Foundation
import CoreData
open class CoreDataStack {
public static let modelName = "PandemicReport"
public static let model: NSManagedObjectModel = {
// swiftlint:disable force_unwrapping
let modelURL = Bundle.main.url(forResource: modelName, withExtension: "momd")!
return NSManagedObjectModel(contentsOf: modelURL)!
}()
// swiftlint:enable force_unwrapping
public init() {
}
public lazy var mainContext: NSManagedObjectContext = {
return storeContainer.viewContext
}()
public lazy var storeContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: CoreDataStack.modelName, managedObjectModel: CoreDataStack.model)
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
return container
}()
public func newDerivedContext() -> NSManagedObjectContext {
let context = storeContainer.newBackgroundContext()
return context
}
public func saveContext() {
saveContext(mainContext)
}
public func saveContext(_ context: NSManagedObjectContext) {
if context != mainContext {
saveDerivedContext(context)
return
}
context.perform {
do {
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
}
public func saveDerivedContext(_ context: NSManagedObjectContext) {
context.perform {
do {
try context.save()
} catch let error as NSError {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
self.saveContext(self.mainContext)
}
}
}
4. ReportService.swift
import Foundation
import CoreData
public final class ReportService {
// MARK: - Properties
let managedObjectContext: NSManagedObjectContext
let coreDataStack: CoreDataStack
// MARK: - Initializers
public init(managedObjectContext: NSManagedObjectContext, coreDataStack: CoreDataStack) {
self.managedObjectContext = managedObjectContext
self.coreDataStack = coreDataStack
}
}
// MARK: - Public
extension ReportService {
@discardableResult
public func add(_ location: String, numberTested: Int32, numberPositive: Int32, numberNegative: Int32) -> PandemicReport {
let report = PandemicReport(context: managedObjectContext)
report.id = UUID()
report.dateReported = Date()
report.numberTested = numberTested
report.numberNegative = numberNegative
report.numberPositive = numberPositive
report.location = location
coreDataStack.saveContext(managedObjectContext)
return report
}
public func getReports() -> [PandemicReport]? {
let reportFetch: NSFetchRequest = PandemicReport.fetchRequest()
do {
let results = try managedObjectContext.fetch(reportFetch)
return results
} catch let error as NSError {
print("Fetch error: \(error) description: \(error.userInfo)")
}
return nil
}
@discardableResult
public func update(_ report: PandemicReport) -> PandemicReport {
coreDataStack.saveContext(managedObjectContext)
return report
}
public func delete(_ report: PandemicReport) {
managedObjectContext.delete(report)
coreDataStack.saveContext(managedObjectContext)
}
}
5. ViewController.swift
import UIKit
import CoreData
class ViewController: UIViewController {
// MARK: - Properties
@IBOutlet private weak var tableView: UITableView!
private lazy var coreDataStack = CoreDataStack()
private lazy var reportService = ReportService(
managedObjectContext: coreDataStack.mainContext,
coreDataStack: coreDataStack)
private var reports: [PandemicReport]?
private let segueIdentifier = "showDetail"
private let cellIdentifier = "Cell"
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
reports = reportService.getReports()
tableView.reloadData()
}
// MARK: - Navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard
segue.identifier == segueIdentifier,
let navigationController = segue.destination as? UINavigationController,
let controller = navigationController.topViewController as? ReportDetailsTableViewController
else {
return
}
navigationController.modalPresentationStyle = .fullScreen
controller.reportService = reportService
if let indexPath = tableView.indexPathForSelectedRow, let existingReport = reports?[indexPath.row] {
controller.report = existingReport
}
}
// MARK: - Actions
@IBAction func add(_ sender: Any) {
performSegue(withIdentifier: segueIdentifier, sender: nil)
}
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: segueIdentifier, sender: nil)
}
}
// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return reports?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath)
guard let report = reports?[indexPath.row] else {
return cell
}
cell.textLabel?.text = report.location
return cell
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard
let report = reports?[indexPath.row],
editingStyle == .delete
else {
return
}
reports?.remove(at: indexPath.row)
reportService.delete(report)
tableView.deleteRows(at: [indexPath], with: .automatic)
}
}
6. ReportDetailsTableViewController.swift
import UIKit
class ReportDetailsTableViewController: UITableViewController {
// MARK: - Properties
var report: PandemicReport?
var reportService: ReportService?
@IBOutlet weak var locationTextField: UITextField!
@IBOutlet weak var numberTestedTextField: UITextField!
@IBOutlet weak var numberPositiveTextField: UITextField!
@IBOutlet weak var numberNegativeTextField: UITextField!
@IBOutlet weak var dateReportedLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// disables cell highlighting
tableView.allowsSelection = false
let formatter = DateFormatter()
formatter.dateStyle = .short
// Display values of selected report
if let report = report {
locationTextField.text = report.location
numberTestedTextField.text = "\(report.numberTested)"
numberPositiveTextField.text = "\(report.numberPositive)"
numberNegativeTextField.text = "\(report.numberNegative)"
dateReportedLabel.text = formatter.string(from: report.dateReported ?? Date())
} else {
dateReportedLabel.text = formatter.string(from: Date())
}
}
// MARK: - Actions
@IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
@IBAction func save(_ sender: Any) {
let location = locationTextField.text ?? ""
let numberTested = Int32(numberTestedTextField.text ?? "") ?? 0
let numberPositive = Int32(numberPositiveTextField.text ?? "") ?? 0
let numberNegative = Int32(numberNegativeTextField.text ?? "") ?? 0
if let report = report {
report.location = location
report.numberTested = numberTested
report.numberPositive = numberPositive
report.numberNegative = numberNegative
reportService?.update(report)
dismiss(animated: true, completion: nil)
} else {
reportService?.add(
location,
numberTested: numberTested,
numberPositive: numberPositive,
numberNegative: numberNegative)
dismiss(animated: true, completion: nil)
}
}
}
7. TestCoreDataStack.swift
import Foundation
import CoreData
import PandemicReport
class TestCoreDataStack: CoreDataStack {
override init() {
super.init()
let persistentStoreDescription = NSPersistentStoreDescription()
persistentStoreDescription.type = NSInMemoryStoreType
let container = NSPersistentContainer(
name: CoreDataStack.modelName,
managedObjectModel: CoreDataStack.model)
container.persistentStoreDescriptions = [persistentStoreDescription]
container.loadPersistentStores { _, error in
if let error = error as NSError? {
fatalError("Unresolved error \(error), \(error.userInfo)")
}
}
storeContainer = container
}
}
8. ReportServiceTests.swift
import XCTest
@testable import PandemicReport
import CoreData
class ReportServiceTests: XCTestCase {
// MARK: - Properties
// swiftlint:disable implicitly_unwrapped_optional
var reportService: ReportService!
var coreDataStack: CoreDataStack!
// swiftlint:enable implicitly_unwrapped_optional
override func setUp() {
super.setUp()
coreDataStack = TestCoreDataStack()
reportService = ReportService(managedObjectContext: coreDataStack.mainContext, coreDataStack: coreDataStack)
}
override func tearDown() {
super.tearDown()
reportService = nil
coreDataStack = nil
}
func testAddReport() {
let report = reportService.add("Death Star", numberTested: 1000, numberPositive: 999, numberNegative: 1)
XCTAssertNotNil(report, "Report should not be nil")
XCTAssertTrue(report.location == "Death Star")
XCTAssertTrue(report.numberTested == 1000)
XCTAssertTrue(report.numberPositive == 999)
XCTAssertTrue(report.numberNegative == 1)
XCTAssertNotNil(report.id, "id should not be nil")
XCTAssertNotNil(report.dateReported, "dateReported should not be nil")
}
func testRootContextIsSavedAfterAddingReport() {
let derivedContext = coreDataStack.newDerivedContext()
reportService = ReportService(managedObjectContext: derivedContext, coreDataStack: coreDataStack)
expectation(
forNotification: .NSManagedObjectContextDidSave,
object: coreDataStack.mainContext) { _ in
return true
}
derivedContext.perform {
let report = self.reportService.add("Death Star 2", numberTested: 600, numberPositive: 599, numberNegative: 1)
XCTAssertNotNil(report)
}
waitForExpectations(timeout: 2.0) { error in
XCTAssertNil(error, "Save did not occur")
}
}
func testGetReports() {
let newReport = reportService.add("Endor", numberTested: 30, numberPositive: 20, numberNegative: 10)
let getReports = reportService.getReports()
XCTAssertNotNil(getReports)
XCTAssertTrue(getReports?.count == 1)
XCTAssertTrue(newReport.id == getReports?.first?.id)
}
func testUpdateReport() {
let newReport = reportService.add("Snow Planet", numberTested: 0, numberPositive: 0, numberNegative: 0)
newReport.numberTested = 30
newReport.numberPositive = 10
newReport.numberNegative = 20
newReport.location = "Hoth"
let updatedReport = reportService.update(newReport)
XCTAssertTrue(newReport.id == updatedReport.id)
XCTAssertTrue(updatedReport.numberTested == 30)
XCTAssertTrue(updatedReport.numberPositive == 10)
XCTAssertTrue(updatedReport.numberNegative == 20)
XCTAssertTrue(updatedReport.location == "Hoth")
}
func testDeleteReport() {
let newReport = reportService.add("Starkiller Base", numberTested: 100, numberPositive: 80, numberNegative: 20)
var fetchReports = reportService.getReports()
XCTAssertTrue(fetchReports?.count == 1)
XCTAssertTrue(newReport.id == fetchReports?.first?.id)
reportService.delete(newReport)
fetchReports = reportService.getReports()
XCTAssertTrue(fetchReports?.isEmpty ?? false)
}
}
后记
本篇主要讲述了基于
Unit Testing
的Core Data
测试,感兴趣的给个赞或者关注~~~