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

版本记录

版本号 时间
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使用示例,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容