ボタンを押したらCSVファイルを読み込んでCoreDataに保存する。

alt

試しで作ったCSVファイルはこれ。1行目で属性名を説明代わりに入れており、最後の行はXcodeでcsvファイルを作成すると自動的に改行される。

index,title
0,A1
1,A2
2,A3

実装の流れ(アルバムの場合)

  1. CoreData上のデータを削除
  2. Bundleを使ってcsvファイルのパスを取得し、まず文字列としてcsvファイルを読み込む
  3. 改行位置で分割した配列を作成
  4. 先頭と最後は余計なデータが付いているので削除
  5. 新しいデータをCoreData上に作って、属性(Attributes)に読み取ったデータを型変換しながら代入

個人的にはあまりスマートではない印象。属性を配列の要素としてひとまず扱っているから、属性にデータを代入する際、要素番号で指定しているのって見た目ダサない?これだとCSVファイルの内容が間違っているときの対応を書くのも面倒そうだし。構造体でも使うのかねぇ……「たった2日」のLesson day 2-4でJSONファイルを読み込んでいたが、あんな感じかな、と思いつつ、とりあえず実装できているんだから賢くコーディングするのは後回し。

アルバムとトラックの2つを扱っているけど、トラックはほぼアルバムそのままなのでいったん省略して、アルバムの処理について以下にコードを載せておく。こんなにstaticを使っているとアホそうな実装だと思うのはおれだけ?笑

下の中身を多少?詳しく説明していたりそもそもCoreDataの扱いを時分なりに理解した話は

CoreData:データの更新について(Xcode12.4、Swift 5.3.2)(April 21, 2021) と、

CoreDataをTableViewで扱う(リストの並び替え、削除との連携について)(Xcode12.4、Swift 5.3.2)(April 22, 2021) に書いていたりする。

参考資料は

[Swift]CSVを読み込みRealmに保存してみる | RE:ENGINES

SwiftでCSVデータを読み込んでコンソールに出力 - Qiita

import UIKit
import CoreData

class CoreDataModel {
    static func loadCSV() {
        loadAlbumsCSV()
        loadTracksCSV()
    }

    static func loadAlbumsCSV() {
        guard let path = Bundle.main.path(forResource:"Albums", ofType:"csv") else {
            print("Failed to find Albums.csv.")
            return
        }

        do {
            let csv = try String(contentsOfFile: path, encoding: String.Encoding.utf8)
            var groupedAttributesOfAlbums = csv.components(separatedBy: .newlines)

            // 余計なデータを削除
            groupedAttributesOfAlbums.removeFirst() // 先頭行のラベル
            if groupedAttributesOfAlbums.last == "" { // 末尾の空要素(csv末尾に改行のみあれば)
                groupedAttributesOfAlbums.removeLast()
            }

            // 旧データを削除
            let oldAlbums = fetchAlbums(with: nil)
            oldAlbums.forEach {delete(album: $0)}

            for groupedAttributesOfAlbum in groupedAttributesOfAlbums {
                let attributesOfAlbum = groupedAttributesOfAlbum.components(separatedBy: ",")
                let album = newAlbum()

                album.index = Int16(attributesOfAlbum[0])!
                album.title = attributesOfAlbum[1]
            }

            save()
        } catch let error as NSError {
            print("Failed to load csv: \(error).")
            return
        }
    }

    static func newAlbum() -> Album {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            abort()
        }

        let managedContext = appDelegate.persistentContainer.viewContext
        let entity = NSEntityDescription.entity(forEntityName: "Album", in: managedContext)!
        let album = Album(entity: entity, insertInto: managedContext)

        return album
    }

    static func fetchAlbums(with predicate: NSPredicate?) -> [Album] {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            abort()
        }

        let managedContext = appDelegate.persistentContainer.viewContext
        let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: "Album")
        fetchRequest.predicate = predicate
        fetchRequest.sortDescriptors = [
            NSSortDescriptor(key: "index", ascending: true)
        ]
        fetchRequest.includesSubentities = false

        do {
            let albums = try managedContext.fetch(fetchRequest) as! [Album]
            return albums
        } catch let error as NSError {
            fatalError("Could not fetch albums. \(error), \(error.userInfo)")
        }
    }

    static func save() {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            abort()
        }

        appDelegate.saveContext()
    }

    static func delete(album: Album) {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            abort()
        }

        let managedContext = appDelegate.persistentContainer.viewContext
        managedContext.delete(album)
    }
}

補足:省略したトラックの扱い

だいたいアルバムの処理と同じだが、特定のアルバムとリレーションをもたせた上で作成したいので、

  1. CSVファイルにはアルバムのindexを属性として含めておき、
  2. 上記let album = newAlbum()部分に該当するところより前でlet albums = fetchAlbums(with: nil)として読み込んだアルバムデータの配列を持っておき、
  3. let track = newTrack(into: albums[Int(attributesOfTrack[ここはリレーションをもたせたいアルバムのindexを格納している要素番号])!])で狙ったアルバムのトラックとしてデータを作成する
  4. あとは属性を代入