共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話の続きです。クラスの大まかな説明が終わったので、具体的なSwiftコードの紹介に入ります。
ソースコードの中身の前に、そのファイル名から。古い投稿で紹介したように、私はファイル名に分類情報を入れています。先頭の2文字が分類コードです。ライブラリ用としては、View型のクラスはBV、それ以外のクラスがBCでした。今回作るモデルクラスは特別なので、新しい分類コードとしてBMを割り当てました。モデルクラスのライブラリを、今後も増やしていく予定だからです。
今回のモデルクラスは記録カードなので、ファイル名は「BM_RecCard.swift」としました。ファイル名を見るだけで、基本ライブラリ(B)のモデルクラス(M)に属する、記録カード(RecCard)であることが理解できます。また、ファイル名の一覧でも、モデルクラスのBMが連続して並ぶため、整理された状態で見えます。
ファイル名が決まったので、いよいよSwiftコードです。まずは入れ物となるクラスから。
ライブラリのクラス名はZで始めるルールを採用していて、今回はモデルクラスのRecCardなので、クラス名はZModelRecCardとしました。管理用とデータ用の配列に加え、レコード数の変数も必要です。それらを入れた状態で、次のようになりました。
// 記録カードのモデルクラス(これは入れ物で、この中に後述するコードを追加する)
class ZModelRecCard {
// 主要な数
var cFldCount: Int = 0 // 項目数
var cRecCount: Int = 0 // レコード数
// 管理用の配列
var cAryName: [String] = [String]() // 項目名
var cAryType: [ZmType] = [ZmType]() // 項目のデータ型の定義
var cAryDataIdx: [Int] = [Int]() // データ配列内のインデックス番号
// データを入れる配列
var cAryDataInt: [[Int]] = [[Int]]() // データ配列
var cAryDataFloat: [[Float]] = [[Float]]()
var cAryDataBool: [[Bool]] = [[Bool]]()
var cAryDataStr: [[String]] = [[String]]()
}
Optional型にしたくないので、配列も含めて変数宣言時に初期化しています。
このモデルクラスでは、主に3種類のインデックスを使い分けます。それぞれの意味を、きっちりと整理しておかなければなりません。作っている途中で間違えやすいですから。
そんなミスを防ぐために役立つのが、インデックスの名前と役割を書いたコメントです。どんな名前のインデックスがあり、それぞれが何を指しているのか整理した内容を、コメント行として変数の直前に入れます。以下のように。
// =================================== 3つのインデックスがある
// RecIdx:レコードの並び順を示すインデックス
// FldIdx:項目(フィールド)の並び順を示すインデックス:この値からDataIdxを得て、それでデータへアクセスする
// DataIdx:データ配列内で、どの行(位置)に入っているかを示すインデックス:FldIdxをキーにして、cAryDataIdxから得る
// ==================== 次のように利用する
// cAryName[FldIdx] -> String (項目名)
// cAryType[FldIdx] -> ZmType (データ型)
// cAryDataIdx[FldIdx] -> DataIdx
// cAryDataXXX[DataIdx][RecIdx] -> Data (4種類のデータ型)
// ===================================
このコメントの後半では、用意した配列とインデックスの関係を示しています。どの配列に、どのインデックスを使い、どんな値が得られるのか、利用例の形で説明しています。
各インデックスの説明文だけでは理解しづらいですが、配列とインデックスと取得値の関係を示されると、格段に理解しやすくなります。また、このコメント行を見ながらプログラミングすると、インデックスの使い方を間違う可能性が大きく減ります。ですから、インデックスと配列の利用例のコメント行は、最初に作るべきでしょう。
当然ですが、インデックスとして用いる変数名も、ここで用意したインデックスの名前を使います。そうしないと、正しいインデックスが使われているかどうか、後で目視検査できませんから。
データ型を指定する列挙型も、専用に作りました。こちらもZで始まる名前です。
// 項目の定義に使うデータ型
enum ZmType: Int {
case ZmInt = 0
case ZmFloat = 1
case ZmBool = 2
case ZmStr = 3
}
列挙型の値ですが、先頭にZmを付けています。Zmが付かない、ただのIntでも構わないのですが、検索するときに、関係のない多量のIntが含まれます。それを嫌ってZIntとしたのですが、大文字が続いて読みづらく感じます。仕方なく、Zmと2文字を加えた形に落ち着きました。
本音ではzIntとしたかったのですが、列挙型の値は大文字で始めるルールがあるようで、zIntは避けました。一応、Swiftのコーディングルールは、できるだけ守りたいですからね。
続いて、モデルクラスの初期化です。モデルの定義として必要な、2つの配列を受け取ります。項目名の一覧と、項目ごとのデータ型の一覧です。この2つの配列だけで、モデルクラスの定義が完了します。
まずデータ型の一覧ですが、上記ZmTypeの配列です。使いたい項目の数だけ、データ型を並べます。8項目を使いたい場合は、8つのデータ型を入れます。すべて同じデータ型でも構いません。並び順は自由で、自分が理解しやすい順番を選びます。
データを入れたり出したりする際に、並び順で決まったインデックス番号(FldIdx)で、目的の項目を指定するだけです。その点にだけ注意していれば、あとは自由に使えます。
もう片方の項目名は、処理の中では少ししか使っていません。項目名付きのデータを生成したいときなど、このクラスを参照するだけで済むようにと用意しただけです。ただし、1つだけ重要な役割があります。項目名とデータ型の数を比べて、一致しているか調べます。初期化データの作成ミスを発見するのが目的です。
以上のように考えて作った初期化処理が、次のようなSwiftコードです。
// 初期化:モデルの定義(名前とデータ型)を受け取る
init(_ rAryName:[String], _ rAryType:[ZmType]) {
if rAryName.count == 0 { zSendErrMsgF("ERROR:ZMR_INI:項目名が空"); return }
if rAryName.count != rAryType.count { zSendErrMsgF("ERROR:ZMR_INI:項目名とデータ型の数が不一致"); return }
cAryName = rAryName
cAryType = rAryType
cFldCount = cAryType.count
// 配列の準備
setupArrayF()
}
// 項目ごとに配列を生成して、その位置(インデックス番号)を記憶する
private func setupArrayF() {
for i in 0..<cFldCount { // i=FldIdx
switch cAryType[i] {
case .ZmInt :
cAryDataIdx.append(cAryDataInt.count) // count値がインデックス値より1つ大きいので、作成前に保存する
cAryDataInt.append([Int]())
case .ZmFloat :
cAryDataIdx.append(cAryDataFloat.count)
cAryDataFloat.append([Float]())
case .ZmBool :
cAryDataIdx.append(cAryDataBool.count)
cAryDataBool.append([Bool]())
case .ZmStr :
cAryDataIdx.append(cAryDataStr.count)
cAryDataStr.append([String]())
}
}
}
項目名とデータ型の数が一致するかだけでなく、受け取った配列が空でないかも調べています。これらの検査を通ったら、クラス内の変数に代入するだけです。
続いて、配列の準備処理が続きます。cAryTypeに入っているデータ型を参照しながら、該当するデータ型の2次元配列に、初期化した1次元配列を追加していきます。これが済むと、データをappendできる状態になります。
このように、初期化で指定されたデータ型を見て、それに対応した1次元配列を追加するため、余計な配列は作りません。必要なものしか作らないように、つまり無駄なものを生まない形にしてあります。
以上の初期化処理が終わると、もうデータを追加できる状態になっています。あとは、データを追加して溜め込み、必要なときにデータを参照するだけです。
続いて、メソッドを順番に取り上げましょう。まずは、項目名とデータ型を扱うメソッドから。次のように、2つしか用意していません。
// ======================================== 基本情報
// 項目名の一覧を返す
func getNameF() -> [String] {
return cAryName
}
// データ型の一覧を返す
func getTypeF() -> [ZmType] {
return cAryType
}
これに説明は不要でしょう。保存していたままというか、初期化の際に受け取ったままを返します。
次は、レコード単位の処理です。ここが2番目に悩んだ点です(ちなみに、1番目に悩んだ点は、前の投稿で紹介したデータの持ち方)。
レコード内のデータ型の構成は、使うアプリごとに異なります。その状態で、共通のレコード用メソッドを作ると、AuyやAnyObjectを使うしかないのです。それ以外だと、データ型の組み合わせを全部用意するしかありません。これこそ非現実的でしょう。
発想を切り替えて、レコードの追加は次のようにしました。まずダミー値で、新規のレコードを追加します。その直後に、項目ごとの値を設定するメソッドで、すべての項目の値を設定します。1つの値だけを設定するメソッドなら、データ型を固定した形で作れます。データ型の種類が4つなので、項目の値を設定するメソッドは4つで済みます。
レコードの更新も同様です。レコード単位で更新する場合も、すべての項目を1つずつ更新して、レコード全体を更新します。削除だけは例外で、新しい項目値を用意する必要がないため、1つのメソッドで完了できます。
項目ごとに値を設定する方法は、少し面倒に感じるかもしれませんが、それほどではありません。アプリ側でデータを用意する処理では、項目ごとにデータを集めて、1つのレコードに整えます。その処理を変更して、1つのレコードに整えず、項目ごとのデータを集める処理内で、そのままデータを更新してしまえば良いのです。アプリ側の処理全体として比べると、あまり差がないはずです。
以上のように考えて、作成したのが次の3つのSwiftコードで、1つずつ順番に要点だけ解説します。
// ======================================== レコード処理
// レコード数を返す
func getRecCountF() -> Int {
return cRecCount
}
レコード数を返す処理は、見たとおりです。モデルクラス内で持っている値を、そのまま返しています。
レコードを追加する処理では、ダミーの値を入れています。それぞれの保存先では、初期化処理の中で空の配列を追加しているため、appendメソッドで追加するだけで済みます。
// レコードを追加:追加したレコードのインデックス番号を返す
func addRecF() -> Int {
for i in 0..<cFldCount { // i=FldIdx
let iDataIdx: Int = cAryDataIdx[i]
switch cAryType[i] {
case .ZmInt :
cAryDataInt[iDataIdx].append(-999)
case .ZmFloat :
cAryDataFloat[iDataIdx].append(-999.9)
case .ZmBool :
cAryDataBool[iDataIdx].append(false)
case .ZmStr :
cAryDataStr[iDataIdx].append("empty")
}
}
++cRecCount
return (cRecCount - 1)
}
一番の肝は、データ型を調べることと、保存先を求めることです。それを項目の数だけ実行しなければなりません。項目の数はcFldCountに入れてあるので、その数だけfor文で回します。このような、項目の数だけ実行するという処理が何度も登場します。このfor文は、このモデルクラスでの定番パターンと言えるでしょう。
また、このfor文の変数iは、FldIdxを意味しています。そのことを伝えるために、コメントを追加しています。このおかげで、インデックスの使い方を間違えるミスが減らせます。別な方法として、変数名をiではなく、iFldIdxと変える方法も考えられます。しかし、ループ処理の中でのiやjは特別な存在で、処理内容を理解しやすくする効果も持っています。iやjのままのほうが良いと考え、コメントで補足する方法を選びました。
データ型はcAryTypeに入っています。その内容を調べることで、データ型を特定できます。switch文で分岐させ、該当するデータ型の処理に進めます。
ダミー値の保存先は、FldIdx値から求めます。保存先といっても、保存されているのはインデックス番号DataIdxで、それはcAryDataIdxに入っています。ここに入っている値DataIdxは、それぞれのデータ型の配列で、何行目なのか示しているだけです。ですから、この値DataIdxを取り出して、データ用の配列を参照するために使う必要があります。保存先のDataIdxが明らかになると、appendメソッドが実行できます。
最後に、追加したレコード番号RecIdxを返しています。レコード追加の直後に項目の値を更新するので、追加したレコード番号が分かると非常に助かるからです。
レコードを削除する処理ですが、私の場合はほとんど使わないものの、とりあえず付けておきました。
// レコードを削除
func deleteRecF(rRecIdx:Int) {
if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_DR:インデックスが範囲外"); return }
for i in 0..<cFldCount { // i=FldIdx
let iDataIdx: Int = cAryDataIdx[i]
switch cAryType[i] {
case .ZmInt :
cAryDataInt[iDataIdx].removeAtIndex(rRecIdx)
case .ZmFloat :
cAryDataFloat[iDataIdx].removeAtIndex(rRecIdx)
case .ZmBool :
cAryDataBool[iDataIdx].removeAtIndex(rRecIdx)
case .ZmStr :
cAryDataStr[iDataIdx].removeAtIndex(rRecIdx)
}
}
--cRecCount
}
まず最初に、レコード番号が範囲外か調べて、範囲外ならエラーメッセージを出して終了します。
あとは全体的に追加の処理と似ていて、データを削除する部分だけが異なります。データが入っている配列で、appendではなくremoveAtIndexを実行します。最後にレコード数を減らして終了です。
ここまででも、かなり長くなってしまいました。続きは次回の投稿にて。
(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)
0 件のコメント:
コメントを投稿