Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 596 - Multiple Managed Object Contextsチュートリアル

2017年3月25日:これからiOS 10向けに改訂を開始します。

オリジナルアプリと似たアプリのチュートリアルを見つけました!

www.raywenderlich.com

このチュートリアルを理解して、オリジナルアプリに利用します。

嬉しい記述がありました。このチュートリアルが理解できたらiOS開発の中級者レベルらしいです(^^)/ 私が中級者? そんな馬鹿なですけど(^^;) まだオリジナルアプリが発表できないド素人です(T_T)

さて、外部ファイルからデータを読み込む復習をします。出ました「JSON」! 以前勉強をしたので自分の記事で復習です。

以前勉強したJSON関連の記事をまとめました。

yataiblue.hatenablog.com

今日はこれだけです。

オリジナルアプリに近いアプリのチュートリアに取り組んでいます。

www.raywenderlich.com

このチュートリアルは中級者向けなんで、既に知っていなければならない項目が多いんです。昨日は「JSON」の復習をしました。

今日はNSBundleの復習です。何度も繰り返して勉強しているのに覚えられないんですよね。

yataiblue.hatenablog.com

今日は読み直して少し記事に手を加えて終了です。

オリジナルアプリに近いアプリのチュートリアに取り組んでいます。

www.raywenderlich.com

まだまだ基本事項の復習です。外部ソースとデータのやり取りをするために知っていなければならないクラスは色々あります。

NSFileManagerも復習しましょう。

yataiblue.hatenablog.com

しっかり読み直して思い出しましょう。

オリジナルアプリに近いアプリのチュートリアに取り組んでいます。

www.raywenderlich.com

今回のチュートリアルは少し複雑なので、ある程度できあがっているスターター・プロジェクトが用意されています

ダウンロードしてプロジェクト・ナビゲータを開くと、数々のファイルが並んでいます。まず、CoreDataStack.swiftファイルと、関連するCoreDataSeedフォルダーに存在するファイルの関連性をみていきます。

f:id:yataiblue:20160229155357j:plain

まずCoreDataStack.swiftファイルのコードをみます。まず全体のコードを眺めてみても何となく理解できるような気がします。やっぱり完全な初心者から脱却できているのかもしれません。チュートリアルで注目している部分から取りかかります。

import CoreData

class CoreDataStack {
  
  let modelName = "SurfJournalModel"
  let seedName = "SurfJournalDatabase"
  
  lazy var applicationDocumentsDirectory: NSURL = {
    let urls = NSFileManager.defaultManager().URLsForDirectory(
      .DocumentDirectory, inDomains: .UserDomainMask)
    return urls[urls.count-1]
    }()
  
  lazy var managedObjectModel: NSManagedObjectModel = {
    let modelURL = NSBundle.mainBundle()
      .URLForResource(self.modelName,
        withExtension: "momd")!
    return NSManagedObjectModel(contentsOfURL: modelURL)!
    }()
  
  lazy var psc: NSPersistentStoreCoordinator = {
    let coordinator = NSPersistentStoreCoordinator(
      managedObjectModel: self.managedObjectModel)
    let url =
    self.applicationDocumentsDirectory
      .URLByAppendingPathComponent(self.seedName + ".sqlite")
    
    // 1
    let bundle = NSBundle.mainBundle()
    let seededDatabaseURL = bundle
      .URLForResource(self.seedName, withExtension: "sqlite")!

    // 2
    let didCopyDatabase: Bool
    do {
      try NSFileManager.defaultManager()
        .copyItemAtURL(seededDatabaseURL, toURL: url)
      didCopyDatabase = true
    } catch {
      didCopyDatabase = false
    }

    // 3
    if didCopyDatabase {
  
      // 4
      let seededSHMURL = bundle
        .URLForResource(self.seedName, withExtension: "sqlite-shm")!
      let shmURL = self.applicationDocumentsDirectory
        .URLByAppendingPathComponent(self.seedName + ".sqlite-shm")
      do {
        try NSFileManager.defaultManager()
          .copyItemAtURL(seededSHMURL, toURL: shmURL)
      } catch {
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
      }

      // 5
      let seededWALURL = bundle
        .URLForResource(self.seedName, withExtension: "sqlite-wal")!
      let walURL = self.applicationDocumentsDirectory
        .URLByAppendingPathComponent(self.seedName + ".sqlite-wal")
      do {
        try NSFileManager.defaultManager()
          .copyItemAtURL(seededWALURL, toURL: walURL)
      } catch {
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
      }
      
      print("Seeded Core Data")
    }
    
    // 6
    do {
      try coordinator.addPersistentStoreWithType(
        NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
    } catch {
      //7
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
    }
    
    return coordinator
    }()
  
  lazy var context: NSManagedObjectContext = {
    var managedObjectContext = NSManagedObjectContext(
      concurrencyType: .MainQueueConcurrencyType)
    managedObjectContext.persistentStoreCoordinator = self.psc
    return managedObjectContext
    }()
  
  func saveContext () {
    if context.hasChanges {
      do {
        try context.save()
      } catch {
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
      }
    }
  }
}

まずこの長ったらしいコードの中でナンバリングされている部分から取りかかります。

// 1
let bundle = NSBundle.mainBundle()
let seededDatabaseURL = bundle
  .URLForResource(self.seedName, withExtension: "sqlite")!
 
// 2
let didCopyDatabase: Bool
do {
  try NSFileManager.defaultManager()
    .copyItemAtURL(seededDatabaseURL, toURL: url)
  didCopyDatabase = true
} catch {
  didCopyDatabase = false
}
  1. 「NSBundle.mainBundle()」は、何度も何度も繰り返して勉強しているので、やっと自然に頭に入ってくるようになりました。まだの人はシングルトン・パターンのページを読みましょう。NSBundleクラスも復習したので、まだの人はこのページを確認しましょう。まずこのデータベースは、立ち上げた時に前もってデータを持たせるステップです。「URLForResource」メソッドは、プロジェクト内に存在するファイルの位置を「NSURL?」クラスで返します。パラメーターで、String型の名前と拡張子を受け取ります。ここで「self.seedName」になっていますが、コードの前半部分で定義されています(ハードコーティング)。「seededDatabaseURL」には、ファイルの位置情報がNSURLクラスとして保持されます。
  2. 「didCopyDatabase」はBoolean型の定数です。私がコーディングするなら、変数にしてしまいそうですが、ベテランプログラマーは、何の躊躇いもなく定数宣言しています。確かに、この定数に値を入れるタイミングは1度だけでいいので変数にする必要もありません。そして、NSFileManagerクラスが出てきます。これも復習したんですが、新しいメソッドが出てきています。「copyItemAtURL」は、2つの引数を取ります。どちらもNSURLクラスのオブジェクトを指定します。最初の引数はコピー元で、2番目の引数はコピー先です。コピー先にオブジェクトが存在すれば、このメソッドはerrorを投げる(throws)タイプなので、必ず「try」が要ります。コピーに成功すれば、定数「didCopyDatabase」を「true」にして、失敗すれば「false」にして後の処理に利用します。

copyItemAtURLメソッドの2つめの引数「url」を明日考察します。

ちょっと複雑なCore Dataチュートリアルを勉強してます。

www.raywenderlich.com

CoreDataStack.swifのコードを詳しくみています。

昨日の記事も確認する必要があります。NSFileManagerクラスの持っている「copyItemAtURL」メソッドのコピー先の定数「url」をコードしている部分をチェックします。

lazy var psc: NSPersistentStoreCoordinator = {
  let coordinator = NSPersistentStoreCoordinator(
    managedObjectModel: self.managedObjectModel)
  let url =
  self.applicationDocumentsDirectory
    .URLByAppendingPathComponent(self.seedName + ".sqlite")
 //
 // 昨日から開設しているコードは、この格納遅延型プロパティ
 // 「psc」のクロージャのコードです。
 // 最後に「}()」が出てきます。

ここで使用されている「lazy」はlazy stored propertyです。今までに何度か説明はしています。しかし、しっかり理解しているとは言いがたいため、「詳解 Swift 改訂版」を調べました。

lazy stored propertyは「遅延格納型プロパティ」と訳されています。今まで「レイジー」なんて直接読んでいましたが、萩原先生に敬意を表し「遅延格納型プロパティ」を使います。

遅延格納型プロパティ(lazy stored property)

  • 初期値に一般式も使えますが、遅延評価するための意味のある関数やメソッドを呼び出したり、クロージャで評価できます。
  • クラス定義と構造体定義の内部にしか定義できません。
  • インスタンス生成後に値が変化するため変数(var)として宣言します。
  • できるだけ型推論を使用せず明記します。

この教科書のいいところは、どういう場合に使用すればいいのかヒントを書いているところです。iOS開発のGUI部品(UIKit)を使う場合、画面に画像を表示する場合でも、インスタンス生成時に画像ファイルのパスだけ指定して、実際画面に表示させるまで画像データの読み込みを遅延させると起動時間を大幅に改善させられます。

細切れになりますが、今日はここまで。

少し複雑なプロジェクトになると理解に時間がかかる中年オヤジの理解力の悪さが露呈してます(^_^;)

ゆっくり下記のチュートリアルを続けています。

www.raywenderlich.com

昨日の続きです。遅延格納型プロパティ「psc」を定義しているクロージャの中をみます。

lazy var psc: NSPersistentStoreCoordinator = {
 let coordinator = NSPersistentStoreCoordinator(
   managedObjectModel: self.managedObjectModel)
 let url =
 self.applicationDocumentsDirectory
  .URLByAppendingPathComponent(self.seedName + ".sqlite")
// まだまだクロージャは続きます...

話は2日前に戻りますが、copyItemAtURLメソッドで指定したコピー先のNSURL型の「url」はここで宣言されています。

「self.applicationDocumentsDirectory」は... 「applicationDocumentsDirectory」もCoreDataStack.swiftが持っているプロパティなので全体を見回すと、コードの前半で宣言されていました。

lazy var applicationDocumentsDirectory: NSURL = {
  let urls = NSFileManager.defaultManager().URLsForDirectory(
   .DocumentDirectory, inDomains: .UserDomainMask)
  return urls[urls.count-1]
}()

このプロパティも遅延格納型プロパティで、型は「NSURL」です。Documentsディレクトリの位置情報(NSURL)を、NSFileManagerクラスのメソッド「URLsForDirectory()」を使って習得しています。下記のように「Documents」フォルダにデータベースを書き込む準備をしています。

f:id:yataiblue:20160302151402j:plain

defaultManager()の存在理由

私のような無学の初心者が疑問に思っていることがあります。上記のクロージャ内部のコードは以下の3パターンで記述することができるのですが、どのパターンがいいのかはっきり分かりません。私自身はパターン1だと思っていますが、チュートリアルはパターン2です。他に調べるとパターン3も散見します。

// パターン1
  lazy var applicationDocumentsDirectory: NSURL = {
    let fm = NSFileManager.defaultManager()
    let urls = fm.URLsForDirectory(
      .DocumentDirectory, inDomains: .UserDomainMask)
    return urls[urls.count-1]
    }()

// パターン2
  lazy var applicationDocumentsDirectory: NSURL = {
    let urls = NSFileManager.defaultManager().URLsForDirectory(
      .DocumentDirectory, inDomains: .UserDomainMask)
    return urls[urls.count-1]
    }()

// パターン3
  lazy var applicationDocumentsDirectory: NSURL = {
    let urls = NSFileManager().URLsForDirectory(
      .DocumentDirectory, inDomains: .UserDomainMask)
    return urls[urls.count-1]
    }()

ファイルの操作を司るため、シングルトン・パターンのインスタンスを1つだけ生成させるのが望ましいためパターン1が安全だと思います。しかし、何度も使い回す必要がないためパターン2でもいいんでしょう。パターン3も見うけますが、ディレクトリ情報を得るためだけならシングルトンのインスタンスを生成させる必要はないかもしれません。クラスプロパティのみ使用するというスタンスでいいんでしょうか? この3つのパターンが説明できる人がいらっしゃれば教えてくださいm(_ _)m

今日はここまで。

下記のチュートリアルを取り組んでいますがチュートリアルで説明されていないNSFileManagerクラスを使ったファイル操作の勉強をしています。

www.raywenderlich.com

昨日のdefaultManagerを使用する理由がまだはっきりしていませんが先に進みます。

NSFileManagerクラスはファイル操作、NSBundleクラスは存在するファイルの検索操作、のように考えていいかもしれません。この2つのクラスを使いこなして、アプリで管理するファイルを自由自在に扱えるようにします。

数日前の「チュートリアル4」でCoreDataStack.swiftのコードを載せましたが、その中にある1つの遅延格納型プロパティのNSPersistentSotoreCordinatorクラスの「psc」のみ注目します。

  lazy var psc: NSPersistentStoreCoordinator = {
    let coordinator = NSPersistentStoreCoordinator(
      managedObjectModel: self.managedObjectModel)
    let url =
    self.applicationDocumentsDirectory
      .URLByAppendingPathComponent(self.seedName + ".sqlite")
    
    // 1
    let bundle = NSBundle.mainBundle()
    let seededDatabaseURL = bundle
      .URLForResource(self.seedName, withExtension: "sqlite")!

    // 2
    let didCopyDatabase: Bool
    do {
      try NSFileManager.defaultManager()
        .copyItemAtURL(seededDatabaseURL, toURL: url)
      didCopyDatabase = true
    } catch {
      didCopyDatabase = false
    }

    // 3
    if didCopyDatabase {
  
      // 4
      let seededSHMURL = bundle
        .URLForResource(self.seedName, withExtension: "sqlite-shm")!
      let shmURL = self.applicationDocumentsDirectory
        .URLByAppendingPathComponent(self.seedName + ".sqlite-shm")
      do {
        try NSFileManager.defaultManager()
          .copyItemAtURL(seededSHMURL, toURL: shmURL)
      } catch {
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
      }

      // 5
      let seededWALURL = bundle
        .URLForResource(self.seedName, withExtension: "sqlite-wal")!
      let walURL = self.applicationDocumentsDirectory
        .URLByAppendingPathComponent(self.seedName + ".sqlite-wal")
      do {
        try NSFileManager.defaultManager()
          .copyItemAtURL(seededWALURL, toURL: walURL)
      } catch {
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
      }
      
      print("Seeded Core Data")
    }
    
    // 6
    do {
      try coordinator.addPersistentStoreWithType(
        NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
    } catch {
      //7
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
    }
    
    return coordinator
    }()

重要なことを思い出す必要があります。Swiftで遊ぼう! - 543 - Core Data シンプルチュートリアル - Swiftで遊ぼう! on Hatena」ここのチュートリアルに説明していますが、プロジェクト作成時に「Use Core Data」のチェックボックスを入れた場合、CoreDataStackはAppDelegateファイル上で初期化が生じるため、自前でCoreDataStackの初期化コードは要らないはずです。しかし、このスタータープロジェクトのAppDelegate.swiftファイルを見ると、CoreDataStackの初期化ステップがありません! という訳で、「Swiftで遊ぼう! - 530 - Core Data - Swiftで遊ぼう! on Hatena」で説明しているように、CoreDataStackクラスを用意しないといけません。私は一瞬勘違いしそうになりました。初心者の皆さん、注意が必要です。

定数「url」をもう一度注目します。

let url =
    self.applicationDocumentsDirectory
      .URLByAppendingPathComponent(self.seedName + ".sqlite")

「applicationDoucumentsDirectory」はDocumentsディレクトリの位置情報を保持する「NSURL」クラスのオブジェクトで、それが持つメソッド「URLByAppendingPathComponent()」を使って、ファイルのディレクトリ情報を「NSURL」クラスオブジェクトとして持たせています。

これが「//2」の「copyItemAtURL()」メソッドで指定されているコピー先です。このメソッドはerrorを投げる(thorws)が組み込まれているので、使用する場合「try」が必要になり、url指定先に既にファイルが存在しているとエラーになります。何もファイルが存在していなければ、メソッドは成功するので、「didCopyDatabase」に「true」が指定されます。

もう少し、この「copyItemAtURL()」メソッドを考えます。このコピー元のNSURLデータの「seededDatabaseURL」がどこから持ってくるのか理解している必要があります。「SurfJournalDatabase.sqlite」というファイルのNSURLオブジェクト情報は、NSFileManagerクラスメソッドではなく、NSBundleクラスメソッドを使って習得しています。

Xcodeに組み込んでいる「SurfJournalDatabase.sqlite」はNSBundleクラスを使ってアプリに取りこみます。
f:id:yataiblue:20160229155357j:plain

1度起動したアプリに存在する「SurfJournalDatabase.sqlite」はNSFileManagerを使って操作します。
f:id:yataiblue:20160302151402j:plain

やっと理解できました。

プロジェクト内にあるファイルをアプリに取りこんで、アプリ内にファイルを移動させる手順を学んだところです。

www.raywenderlich.com

プロジェクト作成時に「Use Core Data」のチェックマークを入れなかったら自前でCoreDataStackクラスを作る必要があることを昨日説明しました。CoreDataStackの主要プロパティは3つあります。

  1. ManagedObjectContext
  2. PersistentStoreCoordinator
  3. ManagedObjectModel

この中で、PersistentStoreCoordinatorに注目しています。

  lazy var psc: NSPersistentStoreCoordinator = {
    let coordinator = NSPersistentStoreCoordinator(
      managedObjectModel: self.managedObjectModel)
    let url =
    self.applicationDocumentsDirectory
      .URLByAppendingPathComponent(self.seedName + ".sqlite")
    
    // 1
    let bundle = NSBundle.mainBundle()
    let seededDatabaseURL = bundle
      .URLForResource(self.seedName, withExtension: "sqlite")!

    // 2
    let didCopyDatabase: Bool
    do {
      try NSFileManager.defaultManager()
        .copyItemAtURL(seededDatabaseURL, toURL: url)
      didCopyDatabase = true
    } catch {
      didCopyDatabase = false
    }

    // 3
    if didCopyDatabase {
  
      // 4
      let seededSHMURL = bundle
        .URLForResource(self.seedName, withExtension: "sqlite-shm")!
      let shmURL = self.applicationDocumentsDirectory
        .URLByAppendingPathComponent(self.seedName + ".sqlite-shm")
      do {
        try NSFileManager.defaultManager()
          .copyItemAtURL(seededSHMURL, toURL: shmURL)
      } catch {
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
      }

      // 5
      let seededWALURL = bundle
        .URLForResource(self.seedName, withExtension: "sqlite-wal")!
      let walURL = self.applicationDocumentsDirectory
        .URLByAppendingPathComponent(self.seedName + ".sqlite-wal")
      do {
        try NSFileManager.defaultManager()
          .copyItemAtURL(seededWALURL, toURL: walURL)
      } catch {
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
      }
      
      print("Seeded Core Data")
    }
    
    // 6
    do {
      try coordinator.addPersistentStoreWithType(
        NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
    } catch {
      //7
        let nserror = error as NSError
        print("Error: \(nserror.localizedDescription)")
        abort()
    }
    
    return coordinator
    }()

アプリを起動した時にDocumentsディレクトリに何もないのでdidCopyDatabaseは「true」になっていて、「//4」のif節が実行されます。

 // 4
 let seededSHMURL = bundle
    .URLForResource(self.seedName, withExtension: "sqlite-shm")!
 let shmURL = self.applicationDocumentsDirectory
    .URLByAppendingPathComponent(self.seedName + ".sqlite-shm")
 do {
   try NSFileManager.defaultManager()
     .copyItemAtURL(seededSHMURL, toURL: shmURL)
 } catch {
   let nserror = error as NSError
   print("Error: \(nserror.localizedDescription)")
   abort()
 }

ここのステップで何をしているのか?

Xcodeで用意している「SurfJournalDatabase.sqlite-shm」ファイルをアプリ内のDocumentsディレクトリ内にコピーして持ってきます。当然エラーを投げるメソッドを使用しているので失敗した時のためにエラーを「catch」させます。そして重要なのが「abort」を使っているところだそうです。

abort()は、強制的にアプリを終了させるコードです。むやみに使うとユーザーが戸惑ってしまうので多用してはいけませんが、こういう状況、データーベースをコピーすることができなかった場合、アプリを起動し続けることに意味のない状態に陥るようなことが分かっている場合、ログを残してアプリを終了させるのがデバックし易くなるということです。

 // 5
 let seededWALURL = bundle
    .URLForResource(self.seedName, withExtension: "sqlite-wal")!
 let walURL = self.applicationDocumentsDirectory
    .URLByAppendingPathComponent(self.seedName + ".sqlite-wal")
 do {
   try NSFileManager.defaultManager()
     .copyItemAtURL(seededWALURL, toURL: walURL)
 } catch {
   let nserror = error as NSError
   print("Error: \(nserror.localizedDescription)")
   abort()
 }

「// 5」のステップも同じステップです。Xcodeに存在する3つのファイルをアプリのDocumentsディレクトリにコピーするステップでした。

さて、ここで初心者の疑問が生まれます。

  • SurfJournalDatabase.sqlite
  • SurfJournalDatabase.sqlite-shm
  • SurfJournalDatabase.sqlite-wal

この3つのファイルの意味は?「shm」「wal」???

明日はこのファイルに関して勉強します。

データベースの3つのファイルをアプリのDocumentsディレクトリに作りました。

www.raywenderlich.com

この3つのファイルの存在で疑問に思いました。

これはSwift関連ではなく、データーベースのSQLiteに関わる内容でした。

「shm」はshared memory file、「wal」はwrite-ahead loggingのことです。最初はさっぱり分からなかったので少し調べて見ると下のような説明を見つけました。

nave-kazu.hatenablog.com

SQLiteにはジャーナルモードがあるようですが、2パターン存在します。このジャーナルモードというのもどういうモノかはっきり分かっていませんが、「delete」モードと「wal」モードが存在して、このチュートリアルで実装されているモードは「wal」モードのようです。皆さん、これをどう読んだらいいのでしょうか?「ウォール・モード」でいいんですか? ご存じの方がいらっしゃれば教えてください。

どちらにしろファイルを3つ用意しなければならないことが分かったので次に進みます。

SQLiteの勉強はもう少し後で取り組みます。

今日はこれだけ。

PersistentManagerCoordinator初期化最後のステップの説明に入ります。

www.raywenderlich.com

アプリを最初に立ち上げた時にDocumentsディレクトリに何もなければ、「didCopyDatabase」が「true」になり、Xcodeプロジェクト内の3つのファイルをコピーしてから、Xcodeのコンソールに"Seeded Core Data"が表示されます。

そして、「//6」と「//7」ののコードを見ます。

// 6
do {
  try coordinator.addPersistentStoreWithType(
    NSSQLiteStoreType, configuration: nil, URL: url, options: nil)
} catch {
  //7
    let nserror = error as NSError
    print("Error: \(nserror.localizedDescription)")
    abort()
}
    
return coordinator

定数「coordinator」はクロージャー内で宣言しているNSPersistentStoreCoordinator型のインスタンスで、実際はreturnされるlazyタイプの変数「psc」のことです。クロージャー内の最初に宣言されているコードをみます。

let coordinator = NSPersistentStoreCoordinator(
      managedObjectModel: self.managedObjectModel)

NSPersistentStoreCoordinatorクラスの初期化には、ManagedObjectModelクラスの情報が要ります。これはデータベースのシェーマにあたり保存形式を決定するために当然と言えば当然です。このManagedObjectModelに「self.」が付いているというのも、プロパティに「Lazy」を付けなければいけない理由の1つになります。

次は、CoreDataStackクラスの重要プロパティである「ManagedObjectModel」型プロパティ宣言をみてみます。

lazy var managedObjectModel: NSManagedObjectModel = {
 let modelURL = NSBundle.mainBundle()
  .URLForResource(self.modelName,
    withExtension: "momd")!
return NSManagedObjectModel(contentsOfURL: modelURL)!
}()

managedObjectModelもXcodeプロジェクトのファイルから取り込む必要があります。NSBundleクラスを使って、Xcodeプロジェクトで設定したManagedObjectModelをNSURLクラスにします。以前にも出てきましたがURLForRrsourecメソッド使っています。String型のファイル名を指定すればNSURLオブジェクトが習得できます。

f:id:yataiblue:20160304095527j:plain

また疑問が生まれました。このURLForResource()メソッドの引数「withExtension」ですが、"momd"を与えています。

ここでパラメータのextensionで疑問があります。Documentation and API Referenceには以下の説明があります。

If extension is an empty string or nil, the extension is assumed not to exist and the file URL is the first file encountered that exactly matches name.

extensionの指定が空かnilならnameと一致するファイルのNSURLオブジェクトを作成しますが、ここで「momd」が指定されています。Xcodeプロジェクトにあるファイルは「SurfJournalModel.xcdatamodeld」なんで、一致しないような気がしますが、「momd」を「xcdatamodeld」に変更するとランタイムエラーになります。どうしてでしょうか?

なぜかわかりませんが、これでmanagedObjectModelオブジェクトができます。

そして、定数coordinatorはこのオブジェクトを使ってプロパティ化されます。

もう一度「//6」のコードを見ます。

try coordinator.addPersistentStoreWithType(
  NSSQLiteStoreType, configuration: nil, URL: url, options: nil)

NSPersistentStoreCoordinatorクラスのメソッド、addPersistentStoreWithTypeが出てきました。そのまま読むと、Documentsディレクトリにある「SurfJournalDatabase.sqlite」をNSSQLiteStoreTypeにして保存する作業ですね。

まあこれでなんとかCoreDataStackの起動ができました。

NSPersistentStoreCoordinatorの働きはバックグラウンドでやらせます。

www.raywenderlich.com

以前マルチスレッドの話をしたときにちょっと説明しましたが、デフォルトで普通にコードを書くと全てメイン・キューで処理されます。時間のかかるデーターベースへのデータ書き込み、外部ファイルへの書き出し、UI(ユーザーインターフェイス)の反応もメインキューで動くと、外部ファイルへの書き出しのような時間のかかる作業をしている間、UIは無反応になってしまいます。これはアプリとして問題があります。

iOSの開発環境の王道を行けば、メイン・キュー以外のQOSとして別スレッドを立てることだと思います。しかし、Core Data managed object contextsは、アップルの用意しているマルチスレッドの仕組みに合わないようです。SQLiteはアップルの用意した仕組みではないため、歪みが生じるのでしょう。これを簡単に解決する方法は、もう一つmanaged object contextsを用意して、バックグラウンド・キューで動かしてやるという考え方です。

実装をする前に、「Export」ボタンの機能について考えます。

f:id:yataiblue:20160304154935j:plain

それは明日(^_^;)

今日はiOS内のデータを外部に書き出す機能について考えます。実はこの機能、私のアプリに必須です。

www.raywenderlich.com

Core Dataで重要なCoreDataStackのコードを見てきましたが、データの実体はManagedObject型です。ということでNSManagedObjectクラスを継承したクラスを用意する必要があります。このスタータープロジェクトをみると「JournalEntity.swift」というファイルがありました。

import Foundation
import CoreData

class JournalEntry: NSManagedObject {

  @NSManaged var date: NSDate?
  @NSManaged var height: String?
  @NSManaged var period: String?
  @NSManaged var wind: String?
  @NSManaged var location: String?
  @NSManaged var rating: NSNumber?
  
  func stringForDate() -> String {
    
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateStyle = NSDateFormatterStyle.ShortStyle
    if let date = date {
      return dateFormatter.stringFromDate(date)
    } else {
      return ""
    }
  }
  
  func csv() -> String {
    
    let coalescedHeight = height ?? ""
    let coalescedPeriod = period ?? ""
    let coalescedWind = wind ?? ""
    let coalescedLocation = location ?? ""
    var coalescedRating:String
    if let rating = rating?.intValue {
      coalescedRating = String(rating)
    } else {
      coalescedRating = ""
    }
    
    return "\(stringForDate()),\(coalescedHeight)," +
      "\(coalescedPeriod),\(coalescedWind)," +
        "\(coalescedLocation),\(coalescedRating)\n"
  }
}

「@NNManaged」枕詞の説明が必要な人は次のページを読むといいでしょう。
yataiblue.hatenablog.com

実はこのクラス宣言内に「CSV」形式*1のファイル書き出しのためのメソッドを用意しています。それが「csv()」です。

  func csv() -> String {
    
    let coalescedHeight = height ?? ""
    let coalescedPeriod = period ?? ""
    let coalescedWind = wind ?? ""
    let coalescedLocation = location ?? ""
    var coalescedRating:String
    if let rating = rating?.intValue {
      coalescedRating = String(rating)
    } else {
      coalescedRating = ""
    }
    
    return "\(stringForDate()),\(coalescedHeight)," +
      "\(coalescedPeriod),\(coalescedWind)," +
        "\(coalescedLocation),\(coalescedRating)\n"
  }

Entityはオプショナルで「nil」を取り得ますが、CSV形式にする時にnilは許されません。nilを空欄""に変更するCoalescing Operator*2を使います。この「csv()」メソッドを呼べばCSVファイルが作られますね。

今日はここまで。

CSVに書き出す準備をしていました。

www.raywenderlich.com

さて、次は「JournalViewController.swift」のコードを見ていきます...

...sigh

コードが長い...

......sigh

溜息しか出ません。今まで勉強してきたことを持ってすれば、コードを読み切ることができるのだろうか?

class JournalListViewController:
  UITableViewController, NSFetchedResultsControllerDelegate,
    JournalEntryDelegate {

このJournalViewControllerクラスは、UITableViewControllerを継承しています。ということは、テーブル表示のステップがかなり簡略化されているということです。必須メソッドを用意するぐらいでいいのでしょう。

そして、準拠しているプロトコールが2つ。「NSFetchedResultsControllerDelegate」と「JournalEntryDelegate」です。プロトコールとデリゲーションの関係性はもう大丈夫です。しかし、NSFetchedResultsControllerDelegateを使ったことがないので今回初めて勉強することになります。

そしてもう一つのプロトコール「JournalEntryDelegate」ですが、これはNSManagedObject継承クラスということで、NSManagedObjectクラスが持つプロトコールなんでしょう。すると、疑問が再び生まれました!

プロジェクト作成時に「Use Core Data」チェックマークに印を入れて作ったシンプルCore DataチュートリアルのViewControllerでは、このプロトコールの利用をしませんでした。Core Dataをカスタムに実装する場合はプロトコールに準拠させる必要があるようです*3

本当に頭の悪い初心者ですね。クラス宣言のところを理解するためにこれだけ悩んでます(^_^;)

今日はここまで。

長ったらしいJournalViewControllerのコードを見ています。

www.raywenderlich.com

クラス宣言のとろこで悩んでしまう人間なのでコードを理解するのは骨が折れます。

ここは素直にチュートリアルで説明されている部分に注目します。実は、データベースの内容を書き出すコードを勉強しているので 「// MARK: - Export」を探すと、コードの後半に4つのメソッドが並んでいます。

  1. activityIndicatorBarButtonItem()
  2. exportBarButtonItem()
  3. showExportFinishedAlertView(exportPath: String)
  4. exportCSVFile()

この中で「exportXCVFile()」を取りあげています。

  func exportCSVFile() {
    
    navigationItem.leftBarButtonItem =
      activityIndicatorBarButtonItem()
    
    // 1
    let results: [AnyObject]
    do {
      results = try coreDataStack.context.executeFetchRequest(
            self.surfJournalFetchRequest())
    } catch {
      let nserror = error as NSError
      print("ERROR: \(nserror)")
      results = []
    }

    // 2
    let exportFilePath =
      NSTemporaryDirectory() + "export.csv"
    let exportFileURL = NSURL(fileURLWithPath: exportFilePath)
    NSFileManager.defaultManager().createFileAtPath(
      exportFilePath, contents: NSData(), attributes: nil)
    
    // 3
    let fileHandle: NSFileHandle?
    do {
      fileHandle = try NSFileHandle(forWritingToURL: exportFileURL)
    } catch {
      let nserror = error as NSError
      print("ERROR: \(nserror)")
      fileHandle = nil
    }

    if let fileHandle = fileHandle {
      // 4
      for object in results {
        let journalEntry = object as! JournalEntry
        
        fileHandle.seekToEndOfFile()
        let csvData = journalEntry.csv().dataUsingEncoding(
          NSUTF8StringEncoding, allowLossyConversion: false)
        fileHandle.writeData(csvData!)
      }
      
      // 5
      fileHandle.closeFile()
  
      print("Export Path: \(exportFilePath)")
      self.navigationItem.leftBarButtonItem =
        self.exportBarButtonItem()
      self.showExportFinishedAlertView(exportFilePath)
    } else {
      self.navigationItem.leftBarButtonItem =
        self.exportBarButtonItem()
    }

  }

このメソッドだけでも複雑に見えてしまいます(T_T)

まず最初の「navigationItem.leftBarButtonItem = activityIndicatorBarButtonItem()」を考えます。実はNavigation Controllerの扱いに慣れていないので少々混乱することがあります。 この「navigationItem」というプロパティは「UINavigationItem」で左側のボタンがUIBarButtonItem型の「leftBarButtonItem」です。しかし、MainStroryboardのナビゲーションバーの左ボタンは「Export」になっていて、これはコード内に「@IBOutlet」付の「UIBarButtonItem型」のプロパティ「exportButton」になっています。

あれ?

実はNavigation Controllerは、VIew Controllerにメニューの「Editor」->「Embed In」->「Navigation Controller」で簡単に組み込むことができるように関連するプロパティも自動的に組み込まれます。したがって、アプリケーションが立ち上がる時にまず、navigationItemが起動して、その上にViewControllerが起動するのに併せてUIBarButoonItem型のexportButtonに切り替わるようです。

コード上のnavigationItemを「Opt + クリック」してみると次のような説明文が出てきます。

f:id:yataiblue:20160311184917j:plain

このNavigation Controller上のBarButtonItemにメソッド「activityIndicatorBarButtonItem()」を使ってBarButtonItemを加えています。このメソッドはカスタムメソッドです。

  func activityIndicatorBarButtonItem() -> UIBarButtonItem {
    let activityIndicator =
      UIActivityIndicatorView(activityIndicatorStyle:
        UIActivityIndicatorViewStyle.Gray)
    let barButtonItem =
      UIBarButtonItem(customView: activityIndicator)
    activityIndicator.startAnimating()
    
    return barButtonItem
  }

アクティビティ・インディケータの使い方は以前、スタンフォードのポール先生の授業にありました。

なんとも理解するのが難しい。中途半端ですが今日はここまで。

このチュートリアルに取り組んでいますが、本筋から外れた解説が続いています...

www.raywenderlich.com

チュートリアルに書かれていることを勉強しようとしても、スタータープロジェクトに組み込まれているコードが理解できない状態が続いています。NSFetichResultsControllerの使い方を知らないのに、このチュートリアルは知っていることを前提にしているので、悩みながら取り組んでいて疲れました。中途半端ですが中断して他のチュートリアルに取りこもうと思います。

今日はチュートリアル中断のお知らせでした。

では。

*1:CSV形式とは、文字列を「,」で区切ったリストです。表形式のデータの基本になります。

*2:この説明は次をみてください→Swiftで遊ぼう! - 285 - 備忘録 「a ? b : c」と「a ?? b」 - Swiftで遊ぼう! on Hatena

*3:もしくは、プロトコールも利用せず全てメソッドをカスタム実装するという選択肢もあるかもしれません。