版本記錄
版本號(hào) | 時(shí)間 |
---|---|
V1.0 | 2020.08.19 星期三 |
前言
數(shù)據(jù)的持久化存儲(chǔ)是移動(dòng)端不可避免的一個(gè)問題扶歪,很多時(shí)候的業(yè)務(wù)邏輯都需要我們進(jìn)行本地化存儲(chǔ)解決和完成,我們可以采用很多持久化存儲(chǔ)方案扬舒,比如說
plist
文件(屬性列表)、preference
(偏好設(shè)置)、NSKeyedArchiver
(歸檔)宝恶、SQLite 3
乘综、CoreData
憎账,這里基本上我們都用過。這幾種方案各有優(yōu)缺點(diǎn)卡辰,其中胞皱,CoreData是蘋果極力推薦我們使用的一種方式邪意,我已經(jīng)將它分離出去一個(gè)專題進(jìn)行說明講解。這個(gè)專題主要就是針對(duì)另外幾種數(shù)據(jù)持久化存儲(chǔ)方案而設(shè)立反砌。
1. 數(shù)據(jù)持久化方案解析(一) —— 一個(gè)簡(jiǎn)單的基于SQLite持久化方案示例(一)
2. 數(shù)據(jù)持久化方案解析(二) —— 一個(gè)簡(jiǎn)單的基于SQLite持久化方案示例(二)
3. 數(shù)據(jù)持久化方案解析(三) —— 基于NSCoding的持久化存儲(chǔ)(一)
4. 數(shù)據(jù)持久化方案解析(四) —— 基于NSCoding的持久化存儲(chǔ)(二)
5. 數(shù)據(jù)持久化方案解析(五) —— 基于Realm的持久化存儲(chǔ)(一)
6. 數(shù)據(jù)持久化方案解析(六) —— 基于Realm的持久化存儲(chǔ)(二)
7. 數(shù)據(jù)持久化方案解析(七) —— 基于Realm的持久化存儲(chǔ)(三)
8. 數(shù)據(jù)持久化方案解析(八) —— UIDocument的數(shù)據(jù)存儲(chǔ)(一)
9. 數(shù)據(jù)持久化方案解析(九) —— UIDocument的數(shù)據(jù)存儲(chǔ)(二)
10. 數(shù)據(jù)持久化方案解析(十) —— UIDocument的數(shù)據(jù)存儲(chǔ)(三)
11. 數(shù)據(jù)持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲(chǔ)示例(一)
12. 數(shù)據(jù)持久化方案解析(十二) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲(chǔ)示例(二)
13. 數(shù)據(jù)持久化方案解析(十三) —— 基于Unit Testing的Core Data測(cè)試(一)
源碼
1. Swift
首先看下工程組織結(jié)構(gòu)
接著看一下sb中的內(nèi)容
下面就是源碼了
1. PandemicReport+CoreDataProperties.swift
import Foundation
import CoreData
extension PandemicReport {
@nonobjc
public class func fetchRequest() -> NSFetchRequest<PandemicReport> {
return NSFetchRequest<PandemicReport>(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> = 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
測(cè)試雾鬼,感興趣的給個(gè)贊或者關(guān)注~~~