美文网首页
数据持久化方案解析(二十) —— 基于批插入和存储历史等高效Co

数据持久化方案解析(二十) —— 基于批插入和存储历史等高效Co

作者: 刀客传奇 | 来源:发表于2020-12-10 22:02 被阅读0次

    版本记录

    版本号 时间
    V1.0 2020.12.10 星期四

    前言

    数据的持久化存储是移动端不可避免的一个问题,很多时候的业务逻辑都需要我们进行本地化存储解决和完成,我们可以采用很多持久化存储方案,比如说plist文件(属性列表)、preference(偏好设置)、NSKeyedArchiver(归档)、SQLite 3CoreData,这里基本上我们都用过。这几种方案各有优缺点,其中,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使用示例,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

          本文标题:数据持久化方案解析(二十) —— 基于批插入和存储历史等高效Co

          本文链接:https://www.haomeiwen.com/subject/xpxdgktx.html