先に言っておくとCoreDataModelは自分で定義したクラスのこと。記事中に部分的なソースコードを、記事末尾に現時点のclass CoreDataModelソースコードを載せておく。GitHub?まあ慌てるな。
配列操作とMOC操作を区別する
// Trackエンティティを定義していたとして、[Track]のインスタンスを作成
var tracks = CoreDataModel.fetchTracks(with: nil)
// tracksの処理としてindexPath.row番目の要素を削除
CoreDataModel.delete(track: tracks[indexPath.row])
tracks.remove(at: indexPath.row)
上記tracksの処理を2種類書いたが、これらは別物。上はMOC処理で、下は単なる配列処理。どちらも実行するか、間にもう一度fetch処理としてtracks = CoreDataModel.fetchTracks(with: nil)を入れないと、整合性がとれなくなる。
[Track]各要素のプロパティは?
「tracksのプロパティを編集する=MOC処理」である。配列操作のremove()はなぜ区別されるのだろう。
例:tracks.lazy.filter { $0.index > Int16(indexPath.row) }.forEach { $0.index -= 1 }
要は、tracks.indexというAttributeがindexPath.rowより大きい要素に対してのみforEachで操作「各要素.indexをマイナス1する」を実行する。これでMOC上はindexが変更したことになっている。これは昨日書いたようにCoreDataModel.save()を実行するまでDBへ保存されないことに注意する。
リストの並び替え
TableView上で並び替えたら、MOC上のデータも並び替わってほしい。
並び順を保持するindexをEntityのAttributeに追加
先出ししているが、DBは配列ではないので、本来は並び順をもたない。そこで並び順を保持するindexをEntityのAttributeに追加する。考え方はswift - CoreDataで並び順を保存したい - スタック・オーバーフローに倣った。

で、「Swift TableView 並び替え」でググったら出てくるfunc tableViewを使って、リスト上に表示させている配列としてのtracks: [Track]操作と、並び替わった配列の順番を基にindexを再定義する。
// 「Swift TableView 並び替え」でググったら出てくるfunc tableView
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let sourceIndexPathRow = sourceIndexPath.row
let destinationIndexPathRow = destinationIndexPath.row
let tmp = tracks[sourceIndexPathRow]
// まずはtracks配列を操作
tracks.remove(at: sourceIndexPathRow)
tracks.insert(tmp, at: destinationIndexPathRow)
tracksTableView.reloadData()
// MOC側のindexを更新する
var iFirst = sourceIndexPathRow
var iEnd = destinationIndexPathRow
if iFirst > iEnd {
iFirst = destinationIndexPathRow
iEnd = sourceIndexPathRow
}
for i in iFirst...iEnd {
tracks[i].index = Int16(i)
}
}
indexの更新があまりかっこよくないというか、実直な実装というか、もっとスマートに書けないものか……。ちなみに先述した配列操作とMOC操作の区別はここにも(見た目は分かりづらいが)生き残っており、あくまでこの時点では並び替わったtracksの順番に沿ってindexを再定義しただけで、MOC上の順番はまだ並び替わっていない。
そこで、[Track]をfetchする前に、いつもindex昇順にソートさせればよい。別にここはindexを再定義した直後に並び替え&Fetchをしてもよいが、実装的には慌てるこっちゃなかったのでそうしなかった。実装もスマートになるし。CoreDataModel.fetchTracks()の内部で、以下のようにsortDescriptorsを定義しておけば良い。
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "index", ascending: true)
]
リストの削除
並び替えと似たノリになる。
// 「Swift TableView 削除」でググったら出てくるfunc tableView
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// tracks.remove()は配列処理なので先にMOC処理が必要
CoreDataModel.delete(track: tracks[indexPath.row])
tracks.remove(at: indexPath.row)
tracksTableView.reloadData()
// MOCのindexを更新する
tracks.lazy.filter { $0.index > Int16(indexPath.row) }.forEach { $0.index -= 1 }
}
}
”先に”MOC処理が必要である。あくまで上記実装の場合であるが、順番を逆にすると
tracks[indexPath.row]が削除されるCoreDataModel.delete(track: tracks[indexPath.row])の引数に入るものは1で要素が1つずれたtracksに対して調べられる- TableView上の削除操作と、表示しているデータで削除されるものが一致しない
indexの更新は先述の通り、tracks.indexというAttributeがindexPath.rowより大きい要素に対してのみforEachで操作「各要素.indexをマイナス1する」を実行する。削除した要素より小さい要素番号に対してはindexを更新する意味がない。
MOC処理を確定するタイミングを操作する
まだ製作途中なので奇妙なUIだが、たとえばNavigation Controllerを使って画面を遷移させているとして、画面下の「確定」ボタンが押される前に左上のBackボタンやスワイプで親ビューに戻った場合はEdit等で行ったMOC処理結果を破棄したいとする。「確定」ボタンが押されるとMOC処理結果が確定される、つまりDBに処理結果を送信してもらう。また、それ以外の画面遷移ではMOC処理結果を保持しておいてほしい。

えんやこらと悩んだが、そのかいあって?実装はなかなかスマートにできたんじゃないだろうか。
上記画面がAlbumSettingViewControllerをCustom Classにしているとして、まず確定ボタンにはCoreDataModel.save()でMOC処理結果をDBに送るのと、確定することを明示するブーリアンisSavingTheEditedAlbumをtrueにする。
class AlbumSettingViewController: UIViewController {
var isSavingTheEditedAlbum = false
// 確定ボタン
@IBAction func saveAlbumButton(_ sender: Any) {
CoreDataModel.save() // 編集内容を確定する
isSavingTheEditedAlbum = true
// これでNavigation Controllerを使って親ビューから遷移していた画面をもどせる
_ = navigationController?.popViewController(animated: true)
}
}
親ビューに戻ることを検知するDelegate methodとして、NavigationBarの戻るボタンをハンドリングしたい - Qiitaを参考に以下を定義する。
AlbumSettingViewControllerクラス定義のviewDidLoad()内部でnavigationController?.delegate = selfを忘れない。
また、ここでは親ビューとしてViewControllerを設定しているが、このクラス定義でoverride func viewWillAppear()を実装している場合、ライフサイクル上ではviewWillAppear()が先に呼ばれてからこのDelegate methodが実行されることに注意する。
extension AlbumSettingViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
// 親ビューに遷移する際、アルバム内容確定ボタン経由でなければCoreDataの編集内容を保存せずに破棄する
if viewController is ViewController && !isSavingTheEditedAlbum {
CoreDataModel.rollback()
}
}
}
isSavingTheEditedAlbumでない場合、CoreDataModel.rollback()で変更を破棄する。CoreDataModel.delete()とか全部やったことが消えちゃう。
まとめ?
以上、現時点の独学理解と実装内容をもとに現状をまとめた。下はclass CoreDataModelのソースコードである。
class CoreDataModelとしてメソッドを作成してまとめているが
きちゃないのできれいにするコツが知りたい。
import UIKit
import CoreData
class CoreDataModel {
static func newTrack() -> Track {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { // TODO: いちいちsaveContext()でもう一度guardを書かずに済まないか? -
abort() // TODO: abort()でよい? -
}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "Track", in: managedContext)!
let track = Track(entity: entity, insertInto: managedContext)
return track
}
static func fetchTracks(with predicate: NSPredicate?) -> [Track] {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
abort()
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Track")
fetchRequest.predicate = predicate
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "index", ascending: true)
]
do {
let tracks = try managedContext.fetch(fetchRequest) as! [Track]
return tracks
} catch let error as NSError {
fatalError("Could not fetch. \(error), \(error.userInfo)")
}
}
static func save() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
abort()
}
appDelegate.saveContext()
}
static func rollback() {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
abort()
}
let managedContext = appDelegate.persistentContainer.viewContext
managedContext.rollback()
}
static func delete(track: Track) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
abort()
}
let managedContext = appDelegate.persistentContainer.viewContext
managedContext.delete(track) // delete()を呼んだ後もsaveContext()されるまでは確定されない
}
}