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

2020-12-10  本文已影响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使用示例,感兴趣的给个赞或者关注~~~

上一篇 下一篇

猜你喜欢

热点阅读