Swiftで遊ぼう! on Hatena

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

Swiftで遊ぼう! - 543 - Core Data シンプルチュートリアル

2017年3月25日:ここの内容は少し古いのでiOS 10向けに書き換えたCore Dataチュートリアルをみてください->Swiftで遊ぼう! - 582 - Core Dataチュートリアル:HitList - Swiftで遊ぼう! on Hatena

developer.apple.com

「Getting Started」の内容を勉強し終えたので、簡単なサンプルプロジェクトを作ってみます。

復習を兼ねて知識の地固めをするわけです。50オヤジは何度復習しても直ぐ手順を忘れてしまうんです。

Core Dataを使用してテキストデータを保存して、書き出して、消去するというシンプルなものです。検索機能や表示機能(テーブル表示)は実装されていません。これらは次の課題として置いておきます。

できあがりのイメージは次の通りです。

f:id:yataiblue:20151231135659j:plain

プロジェクトの初期設定

新しいプロジェクトを作る手順は基本事項です。「Swiftで遊ぼう! - 365 - Developing iOS Apps (Swift) Build a Basic UI」に書いてある「BAIC プロジェyクト初期設定」 をみて、「Product Name:」を「SimpleCoreDataSample」にします。

 重要なのは「Use Core Data」を必ず選択することです。これを選択すると必要なコードが自動的に組み込まれるんです。

この必要なコードというのは、「Core Data Stackの初期化」のことで、ここで説明しています。このCore Data Stackの初期化ステップが自動的に組み込まれる所はどこか?

この初期化ステップは、Core Data Programming Guide: What Is Core Data?で説明されてないんです。そのため実際の実装は次のチャプターを読まないと分からないでしょう。Core Data Programming Guide: Connecting the Model to Viewsを読めばある程度理解できるのですが、私は混乱しました。実は、プロジェクト作成時に「Use Core Data」を選択した場合、「Core Data Stackの初期化」ステップをコードする必要は無いのにコードを加えたためエラーになったんです。「Use Core Data」を選択した時は、AppDelegate.swiftファイルに用意されるんです。

AppDelegate.swiftを必ず確認したほうがいいでしょう。

AppDelegateで以下のインスタンス化がされているのでGetting Startedの内容をコードするとエラーになるんです。「Core Data Stackの初期化」で書かれているコードを加える場合は、「Use Core Data」にチェックを入れちゃいけないようです*1

  • managedObjectModel
  • persistentStoreCoordinator
  • managedObjectContext

まあどちらにしろ、Use Core Dataにチェックマークを入れるだけで、かなりコーディングの量が減るってことです。利用だけ考えるといいので楽になります。

次はUI用の部品を設置します。イメージ画像で説明しているように5つあり上から順番に説明します。

  1. UITextField:テキストの入力を受け付けます。
  2. UILabel:Core Dataに保存されているデータを表示させるラベルです。
  3. UIButton:1番上のボタンは「Write」で、テキストフィールドに入力されている文章をCore Dataを介してSQLiteのデーターベースに保存します。
  4. UIButton:このボタンは「Read」で、Core Dataに保存されているデータを読み込んでLabelに表示させます。
  5. UIButton:このボタンは「Erase」でCore Dataの情報を消去させます。

UIの部品をstoryboardに設置した時に重要なのはレイアウト調整です。デバイスの異なる画面サイズに合わせる方法ですが基本は押さえておきます。コードで調整する方法とXcodeで調整する方法がありますが、私のような素人はXcodeをフル利用して調整した方がいいでしょう。他にも色々と知らなければならない知識があるからです。下記の内容を理解します。

yataiblue.hatenablog.com

するとそれぞれのUI部品にコンストレイントを設定するのは簡単です。

そして、storyboardで設置したUI部品はインスタンス化されることが保証されていますが、コードで操作できません。昨日の記事でまとめた方法でViewControllerクラス内で「@IB」化プロパティとメソッドを作ります。

@IBOutlet weak var dataFiled: UITextField!
@IBOutlet weak var resultLabel: UILabel!
@IBAction func writeData(sender: AnyObject) { }
@IBAction func readData(sender: AnyObject) { }
@IBAction func eraseData(sender: AnyObject) { }

これで準備ができたので、Core Dataの準備に入ります。

まず何をすべきか?

保存すべきデータは、Xcode上で「エンティティ」、コードで「NSManagedObject」型で扱う必要があるので用意する必要があります。

このエンティティに3つの因子(これをどう言えばいいのか知りませんが...)「アトリビューション」、これはクラスで言えば、プロパティになります。そして「リレーションシップ」、これは他のオブジェクトが持っているプロパティへのリファレンス(ポイント)です。そして「フェッチド・プロパティ」はデーターベースからデータをフェッチするためのプロパティです。複数のエンティティが関連するため、その関係性を示したものが「シェーマ」と呼ばれ、それが「NSManagedObjectModel」というわけです。

このManagedObjectModelは、プロジェクト作成時に「Use Core Data」チェックマークをしていれば、「プロジェクト名 + .xcdatamodelid」という形でプロジェクト・ナビゲータに用意されます。このプロジェクトでは「SimpleDataCoreSample.xcdatamodelid」というファイルになります。その下のファイル「DataStore.swift」は新しくプロジェクトを立ち上げた時に存在しないので、無くても心配しないように。

f:id:yataiblue:20160104150520j:plain

このManagedObjectModelのファイルを選択すると、右側に2つのペインに分割されたCore Data Editorが現れます。左側が「ナビゲーターエリア」で右側が「エディタエリア」になります。プロジェクト立ち上げ時に空欄でDefaultのConfigurationsしか設定されていません。データを扱う必要があるので、下部の「Add Entity」を選択して、名前を「DataStore」に変更します。これがNSManagedObjectのサブクラスになります。次は2つの属性を追加します。エディタエリアの「+」ボタンを押して、「memo」と「date」を入れます。Typeは、それぞれ、「String」と「Date」です。ここで注意すべきことは、このTypeはSwiftの「String」では無く、「NSString」と「NSDate」になることです。利用時にキャスティングが必要になります。

f:id:yataiblue:20160104152954j:plain

このプロジェクトで「リレーションシップ」は設定しません。あくまでもシンプルにCore Dataを使って保存と読み出しの実装を勉強しているからです。

次は、DataStoreというエンティティをOOPプログラムで扱うためにクラス継承をしなければなりません。DataStoreというNSManagedObjectを継承したサブクラスを用意します。

メニューから、「Menu > File > File...」を選んで、「Source」から「Swift File」を選びます*2

ファイルの名前を「DataStore.swift」にします。そして、クラスのコードを以下のようにします。

import Foundation
import CoreData


class DataStore: NSManagedObject {

    @NSManaged var memo: String?
    @NSManaged var date: NSDate?

}

これでNSManagedObjectクラスの定義がされました。ここで概念的に重要なのが「@NSManaged」という枕詞です。データーベースのデータをOOPプログラミングのデータとして扱うための魔法の言葉です。これが無いと変換できないようです。

そして、このクラス定義をエンティティの設定で選ばなければなりません。

Cord Data Editorで「DataStore」エンティティを選択した状態で、「インスペクタ」を選択します。「Class」から先ほど作ったDataStore.swiftを選びます。

f:id:yataiblue:20160104155655j:plain

これでCore Dataで扱うData Modelの用意ができました。Core Dataの理解で重要なことを1ついいます。Core Dataはデーターベースじゃないってことです。ネット上の色々なところでCore Dataをデーターベースという説明をしていますが、それは初心者に勘違いをさせてしまうと思います。データベースの本質は「SQLite」ですが、この「SQLite」の存在をマスクさせてしまうほどシームレスにプログラミング言語と結びつけているフレームワークがCore Dataなんです。したがって、NSManagedObjectModelの設計は、データベースとコミュニケーションするためのステップのためデーターベース設計と同等ということになります。

今日はここまで。

        • -

SimpleCoreDataProjectという簡単なプロジェクトでCore Dataの復習をしています。

NSManagedObjectModel、データベースの「シェーマ」を用意しました。

次は、Core Data Stackの初期化ステップです。

私はこのステップでしばらく悩みました。プログラミングガイドで、スタックの初期化のために3つのオブジェクト生成のステップが説明されていたので、新たにこれを作らないといけないと思い込んでいました。

しかし、プロジェクト作成時に「Use Core Data」にマークを入れていると、自動的にCore Data Stackが生成されていたんです!

どこで?

それがAppDelegate.swiftファイルです。

よく見ると、Core Data Stackに必要な以下のプロパティが用意されています。

  1. Managed Object Context(NSManagedObjectContext)
  2. Persistent Store Coordinator(NSPersistentStoreCoordinator)
  3. Managed Object Model(NSManagedObjectModel)

これを起動させるために何をすべきか?

ViewControllerファイルでCore Data Stackを使いたいので、シングルトン・パターンのデリゲートプロパティをクラスインスタンス化(1つだけ作る)させます。

class ViewController: UIViewController {

 let appDelegate 
     = UIApplication.sharedApplication().delegate as! AppDelegate
 
 ...
 ...

これでCore Data Stackの初期化も準備ができました。

今日はここまで。

        • -

SimpleCoreDataProjectという簡単なプロジェクトでCore Dataの復習をしています。

今日からボタンメソッドの実装をします。まず「Write」ボタンで用意したwriteDataメソッドです。

@IBAction func writeData(sender: AnyObject) { }

このメソッドで何をするのか?

dataField(テキストフィールド)に入力された文字列を読み取って、Core Dataを使ってSQLiteにデータを書き込むのですが、「Core Dataを使って」という部分をもう少し詳しくみる必要があります。

Core Dataの中心的役割は、Core Data Stackです。その中でもManagedObjectContextがスケッチパッドの役割を担っていてデータの一時的な保存(undoやredoも実現できるようです)をして、永続的な保存としてSQLiteにデータを渡す作業をさせます。

ここから 紹介するコードは私が書いているのでヘンテコなコーディングが予想されます。いい方法があったら教えてください。

まず、dataFieldに入力があるかどうか確認します。ここで私の勘違いがありました。dataFieldのプロパティ「text」は、オプショナル型Stringです。「String?」ということなので、Swift2の常套手段、「guard let ~ else」を使えばいいのではないかと思いました。

@IBAction func writeData(sender: AnyObject) {
        
  guard let memo = dataFiled.text else {
   print("no text in the field")
   return
  }
 ....
 ....
// コードは続く

こんなコードを書いて確かめたのですが、テキストフィールドの入力をしなくてもguard内に入りません(T_T) どうしてなのか調べる方法があります。Xcodeに書いたコード上(Filed.textと書いてある部分)で[http://yataiblue.hatenablog.com/entry/2015/10/27/000000:title=Cmd + クリック」するか「Opt + クリック」]をします。

すると「text」のデフォルト値は「@""」という空欄だという事が分かります。ということは、guard文を使ってもエラーにならないんです。しかし、オプショナル型は外れるので「memo」は「String型」になります。中身は「""」で決して「nil」ではありません。

テキストフィールドが空欄の場合は保存させないようにするために次のステップをコードしました。

@IBAction func writeData(sender: AnyObject) {
        
 guard let memo = dataFiled.text else {
  print("no text in the field")
  return
 }
  
 if memo == "" {
  print("blank field")
 } else {
  writeCoreData(memo)
  // これは私が用意するカスタムメソッドです後で出てきます。
 }
}

本当は今日、自分で用意したカスタムメソッドwriteCoreData()のコードも書く予定でしたが長くなったのでまた明日(^_^;)

        • -


SimpleCoreDataProjectという簡単なプロジェクトでCore Dataの復習をしています。

「Write」ボタンのアクションメソッドの実装をしている途中でした。

ヘルパーメソッドとしてwriteCoreData()メソッドを書きます。

func writeCoreData(memo: String) {
       
 let appDelegate 
  = UIApplication.sharedApplication().delegate as! AppDelegate
 // CoreDataStackを初期化する重要なステップです。
 // もう一つ言えば、シングルトン・パターンです。
 // 理由を説明する必要はありませんね。
 let moc = appDelegate.managedObjectContext
 // CoreDataStackのインターフェイス部分は
 // managedObjectContextです。
 // スケッチパッドのように働きます。
 let dataFetch = NSFetchRequest(entityName: "DataStore")
 // NSPersistentStoreCoordinatorクラスがSQLiteへのデータの橋渡し
 // Requestを送ることでデータベースからデータを引っぱって来ます。
 dataFetch.returnsObjectsAsFaults = false
 // データーベースにデータが無ければ何も返しません。

 do {
  let results: Array = try moc.executeFetchRequest(dataFetch)
  // mocが持っている主要なメソッドです。
  // NSFetchRequestを与えることで、NSManagedObjectタイプのArray
  // を返します。当然、エラーを投げるthrowが組み込まれたAPIなので
  // 「do try catch」が必要です。
  if (results.count > 0) {
  // データベースに値があるかどうかの確認です。
  // countに数があればデーターベースに登録が
  // あるということになるので引っぱってきます。
   let object = results[0] as! NSManagedObject
   // Array型のcountがあるということはデータがるので
   // 1番最初の値(AnyObject)なのでNSManagedObjectに
   // キャストして取り出します。
   let text = object.valueForKey("memo") as! String
   // エンティティの属性を呼び出します。
   // memoはString型で、データベースに保存されていた
   // String型のデータをtextに取り出します(古いデータ)
   object.setValue(memo, forKey: "memo")
   // memoは「dataField.text」のデータで、
   // 新しいStringデータで上書きします。
   let oldDate = object.valueForKey("date") as! NSDate
   // 同様にNSDate型の古いタイムスタンプを取りだします。
   let date = NSDate()
   // 新しいタイムスタンプを作ります。
   object.setValue(date, forKey: "date")
   // 新しいタムスタンプデータで上書きします。
   print("Update \(text) to \(memo)")
   print("Update \(oldDate) to \(date)")
   // コンソールに古いデータと新しいデータを表示
   // 注意! NSData型で表示させているので
   // 日本のローカルタイムにはなっていません(^_^;)

   // ここまでのステップはスクラッチパッド上での作業。
   // undoやredoもできる一時保存状態です。

   do {
    try appDelegate.managedObjectContext.save()
   } catch {
     let nsError = error as NSError
     print("Saving failure : \(nsError.localizedDescription)")
   }
   // managedObjectContextを使ってデータの永続保存です。

  } else {
  // resultが空白だった場合、保存されたデータがなければ
  // 以下の新規データ保存のステップが動きます。
    let entity = NSEntityDescription.entityForName("DataStore",
               inManagedObjectContext: moc)
    let object = DataStore(entity: entity!, 
          insertIntoManagedObjectContext: moc)
    // 基本的にエンティティを作ってオブジェクトを登録
    // する必要があります。
    object.setValue(memo, forKey: "memo")
    let date = NSDate()
    object.setValue(date, forKey: "date")
               
    print("Insert \(memo)")
    print("Insert \(date)")

   do {
    try appDelegate.managedObjectContext.save()
   } catch {
    let nsError = error as NSError
    print("Saving failure: \(nsError.localizedDescription)")
   }
  }

 } catch {
 // このステップは基本的に空であろうが無かろうが
 // resultにnilが帰ってきた場合のエラーキャッチです。
  let nsError = error as NSError
  print("Fetch error: \(nsError.localizedDescription)")
 }
}

ダラダラと長ったらしいコードになりました。これでいいのかどうかも分かりません。
もっとすっきりコードできるような気もします。

NSDateの表示を日本のローカルタイムに変更するメソッドは「Read」ボタン用に作りました。
ということは、「Write」ボタンでも再利用できるってことですね。
後で考えてみます。

今日はここまで。

        • -

SimpleCoreDataProjectという簡単なプロジェクトでCore Dataの復習をしています。

「Write」ボタンをコードしたので、次は「Read」ボタンです。このボタンで作成したメソッドは次です。

@IBAction func readData(sender: AnyObject) {       
 readCoreData()
}

自分で作るカスタムメソッド「readCoreData()」を呼ぶだけにします。

このメソッドの特徴は、エンティティが持っている2つの属性の「型」が違うため、ラベルに表示するとき少し工夫が必要です。

func readCoreData() {
 let appDelegate: AppDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
 let moc = appDelegate.managedObjectContext
 let request = NSFetchRequest(entityName: "DataStore")
 request.returnsObjectsAsFaults = false
 // ここまでの手順は「Write」ボタンと同じです。

 do {
   let results = try moc.executeFetchRequest(request) as! [DataStore]
   let result = results[0]
   // 基本的に保存されているデータを1つだけ取り出します。
   // これはNSManagedObject型です。
   print("\(result.memo!)")
   // memoはStringです。
   guard let date = result.date else {
    print("No Date data is included")
    return
   }
   let stringDate = dateString(date)
   // 「date」はNSDate型なので、これをString型に
   // 変更する必要があります。
   // 「dateString()」メソッドは
   // 私が用意するヘルパーメソッドです。
   // このメソッドの下にコードします。
    print("\(stringDate)")
    resultLabel.text = "\(result.memo!)" + "\n" + "\(stringDate)"
 } catch {
   let nsError = error as NSError
   print("Fetal error is \(nsError)")
 }
}

データベース(SQLite)からデータを取り出す手順です。取りだしたNSDate型のデータをString型に変更するメソッドが次です。

func dateString(date: NSDate) -> String {
 let dateFormatter = NSDateFormatter()
 dateFormatter.locale = NSLocale(localeIdentifier: "ja_JP")
 dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
 let dateString: String = dateFormatter.stringFromDate(date)
 return dateString
}

これでdataFieldラベルに2行で表示することができます。

今日はここまで。

        • -

SimpleCoreDataProjectという簡単なプロジェクトでCore Dataの復習をしています。

今日は「Erase」ボタンを作って、データベースに保存しているデータを消去します。

@IBAction func eraseData(sender: AnyObject) {
  dataFiled.text = ""
  deleteData()
}

まずdataFieldの表示を消します。そして、私の作った「deleteData()」ヘルパーメソッドを呼びます。

func deleteData() {
        
let appDelegate: AppDelegate 
 = UIApplication.sharedApplication().delegate as! AppDelegate
let moc = appDelegate.managedObjectContext
let request = NSFetchRequest(entityName: "DataStore")
request.returnsObjectsAsFaults = false
        
 do {
  let results : Array 
   = try moc.executeFetchRequest(request) as! [DataStore]
  if (results.count > 0 ) {
   let count = results.count
   for i in 0 ..< count {
    let object = results[i] 
    let text = object.valueForKey("memo") as! String
    print("Delete \(text) and Time stamp")
    moc.deleteObject(object)
    appDelegate.saveContext()
   }
   resultLabel.text = ""
  }
 } catch let error as NSError {
  print("FETCH ERROR:\(error.localizedDescription)")
 }
}

データを消す作業は説明が要らないでしょう。

これでCore Dataのサンプルプロジェクトができました。当然これだけの知識では不十分です。テーブルビューで複数のデータを表示して検索機能を加えないとCore Dataプログラミングができるとは言えないでしょう。

Core Dataの理解に疲れたので、しばらく「詳解 Swift 改訂版」を読んでみます。

*1:しかし、まだこれを試していないので本当かどうかは分かりません(^^;

*2:慣れてくると、「Core Data」から「NSManagedObject.subclass」を選んでもいいでしょう。