Core Data详细解析(六) —— 基于多上下文的Core Data简单解析示例(二)

版本记录

版本号 时间
V1.0 2018.09.26 星期三

前言

数据是移动端的重点关注对象,其中有一条就是数据存储。CoreData是苹果出的数据存储和持久化技术,面向对象进行数据相关存储。感兴趣的可以看下面几篇文章。
1. iOS CoreData(一)
2. iOS CoreData实现数据存储(二)
3. Core Data详细解析(三) —— 一个简单的入门示例(一)
4. Core Data详细解析(四) —— 一个简单的入门示例(二)
5. Core Data详细解析(五) —— 基于多上下文的Core Data简单解析示例(一)

Editing on a Scratchpad - 在Scratchpad上编辑

现在,SurfJournal在创建新日记帐分录或查看现有日记帐分录时使用主要上下文(coreDataStack.mainContext)。这种方法没有错,开始项目按原样运作。

对于像这样的日记式应用程序,您可以通过将编辑或新条目视为一组更改(如便笺簿)来简化应用程序体系结构。在用户编辑日记帐分录时,您将更新managed object的属性。更改完成后,您可以保存或丢弃它们,具体取决于用户想要执行的操作。

您可以将子managed object contexts视为可以完全丢弃的临时暂存区,或者将更改保存并发送到父上下文。

但从技术上讲,子上下文是什么?

所有managed object contexts都有一个父存储,您可以从中检索和更改managed objects形式的数据,例如此项目中的JournalEntry对象。通常,父存储是persistent store coordinator,这是CoreDataStack类提供的主上下文的情况。或者,您可以将给定上下文的父存储设置为另一个managed object context,使其成为子上下文。

保存子上下文时,更改仅转到父上下文。 在保存父上下文之前,不会将对父上下文的更改发送到持久性存储协调器(persistent store coordinator)

在您跳入并添加子上下文之前,您需要了解当前查看和编辑操作的工作原理。

1. Viewing and Editing - 查看和编辑操作

操作的第一部分需要从主列表视图到日志详细信息视图进行划分。 打开JournalListViewController.swift并找到prepare(for:sender :)

// 1
if segue.identifier == "SegueListToDetail" {
  // 2
  guard let navigationController =
    segue.destination as? UINavigationController,
    let detailViewController =
      navigationController.topViewController
        as? JournalEntryViewController,
    let indexPath = tableView.indexPathForSelectedRow else {
      fatalError("Application storyboard mis-configuration")
  }
  // 3
  let surfJournalEntry =
    fetchedResultsController.object(at: indexPath)
  // 4
  detailViewController.journalEntry = surfJournalEntry
  detailViewController.context =
    surfJournalEntry.managedObjectContext
  detailViewController.delegate = self

让我们一步步的看一下代码:

  • 1) 有两个segue:SegueListToDetailSegueListToDetailAdd。当用户点击表视图中的一行以查看或编辑以前的日记帐分录时,将运行前一个代码块中显示的第一个代码块。

  • 2) 接下来,您将获得用户最终会看到的JournalEntryViewController的引用。它出现在导航控制器内部,因此需要进行一些拆包。此代码还验证table view中是否存在选定的索引路径。

  • 3) 接下来,您将使用获取的results controller’sobject(at:)方法获取用户选择的JournalEntry

  • 4) 最后,在JournalEntryViewController实例上设置所有必需的变量。 surfJournalEntry变量对应于在步骤3中解析的JournalEntry实体。上下文变量是用于任何操作的managed object context;现在,它只使用主要上下文。 JournalListViewController将自身设置为JournalEntryViewController的委托,以便在用户完成编辑操作时通知它。

SegueListToDetailAdd类似于SegueListToDetail,除了应用程序创建一个新的JournalEntry实体而不是检索现有的实体。 当用户点击右上角的加号(+)按钮创建新的日记帐分录时,应用程序将执行SegueListToDetailAdd

现在您已了解两个segue的工作原理,打开JournalEntryViewController.swift并查看文件顶部的JournalEntryDelegate协议:

protocol JournalEntryDelegate {
  func didFinish(viewController: JournalEntryViewController,
                 didSave: Bool)
}

JournalEntryDelegate协议非常简短,只包含一个方法:didFinish(viewController:didSave :)。 协议要求委托实施的此方法指示用户是否已完成编辑或查看日记帐分录以及是否应保存任何更改。

要了解didFinish(viewController:didSave :)是如何工作的,切换回JournalListViewController.swift并找到该方法:

func didFinish(viewController: JournalEntryViewController,
               didSave: Bool) {
  // 1
  guard didSave,
    let context = viewController.context,
    context.hasChanges else {
      dismiss(animated: true)
      return
  }
  // 2
  context.perform {
    do {
      try context.save()
    } catch let error as NSError {
      fatalError("Error: \(error.localizedDescription)")
    }
    // 3
    self.coreDataStack.saveContext()
  }
  // 4
  dismiss(animated: true)
}

下面进行详细分解:

  • 1) 首先,使用guard语句检查didSave参数。如果用户点击“保存”按钮而不是“取消”按钮,则会出现这种情况,因此应用应保存用户的数据。 guard语句还使用hasChanges属性来检查是否有任何改变;如果没有任何改变,就没有必要浪费时间做更多的工作。

  • 2) 接下来,在perform(_ :)闭包内进行journalEntryViewController上下文保存。代码将此上下文设置为主上下文;在这种情况下,它有点多余,因为只有一个上下文,但这不会改变行为。

稍后将子上下文添加到工作流后,JournalEntryViewController上下文将与主上下文不同,从而使此代码成为必需。

如果保存失败,请调用fatalError以使用相关错误信息中止应用程序。

  • 3) 接下来,通过在CoreDataStack.swift中定义的saveContext保存主上下文,将任何编辑持久保存到磁盘。

  • 4) 最后,关闭JournalEntryViewController

注意:如果managed object context的类型为MainQueueConcurrencyType,则不必在perform(_ :)中包装代码,但使用它并没有什么坏处。如果您不知道上下文的类型,就像didFinish(viewController:didSave :)中的情况一样,使用perform(_ :)最安全,因此它将适用于父上下文和子上下文。

上述实现存在问题 - 您是否发现了它?

当应用程序添加新的旅行实体记录时,它会创建一个新对象并将其添加到managed object context。 如果用户点击“取消”按钮,则应用程序将不会保存上下文,但新对象仍将存在。 如果用户然后添加并保存另一个条目,则取消的对象仍然存在! 你不会在UI中看到它,除非你有耐心一直滚动到最后,但它会显示在CSV导出的底部。

您可以通过在用户取消视图控制器时删除对象来解决此问题。 但是,如果更改很复杂,涉及多个对象,或者要求您在编辑工作流程中更改对象的属性,该怎么办? 使用子上下文可以帮助您轻松管理这些复杂的情况。

2. Using Child Contexts for Sets of Edits - 使用子上下文编辑集

现在您已了解应用程序当前如何编辑和创建JournalEntry实体,您将修改实现以将子managed object context用作临时暂存区。

这很容易 - 你只需要修改segues。 打开JournalListViewController.swift并在prepare(for:sender :)中找到SegueListToDetail的以下代码:

detailViewController.journalEntry = surfJournalEntry
detailViewController.context =
  surfJournalEntry.managedObjectContext
detailViewController.delegate = self

将上面的代码进行如下替换:

// 1
let childContext = NSManagedObjectContext(
  concurrencyType: .mainQueueConcurrencyType)
childContext.parent = coreDataStack.mainContext

// 2
let childEntry = childContext.object(
  with: surfJournalEntry.objectID) as? JournalEntry

// 3
detailViewController.journalEntry = childEntry
detailViewController.context = childContext
detailViewController.delegate = self

下面进行逐步解析:

  • 1) 首先,使用.mainQueueConcurrencyType创建名为childContext的新managed object context。 在这里,您可以像创建managed object context时通常那样设置父上下文而不是持久性存储协调器(persistent store coordinator)。 在这里,您将parent设置为CoreDataStackmainContext

  • 2) 接下来,使用子上下文的object(with:)方法检索相关的记录。 您必须使用object(with :)来检索条目,因为managed objects特定于创建它们的上下文。 但是,objectID值并非特定于单个上下文,因此您可以在需要访问多个上下文中的对象时使用它们。

  • 3) 最后,在JournalEntryViewController实例上设置所有必需的变量。 这次,您使用childEntrychildContext而不是原始的surfJournalEntrysurfJournalEntry.managedObjectContext

注意:您可能想知道为什么需要将managed objectmanaged object context都传递给detailViewController,因为managed object已经有了上下文变量。 这是因为managed object仅具有对上下文的弱weak引用。 如果您没有传递上下文,ARC将从内存中删除上下文(因为没有其他内容retain它),并且应用程序将不会按预期运行。

构建并运行您的应用程序,它应该像以前一样工作。 在这种情况下,应用程序没有明显的变化是好事;用户仍然可以点击一行来查看和编辑冲浪会话条目。

通过使用子上下文作为日志编辑的容器,您可以降低应用程序体系结构的复杂性。 通过在单独的上下文中进行编辑,取消或保存managed object更改是微不足道的。


源码

1. Swift源码

下面看一下Swift源码。

1. AppDelegate.swift
import UIKit
import CoreData

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  
  var window: UIWindow?
  lazy var coreDataStack = CoreDataStack(modelName: "SurfJournalModel")

   func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    _ = coreDataStack.mainContext

    guard let navigationController = window?.rootViewController as? UINavigationController,
      let listViewController = navigationController.topViewController as? JournalListViewController else {
        fatalError("Application Storyboard mis-configuration")
    }

    listViewController.coreDataStack = coreDataStack

    return true
  }

  func applicationWillTerminate(_ application: UIApplication) {
    coreDataStack.saveContext()
  }
}
2. JournalListViewController.swift
import UIKit
import CoreData

class JournalListViewController: UITableViewController {

  // MARK: Properties
  var coreDataStack: CoreDataStack!
  var fetchedResultsController: NSFetchedResultsController<JournalEntry> = NSFetchedResultsController()

  // MARK: IBOutlets
  @IBOutlet weak var exportButton: UIBarButtonItem!

  // MARK: View Life Cycle
  override func viewDidLoad() {
    super.viewDidLoad()

    configureView()
  }

  // MARK: Navigation
  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    // 1
    if segue.identifier == "SegueListToDetail" {
      // 2
      guard let navigationController = segue.destination as? UINavigationController,
        let detailViewController = navigationController.topViewController as? JournalEntryViewController,
        let indexPath = tableView.indexPathForSelectedRow else {
          fatalError("Application storyboard mis-configuration")
      }
      // 3
      let surfJournalEntry = fetchedResultsController.object(at: indexPath)

      let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
      childContext.parent = coreDataStack.mainContext

      let childEntry = childContext.object(with: surfJournalEntry.objectID) as? JournalEntry

      detailViewController.journalEntry = childEntry
      detailViewController.context = childContext
      detailViewController.delegate = self

    } else if segue.identifier == "SegueListToDetailAdd" {

      guard let navigationController = segue.destination as? UINavigationController,
        let detailViewController = navigationController.topViewController as? JournalEntryViewController else {
          fatalError("Application storyboard mis-configuration")
      }

      let newJournalEntry = JournalEntry(context: coreDataStack.mainContext)

      detailViewController.journalEntry = newJournalEntry
      detailViewController.context = newJournalEntry.managedObjectContext
      detailViewController.delegate = self
    }
  }
}

// MARK: IBActions
extension JournalListViewController {

  @IBAction func exportButtonTapped(_ sender: UIBarButtonItem) {
    exportCSVFile()
  }
}

// MARK: Private
private extension JournalListViewController {

  func configureView() {
    fetchedResultsController = journalListFetchedResultsController()
  }
  
  func exportCSVFile() {
    navigationItem.leftBarButtonItem = activityIndicatorBarButtonItem()
    // 1
    coreDataStack.storeContainer.performBackgroundTask { context in
      var results: [JournalEntry] = []
      do {
        results = try context.fetch(self.surfJournalFetchRequest())
      } catch let error as NSError {
        print("ERROR: \(error.localizedDescription)")
      }

      // 2
      let exportFilePath = NSTemporaryDirectory() + "export.csv"
      let exportFileURL = URL(fileURLWithPath: exportFilePath)
      FileManager.default.createFile(atPath: exportFilePath, contents: Data(), attributes: nil)

      // 3
      let fileHandle: FileHandle?
      do {
        fileHandle = try FileHandle(forWritingTo: exportFileURL)
      } catch let error as NSError {
        print("ERROR: \(error.localizedDescription)")
        fileHandle = nil
      }

      if let fileHandle = fileHandle {
        // 4
        for journalEntry in results {
          fileHandle.seekToEndOfFile()
          guard let csvData = journalEntry
            .csv()
            .data(using: .utf8, allowLossyConversion: false) else {
              continue
          }
          fileHandle.write(csvData)
        }

        // 5
        fileHandle.closeFile()

        print("Export Path: \(exportFilePath)")
        // 6
        DispatchQueue.main.async {
          self.navigationItem.leftBarButtonItem = self.exportBarButtonItem()
          self.showExportFinishedAlertView(exportFilePath)
        }
      } else {
        DispatchQueue.main.async {
          self.navigationItem.leftBarButtonItem = self.exportBarButtonItem()
        }
      }
    } // 7 Closing brace for performBackgroundTask
  }

  // MARK: Export
  
  func activityIndicatorBarButtonItem() -> UIBarButtonItem {
    let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
    let barButtonItem = UIBarButtonItem(customView: activityIndicator)
    activityIndicator.startAnimating()
    
    return barButtonItem
  }
  
  func exportBarButtonItem() -> UIBarButtonItem {
    return UIBarButtonItem(title: "Export", style: .plain, target: self, action: #selector(exportButtonTapped(_:)))
  }
  
  func showExportFinishedAlertView(_ exportPath: String) {
    let message = "The exported CSV file can be found at \(exportPath)"
    let alertController = UIAlertController(title: "Export Finished", message: message, preferredStyle: .alert)
    let dismissAction = UIAlertAction(title: "Dismiss", style: .default)
    alertController.addAction(dismissAction)
    
    present(alertController, animated: true)
  }
}

// MARK: NSFetchedResultsController
private extension JournalListViewController {
  
  func journalListFetchedResultsController() -> NSFetchedResultsController<JournalEntry> {
    let fetchedResultController = NSFetchedResultsController(fetchRequest: surfJournalFetchRequest(),
                                                             managedObjectContext: coreDataStack.mainContext,
                                                             sectionNameKeyPath: nil,
                                                             cacheName: nil)
    fetchedResultController.delegate = self

    do {
      try fetchedResultController.performFetch()
    } catch let error as NSError {
      fatalError("Error: \(error.localizedDescription)")
    }

    return fetchedResultController
  }

  func surfJournalFetchRequest() -> NSFetchRequest<JournalEntry> {
    let fetchRequest:NSFetchRequest<JournalEntry> = JournalEntry.fetchRequest()
    fetchRequest.fetchBatchSize = 20

    let sortDescriptor = NSSortDescriptor(key: #keyPath(JournalEntry.date), ascending: false)
    fetchRequest.sortDescriptors = [sortDescriptor]

    return fetchRequest
  }
}

// MARK: NSFetchedResultsControllerDelegate
extension JournalListViewController: NSFetchedResultsControllerDelegate {

  func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    tableView.reloadData()
  }
}

// MARK: UITableViewDataSource
extension JournalListViewController {

  override func numberOfSections(in tableView: UITableView) -> Int {
    return fetchedResultsController.sections?.count ?? 0
  }

  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return fetchedResultsController.sections?[section].numberOfObjects ?? 0
  }

  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! SurfEntryTableViewCell
    configureCell(cell, indexPath: indexPath)
    return cell
  }

  private func configureCell(_ cell: SurfEntryTableViewCell, indexPath: IndexPath) {
    let surfJournalEntry = fetchedResultsController.object(at: indexPath)
    cell.dateLabel.text = surfJournalEntry.stringForDate()
    
    guard let rating = surfJournalEntry.rating?.int32Value else { return }

    switch rating {
    case 1:
      cell.starOneFilledImageView.isHidden = false
      cell.starTwoFilledImageView.isHidden = true
      cell.starThreeFilledImageView.isHidden = true
      cell.starFourFilledImageView.isHidden = true
      cell.starFiveFilledImageView.isHidden = true
    case 2:
      cell.starOneFilledImageView.isHidden = false
      cell.starTwoFilledImageView.isHidden = false
      cell.starThreeFilledImageView.isHidden = true
      cell.starFourFilledImageView.isHidden = true
      cell.starFiveFilledImageView.isHidden = true
    case 3:
      cell.starOneFilledImageView.isHidden = false
      cell.starTwoFilledImageView.isHidden = false
      cell.starThreeFilledImageView.isHidden = false
      cell.starFourFilledImageView.isHidden = true
      cell.starFiveFilledImageView.isHidden = true
    case 4:
      cell.starOneFilledImageView.isHidden = false
      cell.starTwoFilledImageView.isHidden = false
      cell.starThreeFilledImageView.isHidden = false
      cell.starFourFilledImageView.isHidden = false
      cell.starFiveFilledImageView.isHidden = true
    case 5:
      cell.starOneFilledImageView.isHidden = false
      cell.starTwoFilledImageView.isHidden = false
      cell.starThreeFilledImageView.isHidden = false
      cell.starFourFilledImageView.isHidden = false
      cell.starFiveFilledImageView.isHidden = false
    default:
      cell.starOneFilledImageView.isHidden = true
      cell.starTwoFilledImageView.isHidden = true
      cell.starThreeFilledImageView.isHidden = true
      cell.starFourFilledImageView.isHidden = true
      cell.starFiveFilledImageView.isHidden = true
    }
  }
  
  override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
    guard case(.delete) = editingStyle else { return }

    let surfJournalEntry = fetchedResultsController.object(at: indexPath)
    coreDataStack.mainContext.delete(surfJournalEntry)
    coreDataStack.saveContext()
  }
}

// MARK: JournalEntryDelegate
extension JournalListViewController: JournalEntryDelegate {

  func didFinish(viewController: JournalEntryViewController, didSave: Bool) {
    // 1
    guard didSave,
      let context = viewController.context,
      context.hasChanges else {
        dismiss(animated: true)
        return
    }
    // 2
    context.perform {
      do {
        try context.save()
      } catch let error as NSError {
        fatalError("Error: \(error.localizedDescription)")
      }
      // 3
      self.coreDataStack.saveContext()
    }
    // 4
    dismiss(animated: true)
  }
}
3. JournalEntryViewController.swift
import UIKit
import CoreData

// MARK: JournalEntryDelegate
protocol JournalEntryDelegate {
  func didFinish(viewController: JournalEntryViewController, didSave: Bool)
}

class JournalEntryViewController: UITableViewController {

  // MARK: Properties
  var journalEntry: JournalEntry?
  var context: NSManagedObjectContext!
  var delegate:JournalEntryDelegate?

  // MARK: IBOutlets
  @IBOutlet weak var heightTextField: UITextField!
  @IBOutlet weak var periodTextField: UITextField!
  @IBOutlet weak var windTextField: UITextField!
  @IBOutlet weak var locationTextField: UITextField!
  @IBOutlet weak var ratingSegmentedControl: UISegmentedControl!

  // MARK: View Lifecycle
  override func viewDidLoad() {
    super.viewDidLoad()

    configureView()
  }
}

// MARK: Private
private extension JournalEntryViewController {

  func configureView() {
    guard let journalEntry = journalEntry else { return }

    title = journalEntry.stringForDate()

    heightTextField.text = journalEntry.height
    periodTextField.text = journalEntry.period
    windTextField.text = journalEntry.wind
    locationTextField.text = journalEntry.location

    guard let rating = journalEntry.rating else { return }

    ratingSegmentedControl.selectedSegmentIndex = rating.intValue - 1
  }
  
  func updateJournalEntry() {
    guard let entry = journalEntry else { return }

    entry.date = Date()
    entry.height = heightTextField.text
    entry.period = periodTextField.text
    entry.wind = windTextField.text
    entry.location = locationTextField.text
    entry.rating = NSNumber(value:ratingSegmentedControl.selectedSegmentIndex + 1)
  }
}

// MARK: IBActions
extension JournalEntryViewController {

  @IBAction func cancelButtonWasTapped(_ sender: UIBarButtonItem) {
    delegate?.didFinish(viewController: self, didSave: false)
  }

  @IBAction func saveButtonWasTapped(_ sender: UIBarButtonItem) {
    updateJournalEntry()
    delegate?.didFinish(viewController: self, didSave: true)
  }
}
4. SurfEntryTableViewCell.swift
import UIKit

class SurfEntryTableViewCell: UITableViewCell {

  // MARK: IBOutlets
  @IBOutlet weak var dateLabel: UILabel!
  @IBOutlet weak var starOneImageView: UIImageView!
  @IBOutlet weak var starTwoImageView: UIImageView!
  @IBOutlet weak var starThreeImageView: UIImageView!
  @IBOutlet weak var starFourImageView: UIImageView!
  @IBOutlet weak var starFiveImageView: UIImageView!
  @IBOutlet weak var starOneFilledImageView: UIImageView!
  @IBOutlet weak var starTwoFilledImageView: UIImageView!
  @IBOutlet weak var starThreeFilledImageView: UIImageView!
  @IBOutlet weak var starFourFilledImageView: UIImageView!
  @IBOutlet weak var starFiveFilledImageView: UIImageView!

}
5. CoreDataStack.swift
import CoreData

class CoreDataStack {

  // MARK: Properties
  private let modelName: String

  lazy var mainContext: NSManagedObjectContext = {
    return self.storeContainer.viewContext
  }()

  lazy var storeContainer: NSPersistentContainer = {

    let container = NSPersistentContainer(name: self.modelName)
    self.seedCoreDataContainerIfFirstLaunch()
    container.loadPersistentStores { (storeDescription, error) in
      if let error = error as NSError? {
        fatalError("Unresolved error \(error), \(error.userInfo)")
      }
    }

    return container
  }()

  // MARK: Initializers
  init(modelName: String) {
    self.modelName = modelName
  }
}

// MARK: Internal
extension CoreDataStack {

  func saveContext () {
    guard mainContext.hasChanges else { return }

    do {
      try mainContext.save()
    } catch let nserror as NSError {
      fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
    }
  }
}

// MARK: Private
private extension CoreDataStack {

  func seedCoreDataContainerIfFirstLaunch() {
    
    // 1
    let previouslyLaunched = UserDefaults.standard.bool(forKey: "previouslyLaunched")
    if !previouslyLaunched {
      UserDefaults.standard.set(true, forKey: "previouslyLaunched")
      
      // Default directory where the CoreDataStack will store its files
      let directory = NSPersistentContainer.defaultDirectoryURL()
      let url = directory.appendingPathComponent(modelName + ".sqlite")
      
      // 2: Copying the SQLite file
      let seededDatabaseURL = Bundle.main.url(forResource: modelName, withExtension: "sqlite")!
      _ = try? FileManager.default.removeItem(at: url)
      do {
        try FileManager.default.copyItem(at: seededDatabaseURL, to: url)
      } catch let nserror as NSError {
        fatalError("Error: \(nserror.localizedDescription)")
      }
      
      // 3: Copying the SHM file
      let seededSHMURL = Bundle.main.url(forResource: modelName, withExtension: "sqlite-shm")!
      let shmURL = directory.appendingPathComponent(modelName + ".sqlite-shm")
      _ = try? FileManager.default.removeItem(at: shmURL)
      do {
        try FileManager.default.copyItem(at: seededSHMURL, to: shmURL)
      } catch let nserror as NSError {
        fatalError("Error: \(nserror.localizedDescription)")
      }
      
      // 4: Copying the WAL file
      let seededWALURL = Bundle.main.url(forResource: modelName, withExtension: "sqlite-wal")!
      let walURL = directory.appendingPathComponent(modelName + ".sqlite-wal")
      _ = try? FileManager.default.removeItem(at: walURL)
      do {
        try FileManager.default.copyItem(at: seededWALURL, to: walURL)
      } catch let nserror as NSError {
        fatalError("Error: \(nserror.localizedDescription)")
      }
      
      print("Seeded Core Data")
    }
  }
}
6. JournalEntry.swift
import Foundation
import CoreData

class JournalEntry: NSManagedObject {
  
  @nonobjc public class func fetchRequest() -> NSFetchRequest<JournalEntry> {
    return NSFetchRequest<JournalEntry>(entityName: "JournalEntry")
  }

  @NSManaged var date: Date?
  @NSManaged var height: String?
  @NSManaged var period: String?
  @NSManaged var wind: String?
  @NSManaged var location: String?
  @NSManaged var rating: NSNumber?
}
7. JournalEntry+Helper.swift
import Foundation
import CoreData

extension JournalEntry {

  func stringForDate() -> String {
    guard let date = date else { return "" }

    let dateFormatter = DateFormatter()
    dateFormatter.dateStyle = .short
    return dateFormatter.string(from: date)
  }

  func csv() -> String {
    let coalescedHeight = height ?? ""
    let coalescedPeriod = period ?? ""
    let coalescedWind = wind ?? ""
    let coalescedLocation = location ?? ""
    let coalescedRating: String
    if let rating = rating?.int32Value {
      coalescedRating = String(rating)
    } else {
      coalescedRating = ""
    }

    return "\(stringForDate()),\(coalescedHeight),\(coalescedPeriod),\(coalescedWind),\(coalescedLocation),\(coalescedRating)\n"
  }
}

下面看一下实现效果

后记

本篇主要讲述了基于多上下文的Core Data简单解析示例,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容