Swiftで遊ぼう! on Hatena

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

Swiftで遊ぼう! - 612 - CoreDataMyDemo

2018年6月12日:Swift 4.2でも動くようにしました。Xcode 10.0 beta版を使っているのでイメージは古いままですが、コードは書き換えています。

2018年6月12日追記:Core Dataは私のオリジナルアプリに必要な知識なんですが、Swift 2の頃は複雑で理解しがたく頓挫していました。しばらく、Core Dataの勉強を中断していたんですが。2016年にNSPersistentContainerが導入されて非常に分かりやすくなりました。ここで紹介している内容はユーザーインターフェイスを使わないで、データーベースの書き込みと読み込みをviewDidLoad()メソッドを使って確認しているものです。
f:id:yataiblue:20180612183955j:plain

iOS開発の中で、Core Dataが初心者にとって扱いにくいと思われる理由の1つに、覚えなければならない用語や手順が多いからだと思います。それでも慣れてしまえば便利に使えるんじゃないでしょうか。

UIを使わないシンプルなデモに取り組みます。

Single View Applicationで「CoreDataMyDemo」というプロジェクトを作ります。当然、「Use Core Data」のチェックマークを入れます!こうすることでNSManagedObjectStack*1を含むNSPersistentContainerのコードがAppDelegate*2ファイルに用意されます。

ここの「Use Core Data」のチェックマークを入れずCore Dataをプロジェクトに組み込む方法も知っておくべきですが、チェックマークを入れた時に用意されるコードをAppDelegateファイルで確認して理解できることが重要です。初心者はまず「慣れ」なければなりません。何度も同じようなチュートリアルをこなして使い方に慣れてしまう必要があります。

デモプロジェクトが立ち上がると、プロジェクト・ナビゲータに「CoreDataMyDemo.xcdatamodeld」ができます。

f:id:yataiblue:20160314172249j:plain

これはエンティティー(NSManagedObject)の関連図となるクラスNSManagedObjectModelです。

最初にすべきことは、エンティティを作る事です。エンティティとは、1つのテーブル(表)を意味します。「Add Entity」ボタンで新規の「Entity」を作成してダブルクリックで「Users」に変更します。空っぽのテーブルができたので、「行」を加えていきます。この行は変数のことで、「Attribute(アトリビュート)」と呼びます。「Add Atribute」ボタンを押して、「username」を加えてTypeを「String」に変更します。もう一度「Add Atribute」ボタンを押して、「password」を加えてTypeを「String」にします。

f:id:yataiblue:20160314173033j:plain

ManagedObjectModelで複数のエンティティの関連性と検索時の条件設定をすることもできますが、私はまだちんぷんかんぷんです。今後の課題にしておきます。

これでモデルの作成は終わりました。まだデータベースで扱う値を設定しただけなので、データベースを使用するための司令塔としてNSPersistentContainerを起動させる必要があります。

何度も言いますが、データベースはデモプロジェクトの外にドーンと構えています。データーベースは存在するだけで、それ自身何もしません。働きかけるクラスが存在するんです。それが「Core Data」です。

「Core Data」はアプリケーションとデータベースの間に存在して、アプリケーションで扱ったデータを永続的に保存したり外部のデーターベースとのやりとりもしたりできます。高度な内容は今後取り組んでいきます。

次はNSPersistentContainerを使用するのですが、このコードはAppDelegate.swiftにあり、AppDelegateで数々の機能を発揮できるので、ViewControllerから利用します。

この関係性ですが、Swiftで遊ぼう! - 260 - プロトコールとデリゲーション ProtocolsとDelegation - Swiftで遊ぼう! on Hatenaに書かれている「View」と「Controller」の関係性で考えると、AppDelegateがControllerにあたり、ViewControllerがViewになります。AppDelegateが処理を任された「デリゲート」クラスです。

AppDelegateの機能を利用するステップは、「6ステップ プロトコール・デリゲーション実装法」のステップ2とステップ3です。

ViewControllerクラスを選択します。このプロジェクトでUIを使いません。Core Dataの挙動だけを確認してくので、全てのコードをviewDidLoad()メソッド内で動かします*3

ViewConrollerファイルはそのままでCore Dataのクラスが使えません。「import CoreData」を加えることを忘れないように。

そして、viewDidLoad()メソッド内に次のコードを加えます。

override func viewDidLoad() {
  super.viewDidLoad()
        
  guard let appDelegate = 
    UIApplication.shared.delegate as? AppDelegate else {
      return
    }
  
  let context = 
    appDelegate.persistentContainer.viewContext
....

デリゲートの機能を使うためにViewControllerでデリゲートプロパティとしてインスタンス化させます。当然、重複を避けるためのシングルトン・パターンです。そして、NSPersistentContainerの主要コンテンツであるNSManagedObjectContexのインスタンスviewContextを使用します。これがデータの永続的書き込みとデーターベースから値の取り出しをします。

そのまま新しい値を加えるコードを書きます。

.....
let newValue = 
  NSEntityDescription.insertNewObject(forEntityName: "Users", into: context)
newValue.setValue("Yuji", forKey: "username")
newValue.setValue("123456", forKey: "password")
        
do {
    try context.save()
   } catch let error as NSError {
     print("Error: \(error)")
   }
.....

このコードが新しいデータを加えるステップです。重要なクラスメソッドを覚えなければなりません。

  • NSEntityDescription.insertNewObject(forEntityForName:, into:)
  • setValue(, forKey:)

この2つを覚えます。

もう1つNSManagedObject型の値を加える一般的な方法も覚えます。

.....
let entity  = 
  NSEntityDescription.entity(forEntityName: "Users", in: context)
let user = 
  NSManagedObject(entity: entity!, insertInto: context)
user.setValue("Genki", forKey: "username")
user.setValue("45678", forKey: "password")
        
do {
    try context.save()
   } catch let error as NSError {
     print("Error: \(error)")
   }
.....

空のエンティティを作って、その後に空のNSManagedObjectを作ってから値を代入...

コードミスしそうですね。コードミスをしないようにNSManagedObjectサブクラスを使うといいんですね。

今日からデータベースからデータを取りこむ時に重要なクラス、NSFetchRequestの使い方に慣れます。

しかし、このクラスのプロパティ、「 var returnsObjectsAsFaults: Bool 」が良くわからないのでアップルのドキュメントを読んで考えています。

returnsObjectsAsFaults

レシーバーを使ってフェッチされたオブジェクトに不具合があれば「YES」で、そじゃなければ「NO」になりますが、デフォルトは「YES」です。結果の型が「 NSManagedObjectIDResultType」ならこのプロパティは使用されません。オブジェクトのIDはプロパティの値を持っていないからです。得られたオブジェクトからそのプロパティにアクセスする必要があることが分かっているのであれば設定を「NO」にすることでパフォーマンスが改善するという利点があります。

デフォルトで、フェッチを実行するときにreturnsObjectsAsFaultsは「YES」です。Core Dataは、条件が一致するレコードのオブジェクトデータをフェッチして、ローキャッシュを情報で埋め、managed objectを欠陥として返します。これらの欠陥はmanaged objectsでが、すべてのプロパティは、欠陥が生じるまでローキャッシュに存在し続けます。欠陥が生じた時、Core Dataはローキャッシュからデータを抽出します。この操作によるオーバーヘッドは小さなものですが、データベースが巨大になれば、ちょっとした損失というわけにはならないでしょう。戻ってきたオブジェクトのプロパティにアクセスする必要があるのであれば(例えば、特定のアトリビュートの平均値を計算するために全てのオブジェクトの繰り返しをするような場合)、returnsObjectsAsFaultsを「NO」にして追加のオーバーヘッドを避けることが効果的です。

      • -

ちゃんと日本語に訳せません。訳せないと言うことは、理解できていないと言うことです。

悲しいけど、今の私の実力です(T_T)

新しい値を加えるために、NSEntityDescriptionクラスを使う方法を学びました。なかなか覚えられませんが...

次はデータベースからデータの抽出です。まずシンプルにエンティティを全て取りこむ方法です。

まずリクエストを作ります。これはNSFetchRequestクラスです。いわゆる条件になるので、このリクエストを使って抽出を実行すればデータが読み込まれるという流れです。

viewDidLoad()メソッドに付け加えます。

let request = 
  NSFetchRequest<NSFetchRequestResult>(entityName: "Users")
request.returnsObjectsAsFaults = false
        
do {
    let results = try context.fetch(request)
    print(results)
} catch let error as NSError {
    print("Error: \(error)")
}

データの抽出は、このようにNSFetchRequestを使って選んでからcontextのexecuteFetchRequestを使ってゲットするというやり方。これでコンソールにデータが表示されます。NSManagedObject型のデータを吐き出しているので、色々と要らない情報がついています。

f:id:yataiblue:20160314235737j:plain

ここでコードしたデータベースから欲しいデータの読み込みを確認のため「print(results)」としましたが、resultsは、[NSManagedObject]型なので、普通はそのまま使用することは無いです。アレー型なので複数存在することが予想されるため、「for」を使って個別の要素を取りだし、KVCを使って要素を抽出します。

let request = NSFetchRequest(entityName: "Users")
   request.returnsObjectsAsFaults = false
        
 do {
     let results = try context.executeFetchRequest(request)

     if results.count > 0 {
        for result in results as! [NSManagedObject] {
            print(result.value(forKey: "username")!)
            print(result.value(forKey: "password")!)
        }
    }

  } catch let error as NSError {
  print("Error: \(error)")
}

ここまでは、CoreDataを使ったデータベースへの書き込みとデータの読み込みの基本です。

データーベースからデータを読み出す時の条件は、NSFetchRequestクラスを使います。

昨日使っていたリクエストはエンティティ名だけの指定で読み取りましたが、読み取るデータを絞り込むときに使うのがNSFetchRequestのpredicateプロパティです。これはNSPredicateクラスです。少しアップルのドキュメントを読んで勉強します。

NSPredicate

NSPredicateクラスは、データをフェッチしたりメモリー内でフィルタリングするための検索を絞り込むために使われる論理条件を定義するときに使用されます。

論理条件を定義することができ、永続保存されたオブジェクトやメモリー内でオブジェクトをフィルタリングするために使われます。

ドキュメントを読んでも後は理解し難いので、イニシャライザーに使われているPredicateフォーマットをみます。

init(format predicateFormat: String,
argumentArray arguments: [AnyObject]?)

predicateFormatを記述するときに下のシンタックスを使います。

  • 「$」
  • 「% @」
  • 「% K」

これらの意味を何となく理解しました。本当に色々なルールがありますね。なんせ、「Predicate Programming Guide」が存在するぐらいですから、NSPredicateを使いこなすためにいったいどれだけ時間が必要か(T_T)

CoreDataMyDemoという非常にシンプルな練習プロジェクトをしています。データベースからNSManagedObjectを読むために、NSFetchRequestクラスを使います。読み込みの条件をプロパティである「predicate」で絞り込みをします。

前回までのコードでエンティティ名だけでデータを読み込むと次のような結果がコンソールに表示されます。

let request = NSFetchRequest(entityName: "Users")
request.returnsObjectsAsFaults = false

f:id:yataiblue:20160315180655j:plain

ここにpredicateを「Yuji」だけ絞り込む設定を加えます。

let request = NSFetchRequest(entityName: "Users")
request.returnsObjectsAsFaults = false

let predicate = NSPredicate(format: "username = %@", "Yuji")
request.predicate = predicate

すると検索の絞り込みができました!

f:id:yataiblue:20160315181000j:plain

今日はこれだけ。

ゆっくり進みます。

*1:以前はスタックをイメージする必要があったけどNSPersistentContainerが導入されてあまり意識する必要がなくなりました

*2:シングルトン・デザインパターンで使用したいクラスはここに記述するといいでしょう

*3:なぜ、viewDidLoad()メソッドを使うのか意味が分からなければ、アプリのライフサイクルを理解してからトライして下さい