版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.12.10 星期四 |
前言
数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说
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测试(一)
14. 数据持久化方案解析(十四) —— 基于Unit Testing的Core Data测试(二)
15. 数据持久化方案解析(十五) —— 基于Realm和SwiftUI的数据持久化简单示例(一)
16. 数据持久化方案解析(十六) —— 基于Realm和SwiftUI的数据持久化简单示例(二)
17. 数据持久化方案解析(十七) —— 基于NSPersistentCloudKitContainer的Core Data和CloudKit的集成示例(一)
18. 数据持久化方案解析(十八) —— 基于NSPersistentCloudKitContainer的Core Data和CloudKit的集成示例(二)
19. 数据持久化方案解析(十九) —— 基于批插入和存储历史等高效CoreData使用示例(一)
源码
1. Swift
首先看下工程组织结构:
下面就是源码啦
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使用示例,感兴趣的给个赞或者关注~~~
网友评论