共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話の続きです。Swiftコードの紹介を続けます。
取り上げるのは、モデルクラス内のデータをファイルへ保存する機能に関係したメソッドです。以前の投稿で紹介した、ファイルの読み書きを間接参照にするクラスを、そのまま利用します。
ファイルとして保存する場合、どのような形にすべきかを最初に考えます。
まずは、全体像です。データは4つの配列に入っていますが、これを1つにまとめて、1ファイルとして保存するのがベストです。4つよりも1つのほうが、ファイル管理の面で扱いやすいですから。
クラス内のデータは、データ型ごとに4つの配列に分かれています。これを1つのファイルにまとめる場合、どのように結合すれば良いのでしょうか。もとが配列ですから、結合されたデータも配列が良いでしょう。つまり、4つの配列を1つの配列に変換するわけです。
配列では、データの並び順が大事です。1つの考え方ですが、保存されたファイルの形式も、ある程度は意味のある形が良いのではと思います。その場合、全体がレコード順に並んでいるのが、意味のある形の1つでしょう。タブ区切りのテキストと同じ形です。これ以外を思いつかなかったので、レコード順と決めました。
ファイルの保存するためには、保存手段のあるデータ型が必要です。配列として1つにまとめるとなると、NSArrayしかありません。データを結合するのに加工処理が必要ですから、サブクラスのNSMutableArrayを使うことになります。
ファイルとして保存する範囲ですが、項目名やデータ型定義を含みません。これらも含めて保存し、その後にロードしてインスタンス内を書き換えると、データ型の構成や項目名がまったく違うインスタンスになってしまいます。そうなってはアプリが困るでしょう。インスタンス内を書き換えるのは、あくまでデータ部分であって、項目名とデータ型定義は固定したままです。
では、具体的なSwiftコードを見ていきましょう。ファイル関連メソッドの先頭部分です。
// ======================================== ファイル保存
// 項目名やデータ型定義は含まない。1つの配列にまとめて、1つのファイルとして保存する。
// 保存ファイルの形式:2次元の配列で、1次元目はレコード、2次元目にレコード内の項目が入る。
// (こんな感じ:[[項目1,項目2,項目3,項目4],[項目1,項目2,項目3,項目4], ... ,[項目1,項目2,項目3,項目4]])
var cLink2Storage: ZSLAry2Storage!
// 保存先の登録
func setStorageF(rStorage:ZSLAry2Storage) {
cLink2Storage = rStorage
}
先頭のコメント行では、どんな形式で保存するのかを簡単に説明しています。処理内容を理解するのに役立ちます。
続いて、保存先を登録する変数です。NSArrayをそのまま保存するための、ファイルへのリンククラスを指定します。以前に作ったクラスのうち、NSArrayを直接渡せるクラスZSLAry2Storageを使いました。
保存先を登録するメソッドは、受け取ったインスタンスを、そのまま変数に入れているだけです。リンククラスのインスタンスは、アプリ側が生成して、モデルクラスに渡すのが使う際のルールです。
続いて、データを保存先に書き出すメソッドです。NSArrey形式の保存データを生成した後、登録してある保存先へ渡します。
// データを保存先へ書き出す
func save2StorageF() -> Bool {
// 保存先が未登録ならエラー
if cLink2Storage == nil { zSendErrMsgF("ERROR:ZMR_SV:保存先が未登録"); return false }
// 出力用に変数を用意
var iNSAryOut: NSMutableArray = NSMutableArray()
// 保存用データの生成
for i in 0..<cRecCount { // i=RecIdx
var iNSA: NSMutableArray = NSMutableArray()
for j in 0..<cFldCount { // j=FldIdx
let iDataIdx: Int = cAryDataIdx[j]
switch cAryType[j] {
case .ZmInt :
let iInt: Int = cAryDataInt[iDataIdx][i]
iNSA.addObject(iInt)
case .ZmFloat :
let iFloat: Float = cAryDataFloat[iDataIdx][i]
iNSA.addObject(iFloat)
case .ZmBool :
let iBool: Bool = cAryDataBool[iDataIdx][i]
iNSA.addObject(iBool)
case .ZmStr :
let iStr: String = cAryDataStr[iDataIdx][i]
iNSA.addObject(iStr)
}
}
iNSAryOut.addObject(iNSA)
}
// 保存用データを渡して、保存してもらう
let iResult: Bool = cLink2Storage.saveF(iNSAryOut)
if iResult == false { zSendErrMsgF("ERROR:ZMR_SV:保存先への書き出しが失敗"); return false }
return true
}
一番最初に、保存先となるリンククラスが登録されているか調べています。未登録ならエラーメッセージを出して終了します。
データ生成の処理では、レコード順に処理しています。空のNSMutableArray(全体用)を用意して、そこへレコード単位で追加する形です。
レコード内の処理では、まず空のNSMutableArray(前とは別なもので、レコード1件分用)を用意して、それに項目を順番に追加します。項目は、それぞれのデータ型に合わせて取り出しています。全部の項目を追加できたら、全体用のNSMutableArrayへ追加するわけです。
最後のレコードを処理し終わると、保存用のNSMutableArray(NSArreyでもある)が完成します。これをリンククラスへ渡して保存してもらいます。成否の結果がBool値で返ってくるので、失敗したときだけエラーメッセージを出します。また、受け取ったBool値は、このメソッドの戻り値として返します。
かなり単純な処理で、データ全部を保存できました。plist形式で保存されるため、plistが開けるアプリなどへ持っていけば、保存されたデータを活用できます。ただし、項目名などが含まれていないため、手作業で補助することになりますが。
データをファイルとして保存する場合は、似たようなファイルを読み込んでしまわないように、ファイルの中身を識別できる情報を付加する方法もあります。でも、今回は含めませんでした。
ここで使っているファイル保存は、ファイルへのリンククラスを用いています。呼び出すアプリ側が生成し、ファイル名なども固定しています。そのため、間違ったファイルを読み込むミスは、ほとんど起こらないでしょう。このような理由から、識別情報を含めていません。
逆に、読み込むファイルをユーザー操作で選ぶ方式なら、識別情報をファイルに埋め込む必要があります。入れるべき情報としては、このクラス名、項目名、データ型、任意の識別コードなどの組み合わせになるでしょう。
次は、保存したデータを読み込むメソッドです。
大まかな流れは、次のようになります。まず、ファイルへのリンククラスから、NSArreyの形でデータを受け取ります。一応、正しいデータなのか最低限の検査をします。それが通ったら、このモデルクラスの内部変数を初期化して、読み込んだデータを変数に入れていきます。
この種の処理で大事なのは、内部変数を初期化するタイミングです。読み込みが失敗したり、検査を通らない場合もあるので、大丈夫なデータだと確認できてから、変数を初期化しなければなりません。読み込みが失敗し、直前まで持っていたデータも失ったら、悲惨ですから。
具体的なSwiftコードは、次のようになりました。
// 保存先からデータを読み込む
func loadF() -> Bool {
// 保存先が未登録ならエラー
if cLink2Storage == nil { zSendErrMsgF("ERROR:ZMR_LD:保存先が未登録"); return false }
// データを読み込む
let iNSAryIn: NSArray? = cLink2Storage.loadF()
if iNSAryIn == nil { zSendErrMsgF("ERROR:ZMR_LD:保存先からの読み込みが失敗"); return false }
// 最低限の検査:データ件数の検査
if (iNSAryIn as! [[AnyObject]]).count < 1 {
zSendErrMsgF("ERROR:ZMR_LD:読み込みデータが0件")
return false
}
if (iNSAryIn as! [[AnyObject]])[0].count != cAryType.count {
zSendErrMsgF("ERROR:ZMR_LD:読み込んだ項目数が項目定義と不一致")
return false
}
// 内部変数の初期化
cRecCount = 0
cAryDataInt = [[Int]]()
cAryDataFloat = [[Float]]()
cAryDataBool = [[Bool]]()
cAryDataStr = [[String]]()
setupArrayF()
// 読み込んだデータを、内部変数に入れる
for i in 0..<iNSAryIn!.count { // i=RecIdx
// 空レコードの追加
let iRecIdx: Int = addRecF() // iRecIdx=iなので、iを使う
// レコードへ値を設定
for j in 0..<cFldCount { // j=FldIdx
switch cAryType[j] {
case .ZmInt :
let iInt: Int = (iNSAryIn as! [[AnyObject]])[i][j] as! Int
setDataIntF(i, j, iInt)
case .ZmFloat :
let iFloat: Float = (iNSAryIn as! [[AnyObject]])[i][j] as! Float
setDataFloatF(i, j, iFloat)
case .ZmBool :
let iBool: Bool = (iNSAryIn as! [[AnyObject]])[i][j] as! Bool
setDataBoolF(i, j, iBool)
case .ZmStr :
let iStr: String = (iNSAryIn as! [[AnyObject]])[i][j] as! String
setDataStrF(i, j, iStr)
}
}
}
return true
}
このメソッドでも、処理結果をBool値で返しています。読み込みが失敗した場合は、もとから入っていたデータがそのまま残ります。
処理の一番最初では、保存先となるリンククラスが登録されているか調べています。未登録ならエラーメッセージを出して終了です。
読み込んだデータの検査としては、まずデータ件数を調べます。含まれるデータがゼロ件なら、当然エラーです。続いて、1レコードの項目数を調べます。データ型定義の項目数と一致していなければ、エラーとして扱います。ファイルを間違う可能性が低いので、これ以上の検査は付けませんでした。
内部変数を初期化した後、読み込んだデータを内部変数に入れます。データがレコード順に並んでいるので、アプリ用に作ったメソッドをそのまま使いました。レコードを追加するメソッドと、項目ごとの値を設定するメソッドです。
読み込んだNSArrayからデータを取り出す部分では、まずNSArrayを、AnyObjectの2次元配列にキャストします。そこから1つの要素を取り出して、それを該当するデータ型(4つのデータ型の1つ)にキャストして、一時変数に入れています。その一時変数の値を、項目設定メソッドにて、クラス内のデータ配列へ入れるという手順です。
実際に試しても、正しく動きました。保存メソッドで書き出したファイルを、このメソッドで読み込んだところ、保存されているデータが再現できました。
以上で、モデルクラスのメソッドは全部紹介し終わりました。でも1つだけ、余計なメソッドを追加してあります。
デバッグなどの目的で使う、1レコードを取得するメソッドです。項目値のデータ型がさまざまなので、AnyObjectの配列という形で1レコード分の項目値を返します。具体的なSwiftコードは、次の通りです。
// レコードを取得:このメソッドだけは、値がAnyObjectで返るため、扱いに注意を!(基本的にデバッグ用)
func getRecF(rRecIdx:Int) -> [AnyObject] {
var iAryAObj: [AnyObject] = [AnyObject]()
for i in 0..<cFldCount { // i=FldIdx
let iDataIdx: Int = cAryDataIdx[i]
switch cAryType[i] {
case .ZmInt :
let iInt: Int = cAryDataInt[iDataIdx][rRecIdx]
iAryAObj.append(iInt)
case .ZmFloat :
let iFloat: Float = cAryDataFloat[iDataIdx][rRecIdx]
iAryAObj.append(iFloat)
case .ZmBool :
let iBool: Bool = cAryDataBool[iDataIdx][rRecIdx]
iAryAObj.append(iBool)
case .ZmStr :
let iStr: String = cAryDataStr[iDataIdx][rRecIdx]
iAryAObj.append(iStr)
}
}
return iAryAObj
}
細かな部分の処理は、今まで登場したメソッドに含まれていますから、説明する必要はないでしょう。
モデルクラスの説明が終わったので、使用例を紹介します。新たに使用例を作るのは大変なので、iOS実験専用アプリで使っている、テスト用のコードから抜き出して構成しました。
まずは準備から。ここが一番大事です。具体的には、次のように作ります。
// アプリ側で、記録カードのモデルクラスを使う(準備1)
// 変数と定義値を準備
var modelRecCard25: ZModelRecCard! // モデルクラス
let iAryName: [String] = ["Number", "Rate", "SpMember", "Name", "Place"] // 項目名の定義値
let iAryType: [ZmType] = [.ZmInt, .ZmFloat, .ZmBool, .ZmStr, .ZmStr] // データ型の定義値
// モデルクラスのインスタンス生成
modelRecCard25 = ZModelRecCard(iAryName, iAryType)
たったこれだけで、モデルクラスが使えるようになります。
項目名とデータ型の定義値を入れた配列でも、上手な作り方があります。配列の要素を並べる部分では、空白文字を入れて位置を揃え、同じ数だけ含まれていることが確認しやすいように仕上げます。
実は、もう1つ大事な準備があります。項目インデックス番号を指定するための変数(定数)を、項目の数と同じだけ準備します。項目指定と同じ並び順で作ります。以下のように。
// アプリ側で、記録カードのモデルクラスを使う(準備2)
// 項目インデックス番号を指定するための変数(定数)
let fNumber_i: Int = 0
let fRate_f: Int = 1
let fSpMember_b: Int = 2
let fName_s: Int = 3
let fPlace_s: Int = 4
ここでの作成ポイントは、変数名の工夫です。項目(フィールド)を示す名前だと分かるように、最初にfを付けました。また、変数名の最後には、データ型を示す1文字を加えてあります(i=Int, f=Float, b=Bool, s=String)。さらに、データ型の文字が目立つようにと、間にアンダースコアを入れました。
メソッドの引数として、項目インデックス番号を指定する場合は、この変数名を必ず使います。この変数は、指定した項目数しかありませんから、項目インデックス番号を間違うことはないのです。結果として、メソッド内での項目インデックス番号の検査を外せました。
モデルクラスのインスタンスを生成した直後は、空の状態です。さっそく、1つ目のレコードを追加します。次のような形で。
// アプリ側で、記録カードのモデルクラスを使う(レコードの追加)
let iRecIdx: Int = modelRecCard25.addRecF()
modelRecCard25.setDataIntF(iRecIdx, fNumber_i, 41975)
modelRecCard25.setDataFloatF(iRecIdx, fRate_f, 0.8)
modelRecCard25.setDataBoolF(iRecIdx, fSpMember_b, true)
modelRecCard25.setDataStrF(iRecIdx, fName_s, "T.Sugimoto")
modelRecCard25.setDataStrF(iRecIdx, fPlace_s, "Akita")
レコード追加のメソッドを実行し、レコード番号を受け取ります。続いて、その番号を使い、すべての項目で値を設定します。項目インデックス番号の変数(定数)名には、その項目のデータ型が示されていますから、正しいデータ型のメソッドが選べます。また、後で目視検査するときも、項目インデックス番号の変数名を見ながら、正しいデータ型のメソッド名が選ばれているか確認できます。
このように、項目インデックス番号の変数名を工夫することで、間違ったメソッドを使わなくなります。コーディング中に目視検査しておけば、メソッドのデータ型を選び間違えたエラーは出さないでしょう。
続いて、レコードの読み出しです。レコード番号を指定して、ここの項目を呼び出すだけです。次のように。
// アプリ側で、記録カードのモデルクラスを使う(レコードを読み出す)
let iRecIdx: Int = 16
let iInt: Int = modelRecCard25.getDataIntF(iRecIdx, fNumber_i)
let iFloat: Float = modelRecCard25.getDataFloatF(iRecIdx, fRate_f)
let iBool: Bool = modelRecCard25.getDataBoolF(iRecIdx, fSpMember_b)
let iStr: String = modelRecCard25.getDataStrF(iRecIdx, fName_s)
let iStr2: String = modelRecCard25.getDataStrF(iRecIdx, fPlace_s)
ここでも、項目インデックス番号の変数(定数)名を使います。変数名にデータ型が含まれていますから、正しいデータ型のメソッドを選べます。
当たり前のことですが、全部の項目を毎回読み出す必要はありません。利用する項目だけ読み出し、無駄なアクセスを減らします。
このモデルクラスに含まれるメソッドは、単純なものがほとんどです。そのため、このブログを読むような方には、全メソッドの使用例の紹介は不要でしょう。これ以降では、大事なメソッドだけで使用例を紹介します。
次は、ファイルへ保存するメソッドです。
// アプリ側で、記録カードのモデルクラスを使う(ファイルへ保存)
// 保存先となるリンククラスのインスタンスを生成
let link2File25: ZSLAry2File = ZSLAry2File(.Docs, "rec_card.plist")
// モデルクラスのインスタンスに、保存先を登録
modelRecCard25.setStorageF(link2File25)
// 必要なタイミングで、モデルクラスのインスタンスに入っているデータを保存
let iResult: Bool = modelRecCard25.save2StorageF()
if iResult == true {
setMsgF(4, "ファイルへの保存が成功。")
} else {
setMsgF(4, "ファイルへの保存が失敗。")
}
見てのとおり、保存先のインスタンス生成も、モデルクラスへの設定も、ファイルへの保存も、非常に簡単です。
ファイルへの保存が失敗する可能性は、機械的な要素のほとんどないiPadだとゼロに近いでしょう。でも念のために、エラーメッセージを画面へ表示するぐらいの処理は、付けておきたいものです。
最後に、前述のように保存したファイルから、モデルクラスのインスタンスへデータを読み込む例を挙げます。保存先となるリンククラスが設定されていることが、使う際の前提条件です。
// アプリ側で、記録カードのモデルクラスを使う(ファイルから読み出す)
let iResult: Bool = modelRecCard25.loadF()
if iResult == true {
setMsgF(4, "ファイルからの読み込みが成功。")
} else {
setMsgF(4, "ファイルからの読み込みが失敗。")
}
こちらも非常に簡単です。読み込む処理はたった1行で、その結果を調べて対処する処理が大半を占めます。
モデルクラスの内容を簡単にファイル保存できると、定期的なバックアップにも使えます。
たとえばアプリ内で、最後にバックアップした時刻を保存してあるとします。新しいデータを追加した後で、最後のバックアップ時刻と現在の時刻を比べます。1時間以上経過していたら、最新のバックアップを実行し、バックアップ時刻も更新します。
このようにすると、バックアップの間隔が短くなり、最悪の場合にもデータ損失が少なくて済みます。ちなみに、もっともデータを失わない方法は、データを追加するたびにバックアップする方法です。
アプリが安定しているなら、細かくバックアップする方法は不要でしょう。1日の最後に1回とか、お昼も加えて1日2回とか、適切なタイミングでバックアップする方法が適しています。
以上で、記録カードのモデルをライブラリ化した内容の説明は終わりです。最後に、今回の試作で感じたことを少し書きます。
まず内部構造から。クラス内のデータを配列に入れていますが、Int型データはIntの配列など、データと同じ型の配列を用意して、それぞれに入れてます。このおかげで、インデックスの処理が複雑になってしまいました。クラス内で持つデータは、同じデータ型にしなくても構わないのではと今は思っています。
データを保存する配列をAnyまたはAnyObjectで作ったら、もっと単純なインデックス処理になりました。ライブラリ用のクラスは、十分にテストができて、品質を確保しやすい特徴もあります。内部データをAnyまたはAnyObjectの配列に入れても、十分な品質で作れるでしょうし。
内部データをどのような型で持つかは、意外に難しい問題です。いつも悩みますし、おそらく正解はないでしょう。また、どちらが良いかの感覚は、実際に作ってみて経験しないと育たないので、今回のように作った意味はあると思います。今回のモデルクラスに関しては、AnyまたはAnyObjectのほうが良さそうだというのが、現時点での感覚です。しかし、実際に作ったわけではありませんから、あくまで予想として。
次にライブラリ化するモデルクラスは決まっていて、その作成では、内部配列をAnyまたはAnyObjectで作ろうと考えています。そうすれば、現時点での感覚が正しいのか、判断できるでしょう。もし正しいと確認できたら、こちらのモデルクラスも、内部配列をAnyまたはAnyObjectに変更するつもりです。
データ型を決めた作り方は、外部からアクセスするメソッドにも適用しました。こちらは、今のままが良いと思います。データ型の定義と照らし合わせて、間違いを発見する機能は有効ですし、必要だと感じています。
ファイルへの保存と読み込みは、以前に作ったリンククラスが活きました。切り替えが可能なのに、簡単に使えて便利です。モデルクラス側では、データを生成したり、復元したりする処理に集中できるのが良いですね。
今回のモデルクラスの全体の出来は、使えるレベルに達していると判断しています。検索機能など、まだ不足している機能があるものの、今の状態でも確実に役立ちます。おそらく、実際のアプリで使ってみることが、必要な機能を見付けることにもつながり、このモデルクラスの改良に役立つでしょう。
出来上がったモデルクラスが簡単に使えるのを見て、モデルクラスまでライブラリ化するという考えが、非常に有効だと感じました。まだ1つだけですが、もし5つぐらいの代表的なモデルクラスをライブラリ化できたとき、アプリ内で新規にコーディングするモデルクラスは半数以下になる(つまり、モデルクラスの半数以上がライブラリから生成する)と予想します。それが実現するように、モデルクラスの種類を増やしながら、各モデルクラスの機能も向上させる予定です。
(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)