先に言っておくと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で並び順を保存したい - スタック・オーバーフローに倣った。

alt

で、「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処理が必要である。あくまで上記実装の場合であるが、順番を逆にすると

  1. tracks[indexPath.row]が削除される
  2. CoreDataModel.delete(track: tracks[indexPath.row]) の引数に入るものは1で要素が1つずれたtracksに対して調べられる
  3. TableView上の削除操作と、表示しているデータで削除されるものが一致しない

indexの更新は先述の通り、tracks.indexというAttributeがindexPath.rowより大きい要素に対してのみforEachで操作「各要素.indexをマイナス1する」を実行する。削除した要素より小さい要素番号に対してはindexを更新する意味がない。

MOC処理を確定するタイミングを操作する

まだ製作途中なので奇妙なUIだが、たとえばNavigation Controllerを使って画面を遷移させているとして、画面下の「確定」ボタンが押される前に左上のBackボタンやスワイプで親ビューに戻った場合はEdit等で行ったMOC処理結果を破棄したいとする。「確定」ボタンが押されるとMOC処理結果が確定される、つまりDBに処理結果を送信してもらう。また、それ以外の画面遷移ではMOC処理結果を保持しておいてほしい。

alt

えんやこらと悩んだが、そのかいあって?実装はなかなかスマートにできたんじゃないだろうか。

上記画面がAlbumSettingViewControllerをCustom Classにしているとして、まず確定ボタンにはCoreDataModel.save()でMOC処理結果をDBに送るのと、確定することを明示するブーリアンisSavingTheEditedAlbumtrueにする。

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()されるまでは確定されない
    }
}