// // 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) { } func listMonitorDidChange(_ monitor: ListMonitor) { 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) { 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? private var listMonitor: ListMonitor? { 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(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(.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 ) } } }