Embedded FrameworksでCoreDataを使う（Swift4)

# まえおき
　表題ですが、まともに本とかも読まずにネットに転がっている情報だけでやってたので随分とまぁ、時間がかかりましたので、他の人のヒントになればいいなぁ、と思って足跡や個人的にアレっと思ったところのメモを残しておきます。
しかも、まだ理解したとは言い難いので、あれ？これは？おかしくない？？みたいなのがあると思います。そういったところを見つけたら、ご教示頂ければ幸いです。

# 目指すもの
　私の場合ですが、Today'sExtensionとアプリケーション本体で同じDBを参照したいという所でした。出来るだけコードは共有したい。

# 使うもの(使ったもの)、条件、キーワード
- swift4.2
- iOS10以降
- Xcode 10.1
- Today's Extension
- CoreData
- Embedded Framework
- AppGroup、Shared Container
- csvファイル

# 前提となるもの
- AppGroup、SharedContainerについて

　ExtensionのないアプリであればAppGroupを意識する必要は特に無いのですが（他のアプリと連携してるなら別）、Extensionは同じプロジェクトにいても、極端に言ってしまえば別アプリです。
　本来、アプリ内でDBを使うとすると、アプリ内部の保存領域にCoreDataStackを作って、そこでデータを読み書きするという形になりますが、前述したように、アプリとExtensionは別アプリなので、この保存領域も別々に存在しています。何も考えずにそのまま自動生成されるCoreDataのソースを使うと各々の保存領域にあるデータを使うことになるので、アプリで保存したデータをExtensionで読むことは出来ません。そこで登場するのがAppGroup。このAppGroupを使うよ、と指定するとそのグループで共有している保存領域(Shared Container)が使えるようになるわけです。CoreDataだけでなく、NSUserDefaultとかも共有できます。

AppGroupに関してはQiitaにも沢山説明してくださっている方がおりますので、そちらを参照していただければ、と思います。

[AppExtensionをCoreDataと連携させる(swift)]
(https://qiita.com/peromasamune/items/d7ea430aba1fc0f546dd)
など。


- Embedded Frameworks

　ソースを共有したいとなると、ライブラリとして使うという感じになると思うのですが、その仕組としてEmbedded Frameworksが準備されています。こちらも沢山説明してくださっている先達の方がいらっしゃいますので、そちらを。

[Embedded Frameworkを使ってiOSアプリを適当なレイヤーごとに分割する](https://masamichi.me/development/2018/05/06/embedded-framework.html)
など。

私はまだそこまで綺麗に分けて書いてないんですけど、こちらではCoreDataのEntityがあるxcdatamodeldなんかもFrameworksに移行してますね。


- アプリケーションの設計として

　結構悩んだ所なんですが、初期データをどう保持しておくか、という問題がありました。みんなどうしているんだろう、というのが会社に属さずに仕事していると聞ける人がおらず、さっぱりわからないので一生懸命ぐぐりました。Appleでもこういうふうにしたらいいよ、という指針はよくよく調べていくと出してくれているようなんですが、今現在日本語ドキュメントがなさすぎて英語だと今一頭にすっと入って来なくて苦労しました（苦笑）。

　幾つか出てきたのですが、本当に少ない場合はPlistに保持しておくというもの（今は非推奨っぽいです。）や、少数であればファイルに保持しておく（ファイルから読み込むのはオーバーヘッドが大きいからとか否定されてたような気も）、ある程度の件数があれば別プロジェクトでデータだけ作成して、そのデータをリソースファイルとしてバンドルして初回起動時に移動する、など。
　少数とかある程度とか、具体的にどれくらいなのかさっぱりわからねえ！とか素で思いました。まぁ、結局私の場合は一年分のデータでそのテキスト量も大したことがないのでCSVファイルをプロジェクトにバンドルすることにしましたけども。


- csv

　エクセルに染まりすぎなような気もするんですが、昔は文字列はダブルクォーテーションで囲っていたような。囲わなくて良いみたい。


# CoreDataのラッパークラスを作る
- コンテナを持つラッパークラスを作ります。

基本はAppDelegateからCoreDataに関するところを抜き出した感じです。
それ以外のクラスについては後述。


```swift:DataController.swift

import Foundation
import CoreData
import UIKit

class DataController: NSObject {
    var persistentContainer: NSPersistentContainer!
    
    public func getContainer() -> NSPersistentContainer {
        return persistentContainer
    }
    
    init(completionClosure: @escaping () -> ()) {
        // SharedContainerの場所を見る
        let storeUrl =  FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.〜AppGroupのidentifierをここに")!.appendingPathComponent("Sqliteのファイル名（拡張子まで）をここに")
        // 詳細の設定
        let description = NSPersistentStoreDescription()
        description.shouldInferMappingModelAutomatically = true
        description.shouldMigrateStoreAutomatically = true
        description.url = storeUrl
        
        // コンテナの生成xcdatamodeldファイルの名前にする
        persistentContainer = NSPersistentContainer(name: "〜entityのある.xcdatamodeld")
        // descriptionの設定
        persistentContainer.persistentStoreDescriptions = [NSPersistentStoreDescription(url: storeUrl)]
        // ストアのロード
        persistentContainer.loadPersistentStores() { (description, error) in
            if let error = error {
                fatalError("Failed to load Core Data stack: \(error)")
            }
            completionClosure()
        }
        // レコード登録されているか調べてないなら登録する
        // sqliteのファイルが有るならCSVから読み込んで登録
        // ストアをロードしているから、基本的にファイルはあるはずだけど
        // 初期登録が要らないならここ以下は不要
        if FileManager.default.fileExists(atPath: (storeUrl.path)) {
            let readText = readData()
            let count = readText.numberOfData(managedObjectContext: persistentContainer.viewContext)
            if count == 0 {
                readText.initialData(managedContext: persistentContainer.viewContext)
            }
        }

    }
    
    func saveContext() {
        let context = persistentContainer.viewContext
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                // Replace this implementation with code to handle the error appropriately.
                // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
                let nserror = error as NSError
                fatalError("Unresolved error \(nserror), \(nserror.userInfo)")
            }
        }
    }
}

// Date型のextension
// カレンダーにグレゴリオ暦設定、後は端末の現在の設定を代入（タイムゾーン、位置）
extension Date {
    
    var calendar: Calendar {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = .current
        calendar.locale   = .current
        return calendar
    }
}



// データ取得用クラス
public class readData{
    
    //data
    var tData:(iDay:Int16, iMonth:Int16, sText:String)
    
    // data取得関数
    // return: tData:DBから取得した指定日のデータ
    // 現在の日付を使用する
    public func getData(managedContext: NSManagedObjectContext) -> (iDay:Int16, iMonth:Int16, sText:String){
        // 現在日時
        let date = Date()
        // 月日をそれぞれ個別に取得
        let calendar = Calendar.current
        let iCurrentMonth:Int16 = Int16(calendar.component(.month, from: date))
        let iCurrentDay:Int16 = Int16(calendar.component(.day, from: date))
        // data読込
        let nsData = readTextData(managedContext: managedContext)
        
        if nsData.count == 1 {
            // data設定
            tData.sText = nsData[0].value(forKey: "Text") as! String
        } else {
            tData.sFlower = "えらーだったよー"
        }
        return tData
    }

    // data初期化
    init(){
        // init
        tData.iDay = 1
        tData.iMonth = 1
        tData.sText = ""
    }
    
    private func readData(managedContext: NSManagedObjectContext) -> [NSManagedObject]{
        
        // 検索条件の設定
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: constFDENTITY)
        // 一件だけ取得
        fetchRequest.fetchLimit = 1
        // 月、日をANDで指定
        let predicate = NSPredicate(format: "month = %ld AND day = %ld", tData.iMonth, tData.iDay)
        fetchRequest.predicate = predicate
        
        // 検索結果の配列を ManagedObjectContext から取得
        var fetchResults: [NSManagedObject] = []
        do {
            fetchResults = try managedContext.fetch(fetchRequest) as! [NSManagedObject]
        }catch{
            print("fetch error")
        }
        // NSManagedObjectの配列を返します
        return fetchResults
    }
    
    // 初期データをCoreDataStackに登録。
    public func initialData(managedContext: NSManagedObjectContext){
        // リソースにある初期化用マスターデータのパスを取得
        // マスターデータのファイルURL作成
        guard let mstDataUrl = Bundle.main.url(forResource: "マスターデータのファイル名（拡張子抜き）", withExtension: "拡張子") else {return}
        // バンドルしたリソースにマスターデータファイルがある時だけ実行する
        if !(FileManager.default.fileExists(atPath: mstDataUrl.path)){
            return
        }
        //csvファイルを格納するための配列を作成
        var csvArray:[String] = []
        //csvファイルの読み込み
        let csvBundle = Bundle.main.path(forResource: "ファイル名", ofType: "csv")
        
        var tsvData:String
        //csvBundleのパスを読み込み、UTF8に文字コード変換して、NSStringに格納
        do {
            tsvData = try String(contentsOfFile: csvBundle!,
                                 encoding: String.Encoding.utf8)
        }
        catch let error as NSError {
            print(error)
            return
        }
        
        //改行コードが\n一つになるようにします
        var lineChange = tsvData.replacingOccurrences(of: "\r", with: "\n")
        lineChange = lineChange.replacingOccurrences(of: "\n\n", with: "\n")
        //"\n"の改行コードで区切って、配列csvArrayに格納する
        csvArray = lineChange.components(separatedBy: "\n")
        
        // 一行ずつ処理
        for strCSVLine in csvArray {
            // ","で分割
            let strLineArray:[String] = strCSVLine.components(separatedBy: ",")
            
            // エンティティのオブジェクト生成
            let entity = NSEntityDescription.entity(forEntityName: "エンティティの名前", in: managedContext)
            let dData = NSManagedObject(entity:entity!,insertInto:managedContext)
            // オブジェクトのデータ設定
            dData.setValue(Int16(strLineArray[0])!, forKey: "day")
            dData.setValue(Int16(strLineArray[1])!, forKey: "month")
            dData.setValue(strLineArray[2], forKey: "text")
            // 保存
            do{
                try managedContext.save()
            }catch{
                print(error)
            }
        }
    }
    // DBの件数を返します。
    public func numberOfData(managedObjectContext: NSManagedObjectContext) -> Int {
        // FetchRequestを生成
        let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "エンティティの名前")
        fetchRequest.includesSubentities = false
        
        // エラーとカウント値を初期化
        var error: NSError? = nil;
        var count = 0;
        // 条件なしで全体の件数を取得
        do{
            count = try managedObjectContext.count(for: fetchRequest)
        }catch let error1 as NSError{
            error = error1
        }catch{
            fatalError()
        }
        // エラーが発生してたら戻すカウントとして0を与え、そうでない時は取得したCount(件数）を返します
        return error == nil ? count : 0
    }
}
```

- データの読み書き

DataControllerの下で作っているクラスで読み書きしています（とはいえ、基本的には準備されているのを読むだけのクラスで、書くのは初期データのみです。）。DataControllerに含めてしまうのが本当なのかも知れませんけど、今回自分で習作を作ったときによくわからないものを試行錯誤しながら作った結果こんな感じになってます。次作る時は一緒にしちゃうかも。


# 使い方


```swift:Todayviewcontroller.swift

class TodayViewController: UIViewController, NCWidgetProviding {
    
    @IBOutlet weak var LabName: UILabel!
    // ここで生成するのはあまり良くない気もしますがここで生成
    let dataController = DataController(){}    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view from its nib.
        let readText = readData()
        // data読込
        let tapData = readText.getData(managedContext: dataController.getContainer().viewContext)

        LabName.text = tapData.sText
    }
```

Extensionじゃない方のアプリでも全く同じようにして使えます。


[Core DataをSwift 4で使う (iOS 10以降)](https://qiita.com/da1ssk/items/3b1b9c11106717a5a935)
とても参考にさせていただきました。