版本記錄
| 版本號 | 時間 |
|---|---|
| V1.0 | 2020.12.10 星期四 |
前言
數(shù)據(jù)的持久化存儲是移動端不可避免的一個問題,很多時候的業(yè)務(wù)邏輯都需要我們進行本地化存儲解決和完成,我們可以采用很多持久化存儲方案,比如說
plist文件(屬性列表)、preference(偏好設(shè)置)、NSKeyedArchiver(歸檔)、SQLite 3、CoreData,這里基本上我們都用過。這幾種方案各有優(yōu)缺點,其中,CoreData是蘋果極力推薦我們使用的一種方式,我已經(jīng)將它分離出去一個專題進行說明講解。這個專題主要就是針對另外幾種數(shù)據(jù)持久化存儲方案而設(shè)立。
1. 數(shù)據(jù)持久化方案解析(一) —— 一個簡單的基于SQLite持久化方案示例(一)
2. 數(shù)據(jù)持久化方案解析(二) —— 一個簡單的基于SQLite持久化方案示例(二)
3. 數(shù)據(jù)持久化方案解析(三) —— 基于NSCoding的持久化存儲(一)
4. 數(shù)據(jù)持久化方案解析(四) —— 基于NSCoding的持久化存儲(二)
5. 數(shù)據(jù)持久化方案解析(五) —— 基于Realm的持久化存儲(一)
6. 數(shù)據(jù)持久化方案解析(六) —— 基于Realm的持久化存儲(二)
7. 數(shù)據(jù)持久化方案解析(七) —— 基于Realm的持久化存儲(三)
8. 數(shù)據(jù)持久化方案解析(八) —— UIDocument的數(shù)據(jù)存儲(一)
9. 數(shù)據(jù)持久化方案解析(九) —— UIDocument的數(shù)據(jù)存儲(二)
10. 數(shù)據(jù)持久化方案解析(十) —— UIDocument的數(shù)據(jù)存儲(三)
11. 數(shù)據(jù)持久化方案解析(十一) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲示例(一)
12. 數(shù)據(jù)持久化方案解析(十二) —— 基于Core Data 和 SwiftUI的數(shù)據(jù)存儲示例(二)
13. 數(shù)據(jù)持久化方案解析(十三) —— 基于Unit Testing的Core Data測試(一)
14. 數(shù)據(jù)持久化方案解析(十四) —— 基于Unit Testing的Core Data測試(二)
15. 數(shù)據(jù)持久化方案解析(十五) —— 基于Realm和SwiftUI的數(shù)據(jù)持久化簡單示例(一)
16. 數(shù)據(jù)持久化方案解析(十六) —— 基于Realm和SwiftUI的數(shù)據(jù)持久化簡單示例(二)
17. 數(shù)據(jù)持久化方案解析(十七) —— 基于NSPersistentCloudKitContainer的Core Data和CloudKit的集成示例(一)
18. 數(shù)據(jù)持久化方案解析(十八) —— 基于NSPersistentCloudKitContainer的Core Data和CloudKit的集成示例(二)
19. 數(shù)據(jù)持久化方案解析(十九) —— 基于批插入和存儲歷史等高效CoreData使用示例(一)
源碼
1. Swift
首先看下工程組織結(jié)構(gòu):

下面就是源碼啦
1. FireballWatchApp.swift
import SwiftUI
@main
struct FireballWatchApp: App {
@Environment(\.scenePhase) private var scenePhase
let persistenceController = PersistenceController.shared
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistenceController.viewContext)
.environmentObject(persistenceController)
}
.onChange(of: scenePhase) { phase in
switch phase {
case .background:
persistenceController.saveViewContext()
default:
break
}
}
}
}
2. ContentView.swift
import SwiftUI
import CoreData
import os.log
struct ContentView: View {
@EnvironmentObject private var persistence: PersistenceController
@Environment(\.managedObjectContext) private var viewContext
var body: some View {
TabView {
FireballList().tabItem {
VStack {
Image(systemName: "sun.max.fill")
Text("Fireballs")
}
}
.tag(1)
FireballGroupList().tabItem {
VStack {
Image(systemName: "tray.full.fill")
Text("Groups")
}
}
.tag(2)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.managedObjectContext, PersistenceController.preview.viewContext)
.environmentObject(PersistenceController.preview)
}
}
3. FireballList.swift
import SwiftUI
import CoreData
struct FireballList: View {
static var fetchRequest: NSFetchRequest<Fireball> {
let request: NSFetchRequest<Fireball> = Fireball.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \Fireball.dateTimeStamp, ascending: true)]
return request
}
@EnvironmentObject private var persistence: PersistenceController
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
fetchRequest: FireballList.fetchRequest,
animation: .default)
private var fireballs: FetchedResults<Fireball>
var body: some View {
NavigationView {
List {
ForEach(fireballs, id: \.dateTimeStamp) { fireball in
NavigationLink(destination: FireballDetailsView(fireball: fireball)) {
FireballRow(fireball: fireball)
}
}
.onDelete(perform: deleteObjects)
}
.navigationBarTitle(Text("Fireballs"))
.navigationBarItems(trailing:
// swiftlint:disable:next multiple_closures_with_trailing_closure
Button(action: { persistence.fetchFireballs() }) {
Image(systemName: "arrow.2.circlepath")
}
)
}
}
private func deleteObjects(offsets: IndexSet) {
withAnimation {
persistence.deleteManagedObjects(offsets.map { fireballs[$0] })
}
}
}
struct FireballList_Previews: PreviewProvider {
static var previews: some View {
FireballList()
.environment(\.managedObjectContext, PersistenceController.preview.viewContext)
.environmentObject(PersistenceController.preview)
}
}
4. FireballRow.swift
import SwiftUI
struct FireballRow: View {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
let fireball: Fireball
var body: some View {
HStack(alignment: .center) {
FireballMagnitudeView(magnitude: fireball.impactEnergyMagnitude)
.frame(width: 50, height: 50, alignment: .center)
VStack(alignment: .leading, spacing: 8) {
fireball.dateTimeStamp.map { Text(Self.dateFormatter.string(from: $0)) }.font(.headline)
FireballCoordinateLabel(latitude: fireball.latitude, longitude: fireball.longitude, font: .subheadline)
HStack {
FireballVelocityLabel(velocity: fireball.velocity, font: .caption)
Spacer()
FireballAltitudeLabel(altitude: fireball.altitude, font: .caption)
}
}
}
}
}
struct EpisodeRow_Previews: PreviewProvider {
static var fireball: Fireball {
let controller = PersistenceController.preview
return controller.makeRandomFireball(context: controller.viewContext)
}
static var previews: some View {
FireballRow(fireball: fireball)
}
}
5. FireballCoordinateLabel.swift
import SwiftUI
struct FireballCoordinateLabel: View {
static let symbol = UnitAngle.degrees.symbol
let latitude: Double
let longitude: Double
let font: Font
private var latitudeString: String {
return String(format: "%.2f", abs(latitude)) +
(latitude < 0 ? "\(FireballCoordinateLabel.symbol)S" : "\(FireballCoordinateLabel.symbol)N")
}
private var longitudeString: String {
return String(format: "%.2f", abs(longitude)) +
(longitude < 0 ? "\(FireballCoordinateLabel.symbol)W" : "\(FireballCoordinateLabel.symbol)E")
}
var body: some View {
HStack {
Text("Coordinates: \(latitudeString) \(longitudeString)")
.font(font)
}
}
}
struct FireballCoordinateLabel_Previews: PreviewProvider {
static var previews: some View {
VStack {
FireballCoordinateLabel(
latitude: Double.random(in: 0...90),
longitude: Double.random(in: 0...180),
font: .subheadline)
FireballCoordinateLabel(
latitude: Double.random(in: -90...0),
longitude: Double.random(in: -180...0),
font: .subheadline)
}
}
}
6. FireballAltitudeLabel.swift
import SwiftUI
struct FireballAltitudeLabel: View {
let altitude: Double
let font: Font
var measurementFormatter: MeasurementFormatter {
let formatter = MeasurementFormatter()
formatter.unitStyle = .short
formatter.unitOptions = .providedUnit
return formatter
}
// swiftlint:disable:next identifier_name
var km: Measurement<UnitLength> {
return Measurement(value: altitude, unit: UnitLength.kilometers)
}
var body: some View {
Text("Altitude: \(measurementFormatter.string(from: km))")
.font(font)
}
}
struct FireballAltitudeLabel_Previews: PreviewProvider {
static var previews: some View {
FireballAltitudeLabel(altitude: 12.5, font: .caption)
}
}
7. FireballVelocityLabel.swift
import SwiftUI
struct FireballVelocityLabel: View {
let velocity: Double
let font: Font
var measurementFormatter: MeasurementFormatter {
let formatter = MeasurementFormatter()
formatter.unitStyle = .short
formatter.unitOptions = .providedUnit
return formatter
}
var mps: Measurement<UnitSpeed> {
// Data represents km/s so we need to multiply by 3600
return Measurement(value: velocity * 3600, unit: UnitSpeed.kilometersPerHour)
}
var body: some View {
Text("Velocity: \(measurementFormatter.string(from: mps))")
.font(font)
}
}
struct FireballVelocityLabel_Previews: PreviewProvider {
static var previews: some View {
FireballVelocityLabel(velocity: 12.5, font: .caption)
}
}
8. FireballImpactEnergyLabel.swift
import SwiftUI
struct FireballImpactEnergyLabel: View {
let energy: Double
let font: Font
var body: some View {
Text("Impact Energy: \(String(format: "%.2f", energy) ) kt")
.font(font)
}
}
struct FireballImpactEnergyLabel_Previews: PreviewProvider {
static var previews: some View {
FireballImpactEnergyLabel(energy: 0.71, font: .caption)
}
}
9. FireballMagnitudeView.swift
import SwiftUI
struct FireballMagnitudeView: View {
let magnitude: ImpactEnergyMagnitude
var color: Color {
Color(magnitude.color)
}
var size: CGSize {
switch magnitude {
case 1:
return CGSize(width: 15, height: 15)
case 2:
return CGSize(width: 20, height: 20)
case 3:
return CGSize(width: 25, height: 25)
case 4:
return CGSize(width: 30, height: 30)
case 5:
return CGSize(width: 35, height: 35)
case 6:
return CGSize(width: 40, height: 40)
case 7:
return CGSize(width: 45, height: 45)
case 8:
return CGSize(width: 50, height: 50)
default:
return CGSize(width: 10, height: 10)
}
}
var body: some View {
Circle()
.fill(color)
.frame(width: size.width, height: size.height)
}
}
struct FireballMagnitudeView_Previews: PreviewProvider {
static var previews: some View {
FireballMagnitudeView(magnitude: Int.random(in: 0...8))
}
}
10. FireballDetailsView.swift
import SwiftUI
import MapKit
struct FireballDetailsView: View {
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .long
return formatter
}()
@EnvironmentObject private var persistence: PersistenceController
let fireball: Fireball
var mapRegion: MKCoordinateRegion {
let coordinates = CLLocationCoordinate2D(latitude: fireball.latitude, longitude: fireball.longitude)
let span = MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100)
return MKCoordinateRegion(center: coordinates, span: span)
}
var mapAnnotation: FireballAnnotation {
return FireballAnnotation(
coordinates: mapRegion.center,
color: fireball.impactEnergyMagnitude.color)
}
@State var groupPickerIsPresented = false
var body: some View {
VStack {
HStack {
VStack(alignment: .leading, spacing: 8) {
fireball.dateTimeStamp.map { Text(Self.dateFormatter.string(from: $0)) }.font(.headline)
FireballCoordinateLabel(latitude: fireball.latitude, longitude: fireball.longitude, font: .body)
FireballImpactEnergyLabel(energy: fireball.impactEnergy, font: .body)
FireballVelocityLabel(velocity: fireball.velocity, font: .body)
FireballAltitudeLabel(altitude: fireball.altitude, font: .body)
}
Spacer()
FireballMagnitudeView(magnitude: fireball.impactEnergyMagnitude)
.frame(width: 100, height: 100)
}
.padding()
FireballMapView(mapRegion: mapRegion, annotations: [mapAnnotation])
}
.sheet(isPresented: $groupPickerIsPresented) {
SelectFireballGroupView(selectedGroups: (fireball.groups as? Set<FireballGroup>) ?? []) {
setGroups($0)
groupPickerIsPresented = false
}
.environment(\.managedObjectContext, persistence.viewContext)
}
.navigationBarTitle(Text("Fireball Details"))
.navigationBarItems(trailing:
// swiftlint:disable:next multiple_closures_with_trailing_closure
Button(action: { groupPickerIsPresented.toggle() }) {
Image(systemName: "tray.and.arrow.down.fill")
}
)
}
private func setGroups(_ groups: Set<FireballGroup>) {
fireball.groups = groups as NSSet
persistence.saveViewContext()
}
}
struct FireballDetailsView_Previews: PreviewProvider {
static var fireball: Fireball {
let controller = PersistenceController.preview
return controller.makeRandomFireball(context: controller.viewContext)
}
static var previews: some View {
FireballDetailsView(fireball: fireball)
}
}
11. FireballMapView.swift
import SwiftUI
import MapKit
struct FireballAnnotation: Identifiable {
let id = UUID()
let coordinates: CLLocationCoordinate2D
let color: UIColor
}
struct FireballMapView: View {
@State var mapRegion: MKCoordinateRegion
let annotations: [FireballAnnotation]
var body: some View {
Map(coordinateRegion: $mapRegion, annotationItems: annotations) { annotation in
MapMarker(coordinate: annotation.coordinates, tint: Color(annotation.color))
}
}
}
struct FireballDetailsMapView_Previews: PreviewProvider {
static let coordinates = CLLocationCoordinate2D(latitude: -32, longitude: 115)
static let span = MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100)
static var previews: some View {
FireballMapView(
mapRegion: MKCoordinateRegion(
center: coordinates,
span: span),
annotations: [FireballAnnotation(coordinates: coordinates, color: UIColor.orange)]
)
}
}
12. SelectFireballGroupRow.swift
import SwiftUI
struct SelectFireballGroupRow: View {
var group: FireballGroup
@Binding var selection: Set<FireballGroup>
var isSelected: Bool {
selection.contains(group)
}
var body: some View {
HStack {
group.name.map(Text.init)
Spacer()
if isSelected {
Image(systemName: "checkmark")
}
}
.onTapGesture {
if isSelected {
selection.remove(group)
} else {
selection.insert(group)
}
}
}
}
struct SelectFireballGroupRow_Previews: PreviewProvider {
static var group: FireballGroup = {
let controller = PersistenceController.preview
return controller.makeRandomFireballGroup(context: controller.viewContext)
}()
@State static var selection: Set<FireballGroup> = [group]
static var previews: some View {
SelectFireballGroupRow(group: group, selection: $selection)
}
}
13. SelectFireballGroupView.swift
import SwiftUI
import CoreData
struct SelectFireballGroupView: View {
static var fetchRequest: NSFetchRequest<FireballGroup> {
let request: NSFetchRequest<FireballGroup> = FireballGroup.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \FireballGroup.name, ascending: true)]
return request
}
@EnvironmentObject private var persistence: PersistenceController
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
fetchRequest: FireballGroupList.fetchRequest,
animation: .default)
private var groups: FetchedResults<FireballGroup>
@State var selectedGroups: Set<FireballGroup>
let onComplete: (Set<FireballGroup>) -> Void
var body: some View {
NavigationView {
List(groups, id: \.id, selection: $selectedGroups) { group in
SelectFireballGroupRow(group: group, selection: $selectedGroups)
}
.navigationTitle(Text("Select Groups"))
.navigationBarItems(trailing: Button("Done") {
formAction()
})
}
}
private func formAction() {
onComplete(selectedGroups)
}
}
struct SelectFireballGroupView_Previews: PreviewProvider {
static var previews: some View {
SelectFireballGroupView(selectedGroups: []) { _ in }
.environment(\.managedObjectContext, PersistenceController.preview.viewContext)
.environmentObject(PersistenceController.preview)
}
}
14. FireballGroupList.swift
import SwiftUI
import CoreData
struct FireballGroupList: View {
static var fetchRequest: NSFetchRequest<FireballGroup> {
let request: NSFetchRequest<FireballGroup> = FireballGroup.fetchRequest()
request.sortDescriptors = [NSSortDescriptor(keyPath: \FireballGroup.name, ascending: true)]
return request
}
@EnvironmentObject private var persistence: PersistenceController
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
fetchRequest: FireballGroupList.fetchRequest,
animation: .default)
private var groups: FetchedResults<FireballGroup>
@State var addGroupIsPresented = false
var body: some View {
NavigationView {
List {
ForEach(groups, id: \.id) { group in
NavigationLink(destination: FireballGroupDetailsView(fireballGroup: group)) {
HStack {
Text("\(group.name ?? "Untitled")")
Spacer()
Image(systemName: "sun.max.fill")
Text("\(group.fireballCount)")
}
}
}
.onDelete(perform: deleteObjects)
}
.sheet(isPresented: $addGroupIsPresented) {
AddFireballGroup { name in
addNewGroup(name: name)
addGroupIsPresented = false
}
}
.navigationBarTitle(Text("Fireball Groups"))
.navigationBarItems(trailing:
// swiftlint:disable:next multiple_closures_with_trailing_closure
Button(action: { addGroupIsPresented.toggle() }) {
Image(systemName: "plus")
}
)
}
}
private func deleteObjects(offsets: IndexSet) {
withAnimation {
persistence.deleteManagedObjects(offsets.map { groups[$0] })
}
}
private func addNewGroup(name: String) {
withAnimation {
persistence.addNewFireballGroup(name: name)
}
}
}
struct GroupList_Previews: PreviewProvider {
static var previews: some View {
FireballGroupList()
.environment(\.managedObjectContext, PersistenceController.preview.viewContext)
.environmentObject(PersistenceController.preview)
}
}
15. AddFireballGroup.swift
import SwiftUI
struct AddFireballGroup: View {
@State var name = ""
let onComplete: (String) -> Void
var body: some View {
NavigationView {
Form {
Section(header: Text("Name")) {
TextField("Group name", text: $name)
}
Section {
Button(action: formAction) {
Text("Add New Group")
}
}
}
.navigationBarTitle(Text("New Fireball Group"))
}
}
private func formAction() {
onComplete(name.isEmpty ? "Untitled Group" : name)
}
}
struct AddFireballGroup_Previews: PreviewProvider {
static var previews: some View {
AddFireballGroup { _ in }
}
}
16. FireballGroupDetailsView.swift
import SwiftUI
import MapKit
struct FireballGroupDetailsView: View {
let fireballGroup: FireballGroup
var mapRegion: MKCoordinateRegion {
let span = MKCoordinateSpan(latitudeDelta: 100, longitudeDelta: 100)
guard let fireball = fireballGroup.fireballs?.anyObject() as? Fireball else {
return MKCoordinateRegion(center: CLLocationCoordinate2D(), span: span)
}
let coordinates = CLLocationCoordinate2D(latitude: fireball.latitude, longitude: fireball.longitude)
return MKCoordinateRegion(center: coordinates, span: span)
}
var mapAnnotations: [FireballAnnotation] {
guard let fireballs = fireballGroup.fireballs else {
return []
}
return fireballs.compactMap {
guard let fireball = $0 as? Fireball else {
return nil
}
return FireballAnnotation(
coordinates: CLLocationCoordinate2D(
latitude: fireball.latitude,
longitude: fireball.longitude),
color: fireball.impactEnergyMagnitude.color)
}
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Fireballs: \(fireballGroup.fireballCount)")
.padding()
FireballMapView(mapRegion: mapRegion, annotations: mapAnnotations)
}
.navigationBarTitle(Text(fireballGroup.name ?? "Fireball Group"))
}
}
struct FireballGroupDetails_Previews: PreviewProvider {
static var group: FireballGroup {
let controller = PersistenceController.preview
return controller.makeRandomFireballGroup(context: controller.viewContext)
}
static var previews: some View {
FireballGroupDetailsView(fireballGroup: group)
}
}
17. Persistence.swift
import CoreData
import Combine
import os.log
class PersistenceController: ObservableObject {
//
private static let authorName = "FireballWatch"
private static let remoteDataImportAuthorName = "Fireball Data Import"
static let shared = PersistenceController()
var viewContext: NSManagedObjectContext {
return container.viewContext
}
private let container: NSPersistentContainer
private var subscriptions: Set<AnyCancellable> = []
private lazy var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MMMM d, yyyy"
return formatter
}()
private lazy var historyRequestQueue = DispatchQueue(label: "history")
private var lastHistoryToken: NSPersistentHistoryToken?
private lazy var tokenFileURL: URL = {
let url = NSPersistentContainer.defaultDirectoryURL().appendingPathComponent("FireballWatch", isDirectory: true)
do {
try FileManager.default
.createDirectory(at: url, withIntermediateDirectories: true, attributes: nil)
} catch {
let nsError = error as NSError
os_log(
.error,
log: .default,
"Failed to create history token directory: %@",
nsError)
}
return url.appendingPathComponent("token.data", isDirectory: false)
}()
init(inMemory: Bool = false) {
container = NSPersistentContainer(name: "FireballWatch")
let persistentStoreDescription = container.persistentStoreDescriptions.first
if inMemory {
persistentStoreDescription?.url = URL(fileURLWithPath: "/dev/null")
}
persistentStoreDescription?.setOption(
true as NSNumber,
forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
persistentStoreDescription?.setOption(
true as NSNumber,
forKey: NSPersistentHistoryTrackingKey)
container.loadPersistentStores { _, error in
if let error = error as NSError? {
os_log(.error, log: .default, "Error loading persistent store %@", error)
}
}
viewContext.automaticallyMergesChangesFromParent = true
viewContext.mergePolicy = NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType
viewContext.transactionAuthor = PersistenceController.authorName
if !inMemory {
do {
try viewContext.setQueryGenerationFrom(.current)
} catch {
let nsError = error as NSError
os_log(
.error,
log: .default,
"Failed to pin viewContext to the current generation: %@",
nsError)
}
}
NotificationCenter.default
.publisher(for: .NSPersistentStoreRemoteChange)
.sink {
self.processRemoteStoreChange($0)
}
.store(in: &subscriptions)
loadHistoryToken()
}
func saveViewContext() {
guard viewContext.hasChanges else { return }
do {
try viewContext.save()
} catch {
let nsError = error as NSError
os_log(.error, log: .default, "Error saving changes %@", nsError)
}
}
func deleteManagedObjects(_ objects: [NSManagedObject]) {
viewContext.perform { [context = viewContext] in
objects.forEach(context.delete)
self.saveViewContext()
}
}
func addNewFireballGroup(name: String) {
viewContext.perform { [context = viewContext] in
let group = FireballGroup(context: context)
group.id = UUID()
group.name = name
self.saveViewContext()
}
}
// MARK: Fetch Remote Data
func fetchFireballs() {
let source = RemoteDataSource()
os_log(.info, log: .default, "Fetching fireballs...")
source.fireballDataPublisher
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in
os_log(.info, log: .default, "Fetching completed")
}, receiveValue: { [weak self] in
self?.batchInsertFireballs($0)
})
.store(in: &subscriptions)
}
// private func importFetchedFireballs(_ fireballs: [FireballData]) {
// os_log(.info, log: .default, "Importing \(fireballs.count) fireballs")
// container.performBackgroundTask { context in
// context.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
// fireballs.forEach {
// let managedObject = Fireball(context: context)
// managedObject.dateTimeStamp = $0.dateTimeStamp
// managedObject.radiatedEnergy = $0.radiatedEnergy
// managedObject.impactEnergy = $0.impactEnergy
// managedObject.latitude = $0.latitude
// managedObject.longitude = $0.longitude
// managedObject.altitude = $0.altitude
// managedObject.velocity = $0.velocity
//
// do {
// try context.save()
// } catch {
// let nsError = error as NSError
// os_log(.error, log: .default, "Error importing fireball %@", nsError)
// }
// }
// }
// }
private func batchInsertFireballs(_ fireballs: [FireballData]) {
guard !fireballs.isEmpty else { return }
os_log(
.info,
log: .default,
"Batch inserting \(fireballs.count) fireballs")
container.performBackgroundTask { context in
context.transactionAuthor = PersistenceController.remoteDataImportAuthorName
let batchInsert = self.newBatchInsertRequest(with: fireballs)
do {
try context.execute(batchInsert)
os_log(.info, log: .default, "Finished batch inserting \(fireballs.count) fireballs")
} catch {
let nsError = error as NSError
os_log(.error, log: .default, "Error batch inserting fireballs %@", nsError.userInfo)
}
}
}
private func newBatchInsertRequest(with fireballs: [FireballData]) -> NSBatchInsertRequest {
var index = 0
let total = fireballs.count
let batchInsert = NSBatchInsertRequest(
entity: Fireball.entity()) { (managedObject: NSManagedObject) -> Bool in
guard index < total else { return true }
if let fireball = managedObject as? Fireball {
let data = fireballs[index]
fireball.dateTimeStamp = data.dateTimeStamp
fireball.radiatedEnergy = data.radiatedEnergy
fireball.impactEnergy = data.impactEnergy
fireball.latitude = data.latitude
fireball.longitude = data.longitude
fireball.altitude = data.altitude
fireball.velocity = data.velocity
}
index += 1
return false
}
return batchInsert
}
// MARK: - History Management
private func loadHistoryToken() {
do {
let tokenData = try Data(contentsOf: tokenFileURL)
lastHistoryToken = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSPersistentHistoryToken.self, from: tokenData)
} catch {
let nsError = error as NSError
os_log(
.error,
log: .default,
"Failed to load history token data file: %@",
nsError)
}
}
private func storeHistoryToken(_ token: NSPersistentHistoryToken) {
do {
let data = try NSKeyedArchiver
.archivedData(withRootObject: token, requiringSecureCoding: true)
try data.write(to: tokenFileURL)
lastHistoryToken = token
} catch {
let nsError = error as NSError
os_log(
.error,
log: .default,
"Failed to write history token data file: %@",
nsError)
}
}
func processRemoteStoreChange(_ notification: Notification) {
historyRequestQueue.async {
let backgroundContext = self.container.newBackgroundContext()
backgroundContext.performAndWait {
let request = NSPersistentHistoryChangeRequest
.fetchHistory(after: self.lastHistoryToken)
if let historyFetchRequest = NSPersistentHistoryTransaction.fetchRequest {
historyFetchRequest.predicate =
NSPredicate(format: "%K != %@", "author", PersistenceController.authorName)
request.fetchRequest = historyFetchRequest
}
do {
let result = try backgroundContext.execute(request) as? NSPersistentHistoryResult
guard
let transactions = result?.result as? [NSPersistentHistoryTransaction],
!transactions.isEmpty
else {
return
}
// Update the viewContext with the changes
self.mergeChanges(from: transactions)
if let newToken = transactions.last?.token {
// Update the history token using the last transaction.
self.storeHistoryToken(newToken)
}
} catch {
let nsError = error as NSError
os_log(
.error,
log: .default,
"Persistent history request error: %@",
nsError)
}
}
}
}
private func mergeChanges(from transactions: [NSPersistentHistoryTransaction]) {
let context = viewContext
context.perform {
transactions.forEach { transaction in
guard let userInfo = transaction.objectIDNotification().userInfo else {
return
}
NSManagedObjectContext
.mergeChanges(fromRemoteContextSave: userInfo, into: [context])
}
}
}
}
extension PersistenceController {
static var preview: PersistenceController = {
let controller = PersistenceController(inMemory: true)
controller.viewContext.perform {
for i in 0..<100 {
controller.makeRandomFireball(context: controller.viewContext)
}
for i in 0..<5 {
controller.makeRandomFireballGroup(context: controller.viewContext)
}
}
return controller
}()
@discardableResult
func makeRandomFireball(context: NSManagedObjectContext) -> Fireball {
let fireball = Fireball(context: context)
let timeSpan = Date().timeIntervalSince1970
fireball.dateTimeStamp = Date(timeIntervalSince1970: Double.random(in: 0...timeSpan))
fireball.radiatedEnergy = Double.random(in: 0...3)
fireball.impactEnergy = Double.random(in: 0...400)
fireball.latitude = Double.random(in: -90...90)
fireball.longitude = Double.random(in: -180...180)
fireball.altitude = Double.random(in: 1...20)
fireball.velocity = Double.random(in: 200...2000)
return fireball
}
@discardableResult
func makeRandomFireballGroup(context: NSManagedObjectContext) -> FireballGroup {
let group = FireballGroup(context: context)
group.id = UUID()
group.name = "Random Group"
group.fireballs = [
makeRandomFireball(context: context),
makeRandomFireball(context: context),
makeRandomFireball(context: context)
]
return group
}
}
18. Fireball+Extensions.swift
import Foundation
import CoreData
import UIKit
typealias ImpactEnergyMagnitude = Int
extension ImpactEnergyMagnitude {
// a color to represent the magnitude of the impact energey
var color: UIColor {
switch self {
case 0:
return UIColor(hue: 0.6, saturation: 0.8, brightness: 0.64, alpha: 1)
case 1:
return UIColor(hue: 0.56, saturation: 0.9, brightness: 0.51, alpha: 1)
case 2:
return UIColor(hue: 0.52, saturation: 0.55, brightness: 0.63, alpha: 1)
case 3:
return UIColor(hue: 0.18, saturation: 0.43, brightness: 0.73, alpha: 1)
case 4:
return UIColor(hue: 0.11, saturation: 0.65, brightness: 0.93, alpha: 1)
case 5:
return UIColor(hue: 0.09, saturation: 0.67, brightness: 0.92, alpha: 1)
case 6:
return UIColor(hue: 0.05, saturation: 0.72, brightness: 0.88, alpha: 1)
case 7:
return UIColor(hue: 0.02, saturation: 0.78, brightness: 0.83, alpha: 1)
case 8:
return UIColor(hue: 0.01, saturation: 0.80, brightness: 0.81, alpha: 1)
default:
return UIColor.lightGray
}
}
}
extension Fireball {
// an internal scale from 0 to 8 to represent the scale of the impact energey
var impactEnergyMagnitude: ImpactEnergyMagnitude {
let logEnergy = log(impactEnergy)
switch logEnergy {
case (-1 ... -0.5):
return 1
case (-0.5 ... 0):
return 2
case 0...0.5:
return 3
case 0.5...1:
return 5
case 1...1.5:
return 5
case 1.5...2:
return 6
case 2...2.5:
return 7
case _ where logEnergy > 2.5:
return 8
default:
// where logEnergy < -1:
return 0
}
}
}
19. RemoteDataSource.swift
import Foundation
import Combine
import os.log
class RemoteDataSource {
static let endpoint = URL(string: "https://ssd-api.jpl.nasa.gov/fireball.api" )
private var subscriptions: Set<AnyCancellable> = []
private func dataTaskPublisher(for url: URL) -> AnyPublisher<Data, URLError> {
URLSession.shared.dataTaskPublisher(for: url)
.compactMap { data, response -> Data? in
guard let httpResponse = response as? HTTPURLResponse else {
os_log(.error, log: OSLog.default, "Data download had no http response")
return nil
}
guard httpResponse.statusCode == 200 else {
os_log(.error, log: OSLog.default, "Data download returned http status: %d", httpResponse.statusCode)
return nil
}
return data
}
.eraseToAnyPublisher()
}
var fireballDataPublisher: AnyPublisher<[FireballData], URLError> {
guard let endpoint = RemoteDataSource.endpoint else {
return Fail(error: URLError(URLError.badURL)).eraseToAnyPublisher()
}
return dataTaskPublisher(for: endpoint)
.decode(type: FireballsAPIData.self, decoder: JSONDecoder())
.mapError { _ in
return URLError(URLError.Code.badServerResponse)
}
.map { fireballs in
os_log(.info, log: OSLog.default, "Downloaded \(fireballs.data.count) fireballs")
return fireballs.data.compactMap { FireballData($0) }
}
.eraseToAnyPublisher()
}
}
struct FireballsAPIData: Decodable {
let signature: [String: String]
let count: String
let fields: [String]
let data: [[String?]]
}
struct FireballData: Decodable {
private static var dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
return formatter
}()
let dateTimeStamp: Date
let latitude: Double
let longitude: Double
let altitude: Double
let velocity: Double
let radiatedEnergy: Double
let impactEnergy: Double
init?(_ values: [String?]) {
// API fields: ["date","energy","impact-e","lat","lat-dir","lon","lon-dir","alt","vel"]
guard
!values.isEmpty,
let dateValue = values[0],
let date = FireballData.dateFormatter.date(from: dateValue) else {
return nil
}
dateTimeStamp = date
var energy: Double = 0
var impact: Double = 0
var lat: Double = 0
var lon: Double = 0
var alt: Double = 0
var vel: Double = 0
values.enumerated().forEach { value in
guard let field = value.element else { return }
if value.offset == 1 {
energy = Double(field) ?? 0
} else if value.offset == 2 {
impact = Double(field) ?? 0
} else if value.offset == 3 {
lat = Double(field) ?? 0
} else if value.offset == 4 && field == "S" {
lat = -lat
} else if value.offset == 5 {
lon = Double(field) ?? 0
} else if value.offset == 6 && field == "W" {
lon = -lon
} else if value.offset == 7 {
alt = Double(field) ?? 0
} else if value.offset == 8 {
vel = Double(field) ?? 0
}
}
radiatedEnergy = energy
impactEnergy = impact
latitude = lat
longitude = lon
altitude = alt
velocity = vel
}
}
后記
本篇主要講述了基于批插入和存儲歷史等高效CoreData使用示例,感興趣的給個贊或者關(guān)注~~~
