mirror of
https://github.com/JohnEstropia/CoreStore.git
synced 2026-05-28 10:29:21 +02:00
446 lines
16 KiB
Swift
446 lines
16 KiB
Swift
//
|
|
// MigrationsDemoViewController.swift
|
|
// LegacyDemo
|
|
//
|
|
// Created by John Rommel Estropia on 2015/06/21.
|
|
// Copyright © 2018 John Rommel Estropia. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import CoreStore
|
|
|
|
|
|
// MARK: - MigrationsDemoViewController
|
|
|
|
class MigrationsDemoViewController: UIViewController, ListObserver, UITableViewDataSource, UITableViewDelegate {
|
|
|
|
// MARK: UIViewController
|
|
|
|
override func viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
if let segmentedControl = self.segmentedControl {
|
|
|
|
for (index, model) in self.models.enumerated() {
|
|
|
|
segmentedControl.setTitle(
|
|
model.label,
|
|
forSegmentAt: index
|
|
)
|
|
}
|
|
}
|
|
self.set(dataStack: nil, model: nil, scrollToSelection: false)
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
|
|
super.viewDidAppear(animated)
|
|
|
|
let alert = UIAlertController(
|
|
title: "Migrations Demo",
|
|
message: "This demo shows how to run progressive migrations and how to support multiple model versions in a single project.\n\nThe persistent store contains 10000 organisms, which gain/lose properties when the migration evolves/devolves them.\n\nYou can use the \"mutate\" button to change an organism's properties then migrate to a different model to see how its value gets affected.",
|
|
preferredStyle: .alert
|
|
)
|
|
alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
|
|
self.present(alert, animated: true, completion: nil)
|
|
|
|
|
|
let modelMetadata = withExtendedLifetime(DataStack(xcodeModelName: "MigrationDemo")) {
|
|
(dataStack: DataStack) -> ModelMetadata in
|
|
|
|
let models = self.models
|
|
let migrations = try! dataStack.requiredMigrationsForStorage(
|
|
SQLiteStore(fileName: "MigrationDemo.sqlite")
|
|
)
|
|
|
|
guard let storeVersion = migrations.first?.sourceVersion else {
|
|
|
|
return models.first!
|
|
}
|
|
for model in models {
|
|
|
|
if model.schemaHistory.currentModelVersion == storeVersion {
|
|
|
|
return model
|
|
}
|
|
}
|
|
|
|
return models.first!
|
|
}
|
|
|
|
self.selectModelVersion(modelMetadata)
|
|
}
|
|
|
|
// MARK: ListObserver
|
|
|
|
func listMonitorWillChange(_ monitor: ListMonitor<NSManagedObject>) { }
|
|
|
|
func listMonitorDidChange(_ monitor: ListMonitor<NSManagedObject>) {
|
|
|
|
if self.lastSelectedIndexPath == nil,
|
|
let numberOfObjectsInSection = self.listMonitor?.numberOfObjects(in: 0),
|
|
numberOfObjectsInSection > 0 {
|
|
|
|
self.tableView?.reloadData()
|
|
self.setSelectedIndexPath(IndexPath(row: 0, section: 0), scrollToSelection: false)
|
|
}
|
|
else {
|
|
|
|
self.updateDisplay(reloadData: true, scrollToSelection: true, animated: true)
|
|
}
|
|
}
|
|
|
|
func listMonitorDidRefetch(_ monitor: ListMonitor<NSManagedObject>) {
|
|
|
|
self.listMonitorDidChange(monitor)
|
|
}
|
|
|
|
// MARK: UITableViewDataSource
|
|
|
|
@objc dynamic func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
|
|
return self.listMonitor?.numberOfObjects(in: 0) ?? 0
|
|
}
|
|
|
|
@objc dynamic func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
|
|
let cell = tableView.dequeueReusableCell(withIdentifier: "OrganismTableViewCell", for: indexPath) as! OrganismTableViewCell
|
|
|
|
let dna = (self.listMonitor?[indexPath] as? OrganismProtocol)?.dna.description ?? ""
|
|
cell.dnaLabel?.text = "DNA: \(dna)"
|
|
cell.mutateButtonHandler = { [weak self] () -> Void in
|
|
|
|
guard let `self` = self,
|
|
let dataStack = self.dataStack,
|
|
let organism = self.listMonitor?[indexPath] else {
|
|
|
|
return
|
|
}
|
|
|
|
self.setSelectedIndexPath(indexPath, scrollToSelection: false)
|
|
self.setEnabled(false)
|
|
dataStack.perform(
|
|
asynchronous: { (transaction) in
|
|
|
|
let organism = transaction.edit(organism) as! OrganismProtocol
|
|
organism.mutate()
|
|
},
|
|
completion: { [weak self] _ in
|
|
|
|
self?.setEnabled(true)
|
|
}
|
|
)
|
|
}
|
|
return cell
|
|
}
|
|
|
|
|
|
// MARK: UITableViewDelegate
|
|
|
|
@objc dynamic func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
|
|
self.setSelectedIndexPath(indexPath, scrollToSelection: false)
|
|
}
|
|
|
|
|
|
// MARK: Private
|
|
|
|
private typealias ModelMetadata = (label: String, entityType: NSManagedObject.Type, schemaHistory: SchemaHistory)
|
|
|
|
private let models: [ModelMetadata] = [
|
|
(
|
|
label: "Model V1",
|
|
entityType: OrganismV1.self,
|
|
schemaHistory: SchemaHistory(
|
|
XcodeDataModelSchema.from(
|
|
modelName: "MigrationDemo",
|
|
migrationChain: ["MigrationDemoV3", "MigrationDemoV2", "MigrationDemo"]
|
|
),
|
|
migrationChain: ["MigrationDemoV3", "MigrationDemoV2", "MigrationDemo"]
|
|
)
|
|
),
|
|
(
|
|
label: "Model V2",
|
|
entityType: OrganismV2.self,
|
|
schemaHistory: SchemaHistory(
|
|
XcodeDataModelSchema.from(
|
|
modelName: "MigrationDemo",
|
|
migrationChain: [
|
|
"MigrationDemo": "MigrationDemoV2",
|
|
"MigrationDemoV3": "MigrationDemoV2"
|
|
]
|
|
),
|
|
migrationChain: [
|
|
"MigrationDemo": "MigrationDemoV2",
|
|
"MigrationDemoV3": "MigrationDemoV2"
|
|
]
|
|
)
|
|
),
|
|
(
|
|
label: "Model V3",
|
|
entityType: OrganismV3.self,
|
|
schemaHistory: SchemaHistory(
|
|
XcodeDataModelSchema.from(
|
|
modelName: "MigrationDemo",
|
|
migrationChain: ["MigrationDemo", "MigrationDemoV2", "MigrationDemoV3"]
|
|
),
|
|
migrationChain: ["MigrationDemo", "MigrationDemoV2", "MigrationDemoV3"]
|
|
)
|
|
)
|
|
]
|
|
|
|
private var _listMonitor: ListMonitor<NSManagedObject>?
|
|
private var listMonitor: ListMonitor<NSManagedObject>? {
|
|
|
|
return self._listMonitor
|
|
}
|
|
|
|
private var _dataStack: DataStack?
|
|
private var dataStack: DataStack? {
|
|
|
|
return self._dataStack
|
|
}
|
|
|
|
private var _lastSelectedIndexPath: IndexPath?
|
|
private var lastSelectedIndexPath: IndexPath? {
|
|
|
|
return self._lastSelectedIndexPath
|
|
}
|
|
|
|
private func setSelectedIndexPath(_ indexPath: IndexPath, scrollToSelection: Bool) {
|
|
|
|
self._lastSelectedIndexPath = indexPath
|
|
self.updateDisplay(reloadData: false, scrollToSelection: scrollToSelection, animated: true)
|
|
}
|
|
|
|
@IBOutlet private dynamic weak var headerContainer: UIView?
|
|
@IBOutlet private dynamic weak var titleLabel: UILabel?
|
|
@IBOutlet private dynamic weak var organismLabel: UILabel?
|
|
@IBOutlet private dynamic weak var segmentedControl: UISegmentedControl?
|
|
@IBOutlet private dynamic weak var progressView: UIProgressView?
|
|
@IBOutlet private dynamic weak var tableView: UITableView?
|
|
|
|
@IBAction private dynamic func segmentedControlValueChanged(_ sender: AnyObject?) {
|
|
|
|
guard let index = self.segmentedControl?.selectedSegmentIndex else {
|
|
|
|
return
|
|
}
|
|
|
|
self.selectModelVersion(self.models[index])
|
|
}
|
|
|
|
private func selectModelVersion(_ model: ModelMetadata) {
|
|
|
|
if self.dataStack?.modelVersion == model.schemaHistory.currentModelVersion {
|
|
|
|
return
|
|
}
|
|
|
|
self.set(dataStack: nil, model: nil, scrollToSelection: false) // explicitly trigger NSPersistentStore cleanup by deallocating the stack
|
|
|
|
let dataStack = DataStack(schemaHistory: model.schemaHistory)
|
|
|
|
self.setEnabled(false)
|
|
let progress = dataStack.addStorage(
|
|
SQLiteStore(
|
|
fileName: "MigrationDemo.sqlite",
|
|
migrationMappingProviders: [
|
|
CustomSchemaMappingProvider(
|
|
from: "MigrationDemoV3",
|
|
to: "MigrationDemoV2",
|
|
entityMappings: [
|
|
.transformEntity(
|
|
sourceEntity: "Organism",
|
|
destinationEntity: "Organism",
|
|
transformer: { (source, createDestination) in
|
|
|
|
let destination = createDestination()
|
|
destination.enumerateAttributes { (attribute, sourceAttribute) in
|
|
|
|
if let sourceAttribute = sourceAttribute {
|
|
|
|
destination[attribute] = source[sourceAttribute]
|
|
}
|
|
}
|
|
destination["numberOfFlippers"] = source["numberOfLimbs"]
|
|
}
|
|
)
|
|
]
|
|
)
|
|
]
|
|
),
|
|
completion: { [weak self] (result) -> Void in
|
|
|
|
guard let `self` = self else {
|
|
|
|
return
|
|
}
|
|
|
|
guard case .success = result else {
|
|
|
|
self.setEnabled(true)
|
|
return
|
|
}
|
|
|
|
self.set(dataStack: dataStack, model: model, scrollToSelection: true)
|
|
|
|
let count = try! dataStack.queryValue(
|
|
From<NSManagedObject>(model.entityType)
|
|
.select(Int.self, .count(#keyPath(OrganismV1.dna))))!
|
|
if count > 0 {
|
|
|
|
self.setEnabled(true)
|
|
}
|
|
else {
|
|
|
|
for i: Int64 in 0 ..< 20 {
|
|
|
|
dataStack.perform(
|
|
asynchronous: { (transaction) in
|
|
|
|
for j: Int64 in 0 ..< 500 {
|
|
|
|
let organism = transaction.create(Into(model.entityType)) as! OrganismProtocol
|
|
organism.dna = (i * 500) + j + 1
|
|
organism.mutate()
|
|
}
|
|
},
|
|
completion: { _ in }
|
|
)
|
|
}
|
|
dataStack.perform(
|
|
asynchronous: { _ in },
|
|
completion: { [weak self] _ in
|
|
|
|
self?.setEnabled(true)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
)
|
|
|
|
if let progress = progress {
|
|
|
|
progress.setProgressHandler { [weak self] (progress) -> Void in
|
|
|
|
self?.reloadTableHeaderWithProgress(progress)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setEnabled(_ enabled: Bool) {
|
|
|
|
UIView.animate(
|
|
withDuration: 0.2,
|
|
delay: 0,
|
|
options: .beginFromCurrentState,
|
|
animations: { () -> Void in
|
|
|
|
let navigationItem = self.navigationItem
|
|
navigationItem.leftBarButtonItem?.isEnabled = enabled
|
|
navigationItem.rightBarButtonItem?.isEnabled = enabled
|
|
navigationItem.hidesBackButton = !enabled
|
|
|
|
self.segmentedControl?.isEnabled = enabled
|
|
|
|
if let tableView = self.tableView {
|
|
|
|
tableView.alpha = enabled ? 1.0 : 0.5
|
|
tableView.isUserInteractionEnabled = enabled
|
|
}
|
|
},
|
|
completion: nil
|
|
)
|
|
}
|
|
|
|
private func set(dataStack: DataStack?, model: ModelMetadata?, scrollToSelection: Bool) {
|
|
|
|
if let dataStack = dataStack, let model = model {
|
|
|
|
self.segmentedControl?.selectedSegmentIndex = self.models
|
|
.firstIndex(
|
|
where: { (arg) -> Bool in
|
|
|
|
let (_, _, schemaHistory) = arg
|
|
return schemaHistory.currentModelVersion == model.schemaHistory.currentModelVersion
|
|
}
|
|
)!
|
|
|
|
self._dataStack = dataStack
|
|
let listMonitor = dataStack.monitorList(
|
|
From(model.entityType),
|
|
OrderBy<NSManagedObject>(.descending(#keyPath(OrganismV1.dna)))
|
|
)
|
|
listMonitor.addObserver(self)
|
|
self._listMonitor = listMonitor
|
|
|
|
if self.lastSelectedIndexPath == nil {
|
|
|
|
if listMonitor.numberOfObjects(in: 0) > 0 {
|
|
|
|
self.setSelectedIndexPath(IndexPath(row: 0, section: 0), scrollToSelection: true)
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
|
|
self.segmentedControl?.selectedSegmentIndex = UISegmentedControl.noSegment
|
|
self._listMonitor = nil
|
|
self._dataStack = nil
|
|
}
|
|
|
|
self.updateDisplay(reloadData: true, scrollToSelection: scrollToSelection, animated: false)
|
|
}
|
|
|
|
private func reloadTableHeaderWithProgress(_ progress: Progress) {
|
|
|
|
self.progressView?.setProgress(Float(progress.fractionCompleted), animated: true)
|
|
self.titleLabel?.text = "Migrating: \(progress.localizedDescription ?? "")"
|
|
self.organismLabel?.text = "Progressive step \(progress.localizedAdditionalDescription ?? "")"
|
|
}
|
|
|
|
private func updateDisplay(reloadData: Bool, scrollToSelection: Bool, animated: Bool) {
|
|
|
|
var lines = [String]()
|
|
var organismType = ""
|
|
if let indexPath = self.lastSelectedIndexPath, let organism = self.listMonitor?[indexPath] {
|
|
|
|
for property in organism.entity.properties {
|
|
|
|
let value = organism.value(forKey: property.name) ?? NSNull()
|
|
lines.append("\(property.name): \(value)")
|
|
}
|
|
organismType = organism.entity.managedObjectClassName
|
|
}
|
|
|
|
self.titleLabel?.text = organismType
|
|
self.organismLabel?.text = lines.joined(separator: "\n")
|
|
self.progressView?.progress = 0
|
|
|
|
self.headerContainer?.setNeedsLayout()
|
|
|
|
guard let tableView = self.tableView else {
|
|
|
|
return
|
|
}
|
|
|
|
if reloadData {
|
|
|
|
tableView.reloadData()
|
|
}
|
|
|
|
tableView.layoutIfNeeded()
|
|
|
|
if let indexPath = self.lastSelectedIndexPath,
|
|
indexPath.row < tableView.numberOfRows(inSection: 0) {
|
|
|
|
tableView.selectRow(at: indexPath,
|
|
animated: scrollToSelection && animated,
|
|
scrollPosition: scrollToSelection ? .middle : .none
|
|
)
|
|
}
|
|
}
|
|
}
|