2015年6月29日月曜日

共通モデルのライブラリ化を試作(機能追加編)

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話は終わりました。しかし、簡単な検索機能を急に追加したので、続きとして紹介します。

 

今回のモデルクラスは、まだ機能が不足気味です。どんな機能を追加したら良いのか、作り終わっても考え続けていました。やはり検索機能が必要だと感じ、そこだけ集中して検討したのです。凝った機能は実現が非常に難しいと判断し、とりあえず簡単に実現できる方法を追加することにしました。

検索機能では、検索の種類をどれだけ増やせるかが重要です。あらかじめ用意する決められた方法では、やはり限界があります。それよりも、検索で使う判定条件をアプリ側から与えて、モデルクラス側では、与えられた判定条件を実行する形が良いでしょう。

このような仕組みの実現には、判定条件を関数として与える方法が有効です。アプリ側で関数を用意し、その関数をメソッドに渡して、モデルクラス側で実行します。

ただし、なんでも出来るわけではありません。与えられる関数の仕様や、関数の渡し方によって、自ずと限界が生じます。凝った仕組みは大変なので、単純な仕組みを選びました。特定の1項目の値だけを見て、条件に適合しているのか判断する関数です。

関数の仕様は単純です。項目のデータ型がIntの場合は、関数のデータ型が「(Int) -> Bool」となります。つまり、Int型の値を受け取って、何か処理をし、Bool値を返すという関数です。他のデータ型も同様で、データ型がStringであれば、関数のデータ型が「(String) -> Bool」となります。これらの関数は真偽を判定する役割なので、ここでは判定関数と呼びましょう。判定関数の結果がtrueのレコードだけ、検索結果に含めます。

 

具体的なSwiftコードを見てみましょう。データ型が4種類なので、4つのメソッドを追加しましたが、まずはInt型だけ。

// ======================================== 条件検索
func findIntF(rFldIdx:Int, _ rFunc:(Int) -> Bool) -> [Int] {
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    var iAryRecIdx: [Int] = [Int]()
    for i in 0..<cRecCount {   // i=RecIdx
        let iInt: Int = cAryDataInt[iDataIdx][i]
        if rFunc(iInt) {
            iAryRecIdx.append(i)
        }
    }
    return iAryRecIdx
}

4つとも同じような処理なので、Int型だけ説明します。ソースコードも短く、単純な処理です。

引数は、項目インデックス番号FldIdxと判定関数だけです。アプリ側では、FldIdxとして予め用意した変数(定数)名を使うので、FldIdxの範囲の検査は付けていません。

処理の最初では、指定された項目の値が入っている配列の位置を探します。FldIdxからDataIdxを求めるだけです。位置が見つかったので、全部の値を最初から見ていきます。レコード数と同じ回数だけループする処理になります。

ループの直前で、判定関数でtrueとなったレコードの番号RecIdxを入れる配列iAryRecIdxを用意します。ループ内では、データの入った配列cAryDataIntから、項目の値を取り出し、変数iIntに入れます。その値を判定関数に与えて、判定結果がtrueであれば、レコード番号(i=RecIdx)を配列iAryRecIdxに追加します。

ループの最後には、レコード番号の配列iAryRecIdxを返すだけです。もし全部がfalseなら、空の配列を返します。

 

残りの3つのメソッドは、次のとおりです。

// 残り3つのメソッド
func findFloatF(rFldIdx:Int, _ rFunc:(Float) -> Bool) -> [Int] {
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    var iAryRecIdx: [Int] = [Int]()
    for i in 0..<cRecCount {   // i=RecIdx
        let iFloat: Float = cAryDataFloat[iDataIdx][i]
        if rFunc(iFloat) {
            iAryRecIdx.append(i)
        }
    }
    return iAryRecIdx
}
func findBoolF(rFldIdx:Int, _ rBool:Bool) -> [Int] {
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    var iAryRecIdx: [Int] = [Int]()
    for i in 0..<cRecCount {   // i=RecIdx
        let iBool: Bool = cAryDataBool[iDataIdx][i]
        if iBool == rBool {
            iAryRecIdx.append(i)
        }
    }
    return iAryRecIdx
}
func findStrF(rFldIdx:Int, _ rFunc:(String) -> Bool) -> [Int] {
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    var iAryRecIdx: [Int] = [Int]()
    for i in 0..<cRecCount {   // i=RecIdx
        let iStr: String = cAryDataStr[iDataIdx][i]
        if rFunc(iStr) {
            iAryRecIdx.append(i)
        }
    }
    return iAryRecIdx
}

見て分かるように、Bool型だけは関数を使っていません。2種類の値しかないので、指定した値と同じかどうか調べるしかないですから。残りのメソッドの処理内容はInt型と同じなので、説明は省略します。

 

続いて、使用例の紹介です。今回も、iOS実験専用アプリから抜き出しました。

検索するためには、判定関数が必要です。掲載した例では、判定関数をクロージャとして用意し、変数に入れています。それを検索メソッドに渡すだけです。

// アプリ側で検索機能を使ってみる

// 指定された項目の値が、500以下のレコードを検索
let iFuncI1: (Int) -> Bool = { (rInt:Int) -> Bool in return (rInt < 500) }
let iAryRecIdx: [Int] = modelRecCard25.findIntF(fNumber_i, iFuncI1)

// 指定された項目の値が、変数に入っている標準値より大きなレコードを検索
var iIntStd: Int = 100
let iFuncI3: (Int) -> Bool = { (rInt:Int) -> Bool in return (rInt > iIntStd) }
let iAryRecIdx3: [Int] = modelRecCard25.findIntF(fNumber_i, iFuncI3)

// 指定された項目の値が、trueのレコードを検索(関数は不要)
let iAryRecIdxB: [Int] = modelRecCard25.findBoolF(fSpMember_b, true)

// 指定された項目の文字列で、先頭が「C」のレコードを検索
let iFuncS2: (String) -> Bool = { (rStr:String) -> Bool in return rStr.hasPrefix("C") }
let iAryRecIdxS2: [Int] = modelRecCard25.findStrF(fName_s, iFuncS2)

// 指定された項目の文字列で、どこかに「g」があるレコードを検索
let iFuncS3: (String) -> Bool = { (rStr:String) -> Bool
    in if let iRange = rStr.rangeOfString("g") { return true }; return false }
let iAryRecIdxS3: [Int] = modelRecCard25.findStrF(fName_s, iFuncS3)

これらの例のように、関数さえ作れれば、かなり様々な判定が可能です。1つの項目値で判定可能な内容なら、たいていのことができると思います。

アプリ側では、検索条件を満たしたレコード番号RecIdxの配列しか受け取りません。そのレコード番号をもとに、他のgetメソッドを使って、該当するレコードの項目値を取得します。

モデルクラスに多くのデータが入っている場合は、全部の項目値を受け取るよりも、少ないデータ量で済みます。その点が、この検索メソッドの価値でしょう。

 

複数の項目を使った判定も、作ろうと思えば追加できます。しかし、実装は相当に大変そうです。2つの項目値でも、データ型の組み合わせが多く、その組み合わせを全部用意するのか、それともデータ型を指定できる工夫を用意するのか、どちらにしても難しいでしょう。

1つの項目だけを使う検索メソッドなら簡単に作れるので、現実的な選択肢として選びました。これなら、4つのメソッドを用意するだけで済みますから。

 

今回の検索機能は、簡単な機能の追加でしたが、幅広く使えるメソッドになりました。また、何か思い浮かんだら、モデルクラスに機能を追加したいと思います。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年6月28日日曜日

共通モデルのライブラリ化を試作(本編3+使用例編)

共通で使えるモデルクラスをライブラリ化する第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)

2015年6月27日土曜日

共通モデルのライブラリ化を試作(本編2)

共通で使えるモデルクラスをライブラリ化する第1段として、記録カードのモデルクラスを作る話の続きです。Swiftコードの紹介を続けます。

レコート関係メソッドが済んだので、項目関係のメソッドを取り上げます。

まずは、項目の値を取得するgetメソッドから。コンパイラのデータ型検査の機能を生かすために、データ型ごとに用意します。今は4つのデータ型を使ってますから、メソッドも4つになります。次のようなSwiftコードになりました。

// ======================================== 値の取得と設定
// 値の取得
func getDataIntF(rRecIdx:Int, _ rFldIdx:Int) -> Int {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDI:項目定義のデータ型がIntでない"); return -999 }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDI:レコード位置が範囲外"); return -999 }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iInt: Int = cAryDataInt[iDataIdx][rRecIdx]
    return iInt
}
func getDataFloatF(rRecIdx:Int, _ rFldIdx:Int) -> Float {
    if cAryType[rFldIdx] != .ZmFloat { zSendErrMsgF("ERROR:ZMR_GDF:項目定義のデータ型がFloatでない"); return -999.9 }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDF:レコード位置が範囲外"); return -999.9 }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iFloat: Float = cAryDataFloat[iDataIdx][rRecIdx]
    return iFloat
}
func getDataBoolF(rRecIdx:Int, _ rFldIdx:Int) -> Bool {
    if cAryType[rFldIdx] != .ZmBool { zSendErrMsgF("ERROR:ZMR_GDB:項目定義のデータ型がBoolでない"); return false }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDB:レコード位置が範囲外"); return false }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iBool: Bool = cAryDataBool[iDataIdx][rRecIdx]
    return iBool
}
func getDataStrF(rRecIdx:Int, _ rFldIdx:Int) -> String {
    if cAryType[rFldIdx] != .ZmStr { zSendErrMsgF("ERROR:ZMR_GDS:項目定義のデータ型がStringでない"); return "Error!" }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDS:レコード位置が範囲外"); return "Error!" }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iStr: String = cAryDataStr[iDataIdx][rRecIdx]
    return iStr
}

扱うデータ型と、データを取り出す配列が違うだけで、4つとも同じような処理をしています。

最初にチェックするのは、データ型です。指定された項目番号が定義されているデータ型を調べて、メソッドのデータ型と比べています。同じでなければ、エラーメッセージを出して終了です。

続いては、レコード番号の検査です。存在するレコード番号か調べて、範囲内でなければエラーメッセージを出して終了です。

エラーチェックを通ると、FldIdxからDataIdxを求めます。DataIdxは、データが入っている配列のインデックス番号です。それとレコード番号RecIdxにより、目的の項目データを得られます。

鋭い人なら、これら4つのコードを見て、ある疑問が浮かんだと思います。RecIdxは範囲チェックしているのに、FldIdxは範囲チェックしていない、なぜだろうと。実は、FldIdxの範囲チェックを不要にする、使い方の工夫があるのです。使用例編のところで詳しく説明しますので、今は気にしないでください。

 

この4つのメソッドでは、エラーがあったときにnilを返していません。エラーメッセージは出すものの、関係のない値を返しています。このように作った理由は、戻り値をOptional型にしたくなかったからです。

戻り値がOptional型だと、メソッド側でラップし、呼び出し側でアンラップします。両方で無駄な処理が生じます。項目ごとにメソッドを呼び出す方式ですから、もっとも多く使うメソッドになってしまうため、このような無駄を含ませたくないのです。

エラーが出る状況も考えてみました。データ型が間違っているエラーは、メソッドの単なる選択ミスなので、そのコードが最初に動いたときに気付きます。簡単に直して終了です。

もう1つのエラーは、配列の範囲外のインデックス指定です。こちらも、最初に作ったインデックス計算式などが間違っていたときに発生します。その場合は、簡単に直して終了です。でも、隠れているバグにより、インデックス値の計算がミスしてて、間違った値が生じることもあります。この場合は、あきらめるしかないでしょう。ただし、この種のバグが隠れている可能性は極めて低いので、本番時のバグを気にする必要はないと考えます。

このようにエラーが発生する状況を考えた結果、関係のない値を返しても構わないと判断しました。

 

せっかくなので、nilを返した場合の動作も考えてみましょう。戻り値がOptrinal型であっても、正しく動いている前提でプログラミングするため、nilかどうかを検査せず、無条件にアンラップするでしょう。すると、nilが返ったとき、アプリ側でクラッシュします。

配列のインデックス値を検査しないのは、アプリ内で用意した配列を使う場合と同じです。アプリ内の配列でも、インデックス値が範囲外であれば、その場でクラッシュしてしまいます。

つまり、インデックス値が範囲外の場合、アプリ内の配列でも、このモデルクラス内の配列でも、同じようにクラッシュする作り方になるということです。

結果として、Optional型にするかしないかの違いは、インデックス計算のバグが含まれるときに、次のようになります。Optional型でnilを返す場合は、アプリがクラッシュします。Optional型にせず変な値を返す場合は、エラーメッセージが出て、変な値を通常どおり処理し、表示結果などが変な値になるでしょう。また通常時の違いとして、ラップとアンラップの処理がOptional型には含まれます。

インデックス計算のバグは開発時に発見されることがほとんどなので、開発でのバグ発生時にどう動くのが良いのか、ラップとアンラップ処理を気にするのかだけです。最終的な選択は、好みの問題でしょう。私はOptional型にしないほうを選びましたが、それぞれ好きなほうを選べばよいと思います。

 

実行速度が気になる場合は、項目の値の取得や設定で、検査を外すという方法もあります。2つの検査で引っかかるのは、ソースコードを動かし始めたときだけでしょう。アプリ全体が出来上がる直前の段階まで達したら、2つの検査をコメント化することも可能です。次のように。

// 値の取得(2つの検査をコメント化)
func getDataIntF(rRecIdx:Int, _ rFldIdx:Int) -> Int {
    //if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDI:項目定義のデータ型がIntでない"); return -999 }
    //if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_GDI:レコード位置が範囲外"); return -999 }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iInt: Int = cAryDataInt[iDataIdx][rRecIdx]
    return iInt
}

こうすると無駄な処理が減って、少しは高速になります。検査を外した状態では、戻り値をOptional型にしなかったのが、まるで正解のようにも感じられます。

これは取得メソッドでのコメント化ですが、後述する設定メソッドでも、同様にコメント化が可能です。コメント化するなら、両方のメソッドで実施したほうが良いでしょう。

本番用のコンパイルで一部の機能を外すときは、昔から#ifdefが広く使われています。Swiftでも、同様の目的で#ifが使えます。この機能を使って、本番用だけは自動的に機能をオフする手もあります。詳しい使い方はネットなどで調べてください。

 

このモデルクラスでは、データ型が異なる4つセットのメソッドが何セットも含まれます。データ型が異なるものの、処理内容が似ているため、共通部分を探して外に出し、それを呼び出す形に作り変えたくなります。

でも今回は、データを入れている配列も別々ですし、途中で使う変数もデータ型が異なります。共通部分を抜き出すのが難しいケースです。しかも、それぞれのメソッドは短めで、共通部分を抜き出すメリットは小さめです。

このような場合は、無理して共通部分を抜き出しても、別なバグを生んだり、処理内容の読みやすさが低下したりと、デメリットのほうが大きくなりがちです。かなり似ている処理ですが、そのまま共通化しないで作るのがベストだと思います。

 

続いて、項目へ値を設定するsetメソッドです。これもデータ型ごとに合計4つあります。

// 値を設定
func setDataIntF(rRecIdx:Int, _ rFldIdx:Int, _ rData:Int) {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_SDI:項目定義のデータ型がIntでない"); return }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_SDI:レコード位置が範囲外"); return }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    cAryDataInt[iDataIdx][rRecIdx] = rData
}
func setDataFloatF(rRecIdx:Int, _ rFldIdx:Int, _ rData:Float) {
    if cAryType[rFldIdx] != .ZmFloat { zSendErrMsgF("ERROR:ZMR_SDF:項目定義のデータ型がFloatでない"); return }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_SDF:レコード位置が範囲外"); return }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    cAryDataFloat[iDataIdx][rRecIdx] = rData
}
func setDataBoolF(rRecIdx:Int, _ rFldIdx:Int, _ rData:Bool) {
    if cAryType[rFldIdx] != .ZmBool { zSendErrMsgF("ERROR:ZMR_SDB:項目定義のデータ型がBoolでない"); return }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_SDB:レコード位置が範囲外"); return }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    cAryDataBool[iDataIdx][rRecIdx] = rData
}
func setDataStrF(rRecIdx:Int, _ rFldIdx:Int, _ rData:String) {
    if cAryType[rFldIdx] != .ZmStr { zSendErrMsgF("ERROR:ZMR_SDS:項目定義のデータ型がStringでない"); return }
    if rRecIdx < 0 || rRecIdx >= cRecCount { zSendErrMsgF("ERROR:ZMR_SDS:レコード位置が範囲外"); return }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    cAryDataStr[iDataIdx][rRecIdx] = rData
}

検査内容は、getメソッドと同じです。

検査を無事に通過すると、FldIdxからDataIdxを求めます。これもgetメソッドと同じです。あとは、該当するデータ型の配列へ、受け取った値を設定するだけです。

 

次は、苦肉の策として用意したメソッドを取り上げます。

いろいろな使い方への対応も大事です。全部の要望に対応するのは無理なので、アプリ側が何とかできるための手助けを用意するのが、現実的な方法でしょう。

とは言え、どんな手助けが良いのでしょうか。アプリ側の要望を予想して考えてみましょう。たとえばクラス内から、条件に合ったデータを見付けるときです。このクラスで検索機能を用意しても構わないのですが、データ型が4種類あり、それを自由に組み合わせて使えます。検索機能を自由に使える形で実現するのは、難しそうです。

そもそも、検索機能も要望の1つでしかありません。値の傾向を調べたり、中央値を求めたり、要望を考えると次々に出てきます。

そこで思い浮かんだのが、最も簡単に実現でき、かなり幅広く使える機能です。その機能とは、特定の項目の全データを返す機能です。この機能が全部の項目で使えれば、アプリ側で何とでも使いこなせるでしょう。

たとえば、ある項目で上位3つの値を持つレコードだけ探すとしましょう。その項目の全データを得られれば、全データを比較して上位3つを選び出せます。このクラスから受け取ったデータはレコード順に並んでいるため、該当するレコードを特定することは簡単です。あとは、見付かったレコードをメソッドで取得するだけです。レコード単位の取得は用意してないので、項目単位で取得することになりますが。

他の要望も、項目の全レコードを取得することで、何とかなるでしょう。複数の項目で検索する場合も、該当する複数項目の全データがあれば可能です。極端な話、全部の項目で全データを取得すると、クラス内の全データを得ることができます。

以上のように考えて作ったのが、特定項目の全データを返すメソッドです。4つのデータ型ごとに必要なので、4つのメソッドになりました。具体的には、次のようなSwiftコードです。

// ======================================== 特定項目全データを返す
func getDataAllIntF(rFldIdx:Int) -> [Int] {
    if cAryType[rFldIdx] != .ZmInt { zSendErrMsgF("ERROR:ZMR_GDAI:項目定義のデータ型がIntでない"); return [Int]() }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    return cAryDataInt[iDataIdx]
}
func getDataAllFloatF(rFldIdx:Int) -> [Float] {
    if cAryType[rFldIdx] != .ZmFloat { zSendErrMsgF("ERROR:ZMR_GDAF:項目定義のデータ型がFloatでない"); return [Float]() }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    return cAryDataFloat[iDataIdx]
}
func getDataAllBoolF(rFldIdx:Int) -> [Bool] {
    if cAryType[rFldIdx] != .ZmBool { zSendErrMsgF("ERROR:ZMR_GDAB:項目定義のデータ型がBoolでない"); return [Bool]() }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    let iAryBool: [Bool] = cAryDataBool[iDataIdx]
    return cAryDataBool[iDataIdx]
}
func getDataAllStrF(rFldIdx:Int) -> [String] {
    if cAryType[rFldIdx] != .ZmStr { zSendErrMsgF("ERROR:ZMR_GDAS:項目定義のデータ型がStringでない"); return [String]() }
    let iDataIdx: Int = cAryDataIdx[rFldIdx]
    return cAryDataStr[iDataIdx]
}

どのデータ型でも、ほぼ同じです。最初にデータ型を検査してOKなら、DataIdxを求めて、該当する配列を返しています。

いつもなら、return文の前に一時変数を用意して、そこへ戻り値を入れます。でも今回は配列なので、配列がコピーされるという無駄な処理が生じるのを嫌い、返す配列を、return文の中に直接書いています。

あと戻り値のデータ型ですが、Otrional型にしたくなかったので、エラーの際にnilではなく、空の配列を返すようにしました。このエラーが出るのは、データ型を間違えたときだけ、つまり呼び出す側のコードを最初に動かしたときだけですから、データ型の間違いをエラーメッセージで気付くはずです。

全データを返す機能ですが、データ量が少ない場合の特別用であり、データ量が多い場合はお勧めできません。返す配列が大きくなるからです。このモデルクラスを実際にアプリで使ってみて、どのような機能が必要なのか検討し、この機能を使わなくて済むような新機能を追加しようと思っています。

 

このモデルクラス内に入れたデータを活用する目的の機能も、用意しておくと便利でしょう。とりあえず、1つのメソッドを作りました。

データ活用では、タブ区切りのテキストとして生成する機能が、かなり幅広く利用できると思います。そのまま表計算アプリのワークシートへペーストすれば、それぞれの値が個々のセルに入りますから。

というわけで、タブ区切りのテキストを生成して返すメソッドを用意しました。先頭に項目名を入れた、見出し行付きです。具体的には、以下のようなSwiftコードになりました。

// ======================================== データ利用
// タブ区切りのテキストを生成
func getTabTextF() -> String {
    var iStrOut: String = ""   // 出力用
    // テキストの生成:見出し
    for i in 0..<cFldCount {   // j=FldIdx
        iStrOut += cAryName[i]
        if i < (cFldCount - 1) {   // 行末だけタブ文字なし
            iStrOut += CODE_TAB
        }
    }
    iStrOut += CODE_LF
    // テキストの生成:データ
    for i in 0..<cRecCount {   // i=RecIdx
        for j in 0..<cFldCount {   // j=FldIdx
            let iDataIdx: Int = cAryDataIdx[j]
            switch cAryType[j] {
            case .ZmInt :
                let iInt: Int = cAryDataInt[iDataIdx][i]
                let iStr: String = String(iInt)
                iStrOut += iStr
            case .ZmFloat :
                let iFloat: Float = cAryDataFloat[iDataIdx][i]
                let iStr: String = String(stringInterpolationSegment:iFloat)
                iStrOut += iStr
            case .ZmBool :
                let iBool: Bool = cAryDataBool[iDataIdx][i]
                let iStr: String = String(stringInterpolationSegment:iBool)
                iStrOut += iStr
            case .ZmStr :
                let iStr: String = cAryDataStr[iDataIdx][i]
                iStrOut += iStr
            }
            if j < (cFldCount - 1) {   // 行末だけタブ文字なし
                iStrOut += CODE_TAB
            }
        }
        iStrOut += CODE_LF
    }
    return iStrOut
}

タブ区切りのテキストでは、データの並び順が大事です。表計算アプリへペーストする使い方が多いでしょうから、ワークシート上で使う際に一般的な並び方をしていなければなりません。複数レコードのデータなら、もっとも一般的な姿は、1行目に見出しが来て、2行目からデータが並び、1行分が1件のレコードという形でしょう。ですから、その形に合わせて、タブ区切りのテキストを生成します。

まず最初に、項目名をタブで区切った見出し行を生成します。行末には、タブを入れずに、LFコードを入れます。タブもLFも、別な共通ライブラリで定義してあり、その変数を使っています。

続いて、データを順番に追加します。4種類のデータがありますが、どれもStringに変換して入れます。1つのレコードが1行分となり、項目間はタブで区切り、行末はタブなしのLFです。これをレコードの数だけ繰り返します。

最後のレコードが処理し終わったら、テキストの生成も完了です。生成した文字列を呼び出し先へ返して、メソッドは終了します。

 

この機能の使い方ですが、次のようになるでしょう。画面上に、データをコピーするボタンを用意します。そのボタンがタップされたら、このメソッドを呼び出して、タブ区切りのテキストを得ます。それをクリップボードへコピーすれば、アプリ側の処理は完了です。

その際、終了のメッセージも大事ですね。クリップボードへコピーされたこととに加え、何件のレコードが含まれるか表示します。レコード件数が明らかになると、他のアプリへペーストしたとき、漏れなくデータが渡ったと確認できて助かりますから。

 

また長くなってきたので、ここで区切ります。続きは、次回の投稿にて。残りの機能(ファイルへの保存)と使用例を取り上げる予定です。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年6月25日木曜日

共通モデルのライブラリ化を試作(本編1)

共通で使えるモデルクラスをライブラリ化する第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)

2015年6月23日火曜日

共通モデルのライブラリ化を試作(全体検討編)

アプリを作る際には、扱う情報ごとにモデル(MVCのM)のクラスを用意して、アプリ内の構造をスッキリさせます。オブジェクト指向では一般的な考え方でしょう。

情報ごとにモデルを作るのですが、同じようなパターンの情報も意外に多くあります。それらを毎回作っても構わないのですが、もしライブラリ化できれば、クラスを毎回作らずに済むのでは、と考えるようになりました。

もちろん、ライブラリ用として用意するモデルクラスが、1つで済むとは考えていません。よく使いそうなモデルを選び、おそらく数種類のモデルをライブラリ化します。その結果、半分ぐらいのモデルクラスを作らなくて済むと予想しています。

というわけで、試しに作ってみました。予想外に苦労したので、その過程も含めて紹介します。

 

モデルクラスをライブラリ化と言っても、どんな感じなのか想像できない人もいると思います。そこで、大まかな姿を最初に紹介します。それが想像できないと、これからの話が意味不明に感じるかもしれませんので。

ライブラリ化するのですから、モデルクラスで扱うデータの種類は、それぞれのアプリで異なります。まずは基本的なデータ型である、Int、Float、Bool、Stringを考えます。アプリ側では、この中のどれをどんな組み合わせで使うのか、モデルクラスのインスタンス生成時に指定します。データベースのフィールド定義と似たようなものです。このような形で作ると、モデルごとに異なるデータ型の組み合わせに対応でき、より多くのモデルで利用できるように仕上げられます。

インスタンス生成でデータ型の定義が終わったら、どんどん使い始めます。モデルクラスに用意されたメソッドで、新しいデータを追加したり、前に追加したデータを取得したり、データの一部を更新したりします。つまり、データ型の組み合わせ設定を用意し、それでインスタンス生成した後は、ただ使うだけなのです。

ライブラリ化したクラスで、付加機能を最初から用意すれば、さらに使いやすくなります。たとえば、入力したデータをタブ区切りのテキストに変換してくれる機能を付けると、そのデータをアプリ側でコピーして、他のアプリ(表計算アプリなど)へ持っていけます。また、少し前に紹介したファイル保存の機能に対応させておけば、そのメソッドを呼び出すだけでファイルやネットへの保存もできます。わざわざ作る必要はありません。

おそらく一部のアプリでは、特別に追加したい機能も出てくるでしょう。その場合は、ライブラリ化したモデルクラスのサブクラスを作って、必要な機能を追加する方法が可能です。独自要望の多くは、この方法で何とかなると思います。

 

ライブラリ化すると良いことが多いものの、上手に作らないと、デメリットも生じます。その点も最初から考えておかなければなりません。

真っ先に思い浮かんだのが、データ型の検査です。複数のデータ型を自由に組み合わせる場合、AnyやAnyObjectなどの汎用データ型に入れて扱いたくなります。モデルクラス内だけでなく、getなどのメソッドでも汎用データ型を使うと、もう大変です。コンパイラのデータ型検査機能が無効になり、実行時エラーの続出といった悲惨な状況へ向かうかもしれません。できる限り汎用データ型を使わず、データ型検査機能を有効に働かせることが大事です。

と書きましたが、実際に作るとなると大変です。なにしろ、データ型を自由に組み合わせて使うわけですから、汎用データ型を使わずに作ると、組み合わせ数が膨大になりそうです。どうすれば良いのでしょう。

ライブラリ化する際には、ライブラリではない形で作るよりも多くのエラー検査機能を入れます。でもモデルクラスの場合、アクセスが多いことも考えられ、検査が多いと処理が遅くなってしまいます。どの程度の検査を入れるのか、注意深く決める必要があるでしょう。現実的には、検査内容は最低限だけに絞り、あまり入れない方向で進めると思います。

 

以上の点を踏まえた上で、いよいよ具体的なモデルクラスの話に移りましょう。

どんなモデルを最初に作るかは、決めてあります。もっとも簡単そうなモデルを選びました。記録カードを扱うようなモデルをです。

そのカードは記入内容が予め決められていて、用意された欄にどんどん記入し、カードが増えていくような感じです。言ってみれば、カード型データベースからデータベース機能を抜いたようなモデルでしょう。一般的には、記録カードと呼ばれるものです。

もう少し詳しく書きましょう。まず最初に、カードに記載する内容を決めます。何個の項目を含み、それぞれの項目のデータ型が何なのかを決めるわけです。それぞれの項目に名前も必要でしょう。

具体的には、次のような感じになります。ゲームなどのイベントで、参加者の得点を記録するカードの情報を考えます。会員番号はInt、氏名はString、得点はFloat、特別会員はBoolという感じです。これを参加した人の数だけ、カードへ毎回入力します。どんどん入力すると、参加人数と同じ数のカード情報が保存されます。

別な例として、アクセスログなんかも考えられます。日付はIntに変換、時刻もIntに変換、IPアドレスはStringに変換、アクセス種類はString、アクセス内容もString、エラー有無はBoolとかです。

たいていの情報は、4つの基本データ型に変換して保持できます。それ以外のクラス・インスタンスも扱えるようにするかは、4つのデータ型で作った後に検討することにしました。まずは、4つのデータ型で使えるように仕上げるのが先決です。

 

今回は、どんな内部構造で作ったら良いのか、まったく想像できませんでした。そのため、とりあえず作りながら考えることとしました。どんなメソッドが必要なのかとか、何も考えずに作り始めたわけです。

真っ先に検討したのが、クラス内でのデータの持ち方です。複数の項目があって、それぞれデータ型が異なります。また、どの項目なのか外部から指定できる必要もあるでしょう。項目番号で指定するのか、名前で指定するのか。

最初は、項目番号で指定することを考えました。いろいろなデータ型を入れるのですが、データ型を検査するためには、AnyやAnyObjectを使えません。そのデータ型の配列を用意するしかないでしょう。そして、用意した個々の配列を管理するための親配列も追加しました。親配列は[Any]で用意し、その要素にデータ型の配列([Int]や[String])を入れるという形です。

データ型の配列は追加などがあるので、Varで初期化します。昔のソースコードは決して残ってないので、肝心の部分だけ思い出しながら書くと、次のような感じだったと思います。なお、変数名は完全に忘れたのでデタラメです。

// 親配列に、データ型の配列を入れる(思い出しながら書いたので、一部が間違っているかも)
var cAryMain: [Any] = [Any]()
// 子配列を作って、親配列に入れる
var iAryInt: [Int] = [Int]()
var iAryFloat: [Float] = [Float]()
cAryMain.append(iAryInt)     // 最初の項目はInt型
cAryMain.append(iAryFloat)   // 2番目の項目はFloat型

ここまでは問題なく作れました。さっそく次のような形でデータを追加しようとしたところ(これも思い出しながら書いているので、一部が間違っているかもしれません)、appendの行がコンパイルエラーになりました。

// データ型の配列に要素を追加したいが、コンパイルエラーに(同じく、一部が間違っているかも)
let iInt: Int = 100
(cAryMain[0] as! [Int]).append(iInt)  // この行がエラーに

英語のエラーメッセージから推測すると、どうやら配列[Any]に入れてしまった配列[Int]では、appendなどが使えなくなるようです。間違った使い方を防止するために、安全装置的な検査が組み込まれているのでしょうか。これでは、まったく使い物になりません。

諦めきれないので、もう少し粘ってみました。もしかして、参照渡しでアクセスすると、コンパイルエラーが出ない可能性があるかなと。そこで、cAryOya[0]を関数の引数として渡し、inoutを指定してみました。残念ですが、結果は同じでした。

他にも、思い付いた方法で試してみたのですが、最終的にはコンパイルエラーが出てしまいます。やはり、どこかで安全装置が働いて、許してくれないのでしょう。変数[Int]を変数[Any]に入れる方法は、完全に諦めました。

ここまでの段階で、他のメソッドなども少し作っていました。中心となる構造が使用不可になったため、すべて最初からやり直しです。

 

気を取り直して、もっと確実な方法で作ることにしました。データを入れる配列[Int]や配列[Float]などは、配列[Any]に入れず、配列[Int]や配列[Float]のままで扱うことにしました。これが一番確実な方法ですから。

ただし、そのままだと何番目の項目がどこに入っているか不明なので、参照インデックスの役目を持つインデックス用配列[Int]を用意します。このインデックス用配列に、それぞれのデータ配列のインデックス番号を記録しておきます。これとは別にデータ型を示す配列変数も用意します。これら2つが管理用の配列で、両方の値を参照すれば、どのデータ型なのかと、どのデータ位置に保存されているのかが分かるというわけです。

具体的に仕上がったコードは、次のようになりました。

// 管理用の配列
var cAryType: [ZmType] = [ZmType]()  // 項目のデータ型の定義
var cAryDataIdx: [Int] = [Int]()     // データ配列内のインデックス番号
// データを入れる配列
var cAryDataInt: [[Int]] = [[Int]]()        // データ配列
var cAryDataFloat: [[Float]] = [[Float]]()
var cAryDataBool: [[Bool]] = [[Bool]]()
var cAryDataStr: [[String]] = [[String]]()

この中のZmTypeは、データ型を指定するために作った列挙型です。決められたデータ型しか指定できないようにするためです。

ここで、データ型の定義として「Int,Float,Int,String」が指定されたとします。するとcAryDataIdxには、[0, 0, 1, 0]の値が入ります。意味としては「Int型のインデックス0、Float型のインデックス0、Int型のインデックス1、String型のインデックス0」となります。データ型の定義にIntが2つ含まれるので、Intだけはインデックス0とインデックス1が作られたわけです。

4つのデータ型の配列は、どれも2次元で作りました。それぞれ複数の項目で使われる可能性があるからです。「Int型のインデックス0」で使うときは、cAryDataInt[0][0], cAryDataInt[0][1], cAryDataInt[0][2], ...とアクセスし、「Int型のインデックス1」で使うときは、cAryDataInt[1][0], cAryDataInt[1][1], cAryDataInt[1][2], ...とアクセスします。

どのデータ配列もデータ型が指定されているので、データ型が決まった形で使われます。また、すべてのメソッドを、データ型を指定した形で作ります。結果として、コンパイラがデータ型を検査できる形に仕上がりました。設計上の一番大事な点を、何とかクリアーできました。

 

キリが良さそうなので、ここで一旦区切ります。具体的なSwiftコードは、次回の投稿で紹介します。今回の内容は全体設計という形ではないため、「全体設計編」ではなく「全体検討編」としました。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年6月15日月曜日

ファイルの読み書きも間接参照で(追加修正編)

ファイルの読み書きを間接参照で作る話は一旦終わったのですが、使う際の状況をよく考えたら、リンククラスに親クラスが必要だと気付きました。その辺の話を追加で書きます。

 

本題に入る前に、私の普段の作り方を少しだけ。

似たようなクラスを複数作る場合、共通部分を親クラスとして最初に作り、異なる部分はサブクラスとして追加するでしょう。私も同じように作りますが、最初から親クラスを用意することはほとんどありません。似たクラスを作るかどうか不確かなので、似たクラスを作る段階になってから、親クラスを作るのが基本です。

このようにする理由は、無駄な作業を少しでも減らしたいからです。共通部分を親クラスに持つといっても、どの辺までが共通なのかは、似たクラスを実際に作る段階で明確になります。ですから、明確になったときに親クラスを作ったほうが、全体としての作業量が最小限で済みます。そもそも、似たクラスを作るかどうかが、不確実ですから。こう考えているため、最初から親クラスを作ることは、めったにありません。

 

で、今回のリンククラスです。いつもと同じように考えて、親クラスを用意しませんでした。

しかし、よくよく考えると、切り替え可能に作ることが大前提のリンククラスです。切り替え時の変更を最小限に済ませるなら、最初から親クラスを用意するのが、もっとも効率的だと思います。もちろん、切り替え用のリンククラスを作るとき、モデルクラスの中まで含めて変更しても構わないのですが、モデルクラスを変更せずにと考えている以上、やっぱり親クラスは必要でしょう。それに、リンククラスの仕様変更は、生じそうもないですし。

 

というわけで、さっそく親リンククラスを作ってしまいました。出来立てです。

まず、親クラスの役割を説明します。親のリンククラスは、リンククラスの共通部分を持っていますから、どのリンククラスにも使える機能を持ちます。そして、モデルクラスのように、サブのリンククラスを切り替えて使う側に利用されます。このような役割なので、プロトコル的なクラスと考えれば理解しやすいでしょう。

切り替えて使われるサブのリンククラスは、親クラスとは異なる機能を持つだけでなく、共通機能の実装という役割も持ちます。これら2つのうち後半の役割は、プロトコルの実装と考えれば理解しやすいでしょう。

親子クラスなのですが、リンククラスは間接参照が目的なので、プロトコル的な形になるのだと思います。でも、親子クラスとして実現するほうが、作りやすくて扱いやすいので選びました。

 

さて、実際のSwiftコードです。データ形式が4つありますから、それぞれに親クラスを作らなければなりません。

まずは、テキストを扱うリンククラスから。次のようなSwiftコードになりました。

// ============================== 文字列をファイルへ読み書き
// 親クラス(切り替えて利用する側で使う)
class ZSLText2Storage {
    // アプリ内のリソースからロード
    func loadDefaultF() -> String? {
        return nil
    }
    // ロード
    func loadF() -> String? {
        return nil
    }
    // 保存
    func saveF(rStrData:String) -> Bool {
        return false
    }
}
// サブクラス(インスタンスを生成する側で使う)
class ZSLText2File: ZSLText2Storage {
    var cDir: ZFDir = .Docs              // 仮の値
    var cFileName: String = "dummy.txt"
    //初期化
    init(_ rDir:ZFDir, _ rFileName:String) {
        cDir = rDir
        cFileName = rFileName
    }
    // アプリ内のリソースから
    override func loadDefaultF() -> String? {
        let iFileNameN: String = cFileName.stringByDeletingPathExtension
        let iFileNameS: String = cFileName.pathExtension
        let iStr: String? = zFReadFileTextResF(iFileNameN, iFileNameS)
        return iStr
    }
    // 指定されたディレクトリから読み込む
    override func loadF() -> String? {
        let iStr: String? = zFReadFileTextF(cDir, cFileName)
        return iStr
    }
    // 指定されたディレクトリへ書き出す
    override func saveF(rStrData:String) -> Bool {
        let iBool: Bool = zFWriteFileTextF(cDir, cFileName, rStrData)
        return iBool
    }
}

親クラスには、共通する3つのメソッドだけで、初期化は含まれていません。リンククラスの初期化は、保存先が何なのかに依存するため、共通化できないからです。

3つの共通メソッドは、基本的に何もしません。ただし、処理が失敗したときの戻り値を返すように作ってあります。上書きされない場合でも、それなりに良い形で正常に動作するようにとの配慮です。「上書きされてません」とエラーメッセージを出すことも可能ですが、それはやりすぎでしょう。

サブクラスは、基本的に何も変えていません。親クラスが持つ3つのメソッドを上書きするので、メソッドの先頭にoverrideを加えただけです。機能的にも同じですし、使い方も前と変わりません。

 

続いて、NSDictionaryを扱うリンククラスです。こちらも親クラスを追加して、次のようなSwiftコードになりました。

// ============================== NSDictionaryをファイルへ読み書き
// 親クラス(切り替えて利用する側で使う)
class ZSLDic2Storage {
    // アプリ内のリソースからロード
    func loadDefaultF() -> NSDictionary? {
        return nil
    }
    // ロード
    func loadF() -> NSDictionary? {
        return nil
    }
    // 保存
    func saveF(rNSDic:NSDictionary) -> Bool {
        return false
    }
}
// サブクラス(インスタンスを生成する側で使う)
class ZSLDic2File: ZSLDic2Storage {
    var cDir: ZFDir = .Docs                // 仮の値
    var cFileName: String = "dummy.plist"
    //初期化
    init(_ rDir:ZFDir, _ rFileName:String) {
        cDir = rDir
        cFileName = rFileName
    }
    // アプリ内のリソースから
    override func loadDefaultF() -> NSDictionary? {
        let iFileNameN: String = cFileName.stringByDeletingPathExtension
        let iFileNameS: String = cFileName.pathExtension
        let iNSDic: NSDictionary? = zFReadFileDicResF(iFileNameN, iFileNameS)
        return iNSDic
    }
    // 指定されたディレクトリから読み込む
    override func loadF() -> NSDictionary? {
        let iNSDic: NSDictionary? = zFReadFileDicF(cDir, cFileName)
        return iNSDic
    }
    // 指定されたディレクトリへ書き出す
    override func saveF(rNSDic:NSDictionary) -> Bool {
        let iBool: Bool = zFWriteFileDicF(cDir, cFileName, rNSDic)
        return iBool
    }
}

変更点は、テキストを扱うリンククラスと同じです。説明は不要でしょう。

 

このように親リンククラスを追加したので、一部の使い方は変更になります。それは、リンククラスを交換して使うモデルクラスです。今まではリンククラスのデータ型としてサブのリンククラスを指定していましたが、その代わりに、親のリンククラスを指定します。たったこれだけです。

モデルクラスが、親のリンククラスを扱う形になるので、すべてのサブのリンククラスを切り替えて使えるようになり、モデルクラスの変更は生じません。リンククラスの仕様が変更されない限り、モデルクラスはそのまま使い続けられます。

モデルクラス以外では、前に説明したとおりの使い方です。中央処理では、サブのリンククラスとしてインスタンスを生成し、モデルクラスに渡します。また、一括バックアップなどの目的で、サブのリンククラスを操作するときも、前と同じように操作できます。つまり、親クラスの追加で変わったのは、モデルクラス内のデータ型の指定だけなのです。

 

当初の予定外に、親のリンククラスを追加することになりました。ちょっと気付くのが遅かったですが、早めに気付いて良かったです。モデルクラスを使う部分は実際に作ってないため、気付くのが遅くなったのでしょう。もし作ってれば簡単に気付いたのですが。まあ、こんなことは、たまにありますね。素直に修正することが大事です。

以上のように、リンク先の切り替え部分も、将来の変更が最小限になりました。これからも、間接参照の考え方を積極的に活用して、いろいろな機能を実現したいと思います。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年6月14日日曜日

ファイルの読み書きも間接参照で(本編+使用例編)

ファイルの読み書きを間接参照で作る話の続きです。前回は作成理由を書いたので、いよいよSwiftコードを紹介します。

 

その前に、ファイルを読み書きするライブラリの話を少しだけ。

今回のリンククラスを作る前に、ファイルの読み書きライブラリを更新しました。今まではクラスとして作っていたのですが、よく考えると、値を保持する必要はありません。ということは、UI部品の生成関数と同じく、グローバル関数で作ったほうが適しています。グローバル関数なら、インスタンス化が不要となり、何の準備もせずに使い始められますから。また、アプリの初期処理で、インスタンスを生成する順番を気にせずに済みます。

あとエラー処理に関する話も少しだけ。ファイルの読み書きでは、NSErrorを指定できるメソッドの場合、必ず指定しています。処理後にNSErrorがnilでなければ、内部的にエラーメッセージを出して、そのまま処理を終わらせます。読んだ結果を返すメソッドでは、nilが返ることになります。

このような形で作ってあるため、この読み書きライブラリを使う側は、基本的に内部向けのエラーメッセージを出しません。ユーザーに何かを伝える必要があるときだけ、画面上にメッセージを出します。

具体的なSwiftコードも1つだけ。アプリ内のリソースから、テキストファイルを読み込む関数です。

// アプリ内リソースから、テキストファイルを読み込む
func zFReadFileTextResF(rFName:String, rFNameSuf:String) -> String? {
    let iPathFile: String? = NSBundle.mainBundle().pathForResource(rFName, ofType:rFNameSuf)
    if iPathFile == nil { zSendErrMsgF("ERROR:BPF_RFR:パスの生成に失敗:ファイル名を確認"); return nil }
    var iErr: NSError? = nil
    var iNSStr: NSString? = NSString(contentsOfFile:iPathFile!, encoding:NSUTF8StringEncoding, error:&iErr)
    if iErr != nil { zSendErrMsgF("ERROR:BPF_RFR:NSError=\(iErr)") }
    return iNSStr as String?
}

見てのとおり、NSErrorに何かが入っていると、内部的なエラーメッセージを出します。NSErrorの内容もテキスト化して出力されるので、エラーの原因究明に役立つはずです。

この関数では、ファイルを含んだパスを生成する箇所でも、エラーチェックを入れています。これは普通のファイルでは不要ですが、アプリ内のリソースから読む場合には必須となります。もし存在しないファイル名を指定すると、パスの値がnilになり、続きの読み込み処理でクラッシュするからです。クラッシュを避け、原因を知らせるために入れています。

他の読み書き関数も、似たような作り方を採用しました。また、ディレクトリとファイル名を指定してパスを生成する処理などは、共通の関数として別に用意し、いくつもの読み書き関数で使っています。

このライブラリの関数を使ったとき、処理が成功したかどうかは、戻り値を見て判定します。読み込み関数では、読み込んだ結果がnilなら処理の失敗です。書き出し関数では、戻り値のBool値がfalseなら処理の失敗です。

 

ようやく本題に入ります。まずは、テキストを扱うリンククラスから。次のようなSwiftコードになりました。

// ============================== 文字列をファイルへ読み書き
class ZSLText2File {
    var cDir: ZFDir = .Docs              // 仮の値
    var cFileName: String = "dummy.txt"
    //初期化
    init(_ rDir:ZFDir, _ rFileName:String) {
        cDir = rDir
        cFileName = rFileName
    }
    // アプリ内のリソースから
    func loadDefaultF() -> String? {
        let iFileNameN: String = cFileName.stringByDeletingPathExtension
        let iFileNameS: String = cFileName.pathExtension
        let iStr: String? = zFReadFileTextResF(iFileNameN, iFileNameS)
        return iStr
    }
    // 指定されたディレクトリから読み込む
    func loadF() -> String? {
        let iStr: String? = zFReadFileTextF(cDir, cFileName)
        return iStr
    }
    // 指定されたディレクトリへ書き出す
    func saveF(rStrData:String) -> Bool {
        let iBool: Bool = zFWriteFileTextF(cDir, cFileName, rStrData)
        return iBool
    }
}

いつものように、要点だけ簡単に説明します。

初期化の処理で、ディレクトリとファイル名を指定します。これで保存先が決まりますから、このインスタンスを使う限り、保存先を意識する必要がなくなります。

メソッドは3つだけです。デフォルト値のファイルは、アプリ内のリソースに入れますから、そこから読むグローバル関数を呼び出します。残りの読み書きでは、初期化で設定したディレクトリとファイル名を指定します。

どのメソッドでも、ファイル読み書き関数の戻り値を、そのまま返しています。そのため、ファイル読み書き関数と同じ方法で、処理の失敗を呼び出し側で判定できます。

ディレクトリの指定は、好きな値を使えるわけではありません。ファイルの読み書きライブラリで用意した、列挙型の値を使っています。「.Docs」はDocumentsフォルダを意味しています。このクラスを初期化する際にも、同じ列挙型の値を使います。

 

続いて、NSDictionaryのデータを扱うリンククラスです。次のようなSwiftコードになりました。

// ============================== NSDictionaryをファイルへ読み書き
class ZSLDic2File {
    var cDir: ZFDir = .Docs                    // 仮の値
    var cFileName: String = "dummy.plist"
    //初期化
    init(_ rDir:ZFDir, _ rFileName:String) {
        cDir = rDir
        cFileName = rFileName
    }
    // アプリ内のリソースから
    func loadDefaultF() -> NSDictionary? {
        let iFileNameN: String = cFileName.stringByDeletingPathExtension
        let iFileNameS: String = cFileName.pathExtension
        let iNSDic: NSDictionary? = zFReadFileDicResF(iFileNameN, iFileNameS)
        return iNSDic
    }
    // 指定されたディレクトリから読み込む
    func loadF() -> NSDictionary? {
        let iNSDic: NSDictionary? = zFReadFileDicF(cDir, cFileName)
        return iNSDic
    }
    // 指定されたディレクトリへ書き出す
    func saveF(rNSDic:NSDictionary) -> Bool {
        let iBool: Bool = zFWriteFileDicF(cDir, cFileName, rNSDic)
        return iBool
    }
}

見てのとおり、文字列のリンククラスとほぼ同じです。扱っているデータがNSDictionaryに変わっている点と、ファイルを読み書きする関数がNSDictionary用に入れ替わっている点だけが違います。

つまり、もっとも異なるファイルへの読み書き部分が、ファイル関係のライブラリに入っているので、それを呼び出す側は関数名が違うだけになります。結果としてリンククラスは、保存場所やファイル名を保持し、間接参照を担当するだけになっています。まさに目的どおりの機能です。

 

本題とは関係ないですが、このソースコードを見て、return文の書き方に無駄が多いと感じた人もいるのではないかと思います。iNSDicやiBoolという途中の変数を使わず、return文に関数を直接書いたほうが、ソースコードが短くなると。

この途中の変数は、あえて入れています。関数の戻り値のデータ型を、ソースコード上で明示的に記述したいからです。データ型の種類だけでなく、Optional型の有無まで含めて、何のデータ型で返されるのかを示しています。そのため、関数から返されるデータ型で書くのが決まりです。

背景となる考え方としては、データ型を強く意識したプログラミングをしたいのと、忘れたころに見ても思い出しやすく作りたいからです。関数の戻り値のデータ型が何かは、使う前に調べてますから、書いている段階では覚えています。でも少しでも時間が経つと、簡単に忘れてしまいます。この例のように、関数を使う際に一時的な変数を用意して、関数の戻り値と同じデータ型にすると、戻り値のデータ型を明示する効果があります。必ずletの変数にして、戻り値と同じデータ型にするのが必須条件です。

戻り値のデータ型が明確に分かると、データ型を強く意識したプログラミングが容易になります。Optrional型かどうか、キャスト可能かどうか、自動変換してくれるかどうかなど、きちんと作るための基礎情報が揃うからです。

自分自身が数年後にソースコードを見たときも、データ型が分かることで、処理の細かな点まで理解しやすくなります。当然、私以外の人がメンテするときも、同様に理解しやすいでしょう。

この辺のことは、あくまで私の考え方ですから、他人に勧める気はありません。

 

話を本題に戻しましょう。残りのリンククラスは、NSArrayとバイナリー(NSData)です。これらも、紹介した2つのリンククラスと非常に似ていますから、掲載は省略します。

どのリンククラスも、非常に単純なクラスに仕上がっています。もともと間接参照を実現するためのクラスですから、当然ですね。

 

では続いて、使用例を紹介しましょう。とても簡単です。iOS実験専用アプリでテストしたコードから一部を抜き出し、組み合わせて貼り付けます。

// アプリの初期処理内でインスタンス生成
let zData2Storage = ZSLText2File(.Docs, "test_data.txt")
let zPref2Storage = ZSLDic2File(.Docs, "test_pref.plist")

// アプリの中心処理内で使う(読み込み)
var iStrData: String = ...
if let iStr: String = zData2Storage.loadF() {
    // 正常に読み込めたときの処理
    iStrData = iStr
} else {
    // 読み込みが失敗したときの処理
    setMsgF("エラー:保存データの読み込みに失敗しました。")
    ...
}

// アプリの中心処理内で使う(書き出し)
let iResult: Bool = zPref2Storage.saveF(iNSDic)
if iResult {
    // 正常に書き出せたときの処理
    ...
} else {
    // 書き出しが失敗したときの処理
    setMsgF("エラー:データの保存に失敗しました。")
    ...
}

まず最初に、アプリの初期処理内でリンククラスのインスタンスを生成します。このとき、保存場所のディレクトリとファイル名を設定します。あとは必要に応じて、これらのインスタンスにロードまたはセーブのメソッドを実行します。

ロードの際には、正常に読み込めたかを確認します。nilが返ると失敗ですから、if let文を使えば切り分けが簡単です。

セーブの際にも、戻り値で成功と失敗を判定します。falseが返ると失敗なので、ユーザーにメッセージを出してから、失敗時の処理を続けます。

 

以上の使用例は、単純に使う場合です。もっと凝った作り方をする際の、拡張方法も簡単にですが取り上げてみます。

まずは、別な保存先へ切り替える場合です。今回のリンククラスと同じメソッドを持ったクラスを用意して、リンククラスを新しいクラスに置き換えるだけです。変数名が同じままなら、変数のデータ型を新しいリンククラス名に置き換えて、インスタンス生成の箇所を直すだけです。

このような変更を事前に予想し、リンククラスの変数名は、ファイルに関係ないものにしたいものです。「...2File」よりも「...2Storage」のほうが適しています。

次に、複数の保存先へ保存する場合も考えてみましょう。単純に切り替えるなら、同じメソッドの別なリンククラスを用意するでしょう。同じように、複数へ保存するリンククラスを作るのですが、すでにファイルへ保存するリンククラスがあります。同じコードを重複して書くのは好ましくないので、ファイルへのリンククラスのサブクラスを作って、2番目の保存先へ保存する処理だけ加えます。そのサブクラスへ切り替えるというわけです。

2つの保存先といっても、保存する先が2箇所だけで、ロードするときは片方だけでしょう。だとすると、上書きするのはセーブのメソッドだけになります。もしロードも2つから選ぶ場合は、既存のロードはメインの保存先としてそのまま残し、サブとなる保存先からロードするメッソドを追加する形が良いと思います。

 

一応の使い方を紹介しましたが、実際のアプリで使う際には、別な考慮点があります。それは、ファイルへ保存する情報を扱っているクラスとの関係です。

一般的な作り方として、情報をファイルへ保存するのであれば、その情報を扱っているクラスが存在します。MVCの考え方で言うと、Mのモデルに相当するクラスのはずです。保存するファイルごとに、関係するモデルのクラスがあるので、それぞれのファイルはどこかのモデルクラスと関連付けられます。

今回のリンククラスでは、インスタンスを生成する部分のコードに、ディレクトリとファイル名が含まれています。それをモデルクラスに入れてしまうと、モデルクラスは保存先情報を持ってしまいます。別な保存先に切り替えるとき、モデルクラス内のインスタンス生成コード部分で、ディレクトリとファイル名に相当する情報を書き換える必要が生じます。良い使い方とは言えません。

モデルクラスの役割は、ファイルへ保存するデータを生成する部分です。どこへ保存するかは含まず、保存する処理へ保存データを渡すだけなのが基本でしょう。

だとしたら、保存データを生成して渡すgetメソッドを作る方法が、真っ先に考えられます。アプリ全体をコントロールしている処理(中央処理と呼びましょうかね)が、モデルクラスから保存データを受け取り、リンククラスへ渡して保存します。リンククラスのインスタンス生成も、当然ながら、中央処理が担当します。

ロードも同様で、中央処理がリンククラス経由でロードし、モデルクラスに渡します。つまり、モデルクラス側では、ロードした出たを受け取るsetメソッドも必要になります。

モデルクラスが扱っている情報が複雑な場合は、保存ファイルが複数になることもあるでしょう。しかも、保存ファイルのデータ形式が同じとは限りません。今回用意した4種類の中で、複数のデータ形式を使うとかです。そうなると、モデルクラスで用意する保存データ用メソッドは、データ形式の種類分だけ増えることになります。美しくない方向へ、どんどん向かっているような気がします。

 

いろいろと悩みましたが、私が考えたベストは、次のような方法です。

まず、リンククラスのインスタンス生成は、中央処理が担当します。これによりモデルクラスは、保存先の情報を持たなくて済みます。

続いて大事な点ですが、モデルクラスと中央処理は、保存データの受け渡しをしません。保存する処理を実行するのは、モデルクラスです。中央処理は、モデルクラスが用意した保存メソッドを呼び出すだけで、呼び出されたモデルクラスが、リンククラスを呼び出して保存します。

では、モデルクラスとリンククラスの関係はどうなるのでしょうか。モデルクラスでは、中央処理が生成したリンククラスのインスタンスを、受け取るためのメソッドを用意します。つまり、setメソッドです。モデルクラスは、受け取ったリンククラスのインスタンスに対して、生成した保存データを渡して保存させたり、ロードしたデータを受け取って、自分自身の内部データを一括更新したりします。

このような構造だと、モデルクラスはリンククラスに依存します。もしリンククラスの仕様が変更になれば、モデルクラスも変更しなければなりません。ただし、リンククラスは非常に単純なので、仕様変更が発生する可能性はほぼゼロでしょう。つまり、実用上は、依存している点が問題にならないということです。

この方法には、別な良い点もあります。モデルクラスは、保存先のリンククラスを設定するメソッドを持っています。そのおかげで、保存先となるリンククラスを簡単に切り替える機能を持つことになるのです。それによって、複数の保存先へ保存する処理も作りやすくなります。

たとえば、次のような方法で。中央処理が最初に、保存先となるリンククラスのインスタンスを複数用意します。複数へ保存したいときは、最初のリンククラスをモデルに設定してから、保存メソッドを呼び出します。続いて、2番目のリンククラスを、同じモデルクラスに設定し、その後に保存メソッドを再び呼び出します。このように実行すれば、現状のリンククラスのままで(リンククラスを複数保存へ対応させずに)、複数の保存先へ保存する処理が簡単に作れます。

1つのモデルクラスで、複数のファイルを保存する場合も、少し考えてみましょう。複数のファイルですから、リンククラスのインスタンスも複数生成し、それぞれをモデルクラスに設定します。ただし、保存やロードはまとめて行って構わないケースがほとんどでしょうから、保存やロードのメソッドは1つだけにまとめることになります。この形だと、メソッド数が増えすぎて困ることもありません。

 

以上のような形で作るとすると、すぐに思い付くのは、リンククラスを扱う機能だけプロトコルにできないかということでしょう。当然、考えました。でも、すぐに諦めました。

今回のリンククラスは、とりあえず作っただけでも4種類のデータ形式があります。それに加えて、1つのモデルクラスが複数のデータを保存することもあるでしょう。それらが同じデータ形式とは限りません。つまり、何個を保存するのか、それぞれどんな形式なのか、先に問い合わせて、その回答内容に合った保存命令を出す必要があります。

プロトコル作り以上に、そのプロトコルで保存命令を出す側の処理が、かなり複雑になりそうです。やっぱり、それぞれのモデルクラスごとに適したメソッドを用意して、それに合わせて呼び出すのが現実的と言えそうです。もちろん、メソッドの形式は統一しますが。

 

もう1点、リンククラスのインスタンスの管理も、アプリ側での作り方で考慮すべき点です。

リンククラスのインスタンスは、どれかのモデルクラスへ関連付けられます。モデルクラスもインスタンスを生成しますから、それと同じ場所で生成する方法もあるでしょう。

しかし私は、リンククラスのインスタンスだけ、まとめて生成する方法が好きです。アプリの初期化処理の中で、すべての保存ファイル用のインスタンスを生成し、そのコードを一箇所にまとめて書きます。まとめて書くことで、どんなファイルを使っているのか、一目瞭然になるからです。

また一箇所にまとめると、一括バックアップなどの処理が作りやすくなります。リンククラスのインスタンスを並べ、それぞれのロードメソッドで保存データを受け取り、別な場所へ一括保存するといったバックアップ処理です。ファイルだけをまとめて管理しやすいので、こうした方法が好きです。

 

以上で説明は終わりです。後半のほうは、説明だけになってしまいましたので、要点だけでも読み取ってください。

ファイルの読み書きを間接参照で作るという狙いで、今回のようなリンククラスを作りました。間接参照の実現方法は、人によってつくり方が違うと思います。もし今回の考え方に興味を持った場合は、自分なりの間接参照を設計してみたらいかがでしょう。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年6月11日木曜日

ファイルの読み書きも間接参照で(作成理由編)

アプリで作成したデータを保存する場合、ファイルとして書き出すことが多いでしょう。作成データだけでなく、アプリの設定内容なども、ファイルとして保存することが多いはずです。

こうしたファイルに関する読み書きは、独自ライブラリとして用意し、それを使うことで、似たようなコードを何度も書かなくて済みます。ここまでなら、ライブラリ化が好きな人だと普通に行っているでしょう。

では、さらに1歩進めて設計するとしたら、どのような構造になるでしょうか。その辺の検討結果と、実際に作ったクラスを紹介ます。

 

まずアプリの一般的な構造を考えましょう。アプリの中心部分では、そのアプリに固有の情報を扱っていて、画面に表示したり、入力を求めたりします。入力されたデータや、外部から取得したデータを保存するとき、ファイルへ書き出すのが一般的です。

保存先としては、ファイル以外でも可能です。たとえば、ネット上のサーバーとか。つまりファイルとは、情報の保存先の1つでしかありません。

いくつかの中の1つである以上、切り替え可能なように作るのが良い作り方でしょう。保存先を単純に切り替えるだけでなく、同じ情報を複数に保存するとか、その片方への保存は1日に1回だけとか。切り替え可能な形で作っておくことで、保存する機能の拡張が格段にやりやすくなります。

つまり、アプリの内部構造として、中心部分の処理から、保存する処理を分離したいわけです。中心部分の処理では、どこに保存するのか、どんな名前で保存するのか、まったく知らない形となります。保存データを作った際には、保存する処理にデータを渡すだけで、あとは知らないという感じです。

 

以上のように考えた結果、ファイルの読み書きを間接参照で作ることにしました。ファイルの読み書きの基本的な部分は、すでにライブラリ化してあります。現状のアプリでは、それを直接呼び出しているのですが、今後は間接参照に変更しようということです。

間接参照をどのように作るのかが、一番重要です。大事な機能としては、保存先を切り替え可能に作る点があります。つまり、アプリの中心的な処理では、どこへ保存するのか知らなくても動くように作り、保存する処理が複数あって、それらが交換可能になるという形です。

そこで、間接参照を実現するためのリンククラスを用意することにしました。アプリ側では、このリンククラスのインスタンスを生成し、そのインスタンスへアクセスすることで、ファイルへの読み書きを実現します。具体的には、このリンククラスが、ファイルの読み書きのライブラリを呼び出して処理します。今まではアプリが、ファイル関連のライブラリを直接呼び出していましたが、改良版では、アプリがリンククラスを呼び出し、そのリンククラスがファイル関連のライブラリを呼び出します。また、どこへ保存するか、どんな名前で保存するかはリンククラスが持ち、アプリの中心処理では持たない形となります。

アプリがリンククラスのインスタンスを生成する際に、保存場所(ディレクトリ)とファイル名を指定します。そのため、ファイルとして保存するデータの数だけ、インスタンスを生成することになります。1つのファイル用に1つのインスタンスを生成するというルールです。

リンククラスで重要なのは、交換可能に作ることです。ネット経由での保存など、ファイル以外の保存先も考えられます。どの保存先でも共通のメソッドを持たせ、保存先が変更されても、ソースコードの変更を最小限に抑えます。

その実現で大事なのは、メソッド名と引数のデータ型です。これらが同じであれば、別なクラスに切り替えたとしても、修正箇所はたった2箇所だけになります。たとえば、変数名prefStorageで今回のリンククラスをインスタンス生成した場合、インスタンスへのアクセスは変数名prefStorageとメソッド名だけしか使いません。もしメソッド名が全部同じで、ネット上へ保存するリンククラスを別に用意したとき、次の2箇所だけ変更すれば良くなります。インスタンス生成するコードのリンククラス名を変更し、インスタンス生成で指定するする初期化の値(ディレクトリ位置やファイル名)を変えるだけです。それ以外のコードである、変数prefStorageへアクセスする箇所のコードは、変更しなくて済みます。このような形であれば、交換可能の良い実現方法でしょう。

以上のように考えると、ファイルの読み書きを強く意識したメソッド名(readやwrite)は適しません。もっと一般的なloadやsaveが、より適していると言えます。そこでリンククラスでは、メソッド名にloadとsaveを使うことにしました。

 

過去に作ったアプリを見ると、単純なloadとsaveだけでは不足です。デフォルト値を入れたファイルがあり、それを呼び出す機能も必要です。そこで、loadを2つ用意します。単純なloadと、デフォルト値のloadです。

以上を整理すると、次のようになります。とりあえず、ファイル編だけです。

リンククラスの主な仕様(ファイル編)
・保存先の情報を持つ:ディレクトリとファイル名
・初期化:保存先ディレクトリとファイル名を設定する
・保存メソッド:値を受け取ってファイルに保存する
・ロード・メソッド:保存されている値をロードする
・デフォルト値のロード・メソッド:デフォルト値をロードする

機能が少ないので、かなり単純なクラスになるでしょう。

 

さて、ここまでの話では、保存するデータの中身について触れていませんでした。保存データのデータ型が違うと、同じ処理を使えません。たとえば、ファイルから読み込む処理は、文字列、バイナリー、NSDictionary、NSArrayで違います。ファイルへ書き出す処理も同様です。つまり、扱うデータ型ごとに、別々なクラスを用意しないといけないわけです。

リンククラスの種類(ファイル編)
・文字列(文字コードはUTF-8)
・NSDictionary
・NSArray
・バイナリー

構造が簡単なクラスで、しかもファイルの読み書きライブラリが既に作成済みなので、作るのは簡単です。

それぞれのクラスでは、扱うデータの型が決められています。そのおかげで、コンパイラの型チェック機能が有効に働き、ちょっとした間違いなどをコンパイル時点で発見できるようになります。

 

リンククラスの作成理由を簡単に紹介するつもりでしたが、書いてみたら予想より長くなってしまいました。いつもの全体設計編というレベルではないので、作成理由編とし、ここで一旦区切ります。具体的なSwiftコードは、次の投稿にて。

2015年6月3日水曜日

モーダルビューをライブラリ化(本編2+使用例編)

モーダルビューをライブラリとして作った話の続きです。前回では、Swiftコードの説明が途中まででした。残りのソースコードを挙げながら、要点を説明します。さらに、簡単な使用例も紹介します。

 

いよいよ、肝となるボタンとレイアウトを取り上げます。ボタンはキャンセルとOKの2つがあり、基本的には次のように使います。

キャンセルボタンがタップされると、何もせずに、モーダルビューを閉じるだけです。

OKボタンがタップされると、設定された関数を実行します。初期状態では、このクラスの関数が設定されていますが、通常の使い方では、アプリ側で用意した関数を設定し、タップされたときに実行させます。

何かの登録処理でモーダルビューを使う際には、まだ何も選択されていない状態のとき、選択を促すメッセージを表示して、モーダルビューは開いたままにします。このように、開いたまま使い続ける場合もあり得るので、OKボタンの処理には、自動的に閉じる処理は付けていません。

OKボタンでモーダルビューを閉じたい場合には、アプリ側が設定した関数内から、閉じるメソッドを実行します。つまり、アプリ側の関数から、このクラスの閉じるメソッドを呼び出す形となります。

ボタンの初期化と、ボタンに関するメソッドは、次のようなSwiftコードになりました。

// ======================================== ボタン
var btnCancel: UIButton!
var btnOKay: UIButton!
var funcOKay: () -> Void = { () -> Void in return }

// 初期化
private func setupBtnF() {
    // キャンセルボタン
    btnCancel = zCreateBtnF("キャンセル", 20, 400, 550, 120, 50)
    btnCancel.addTarget(self, action:Selector("runCancelF:"), forControlEvents: .TouchUpInside)
    self.addSubview(btnCancel)
    // OKボタン
    btnOKay = zCreateBtnF("OK", 20, 600, 550, 120, 50)
    btnOKay.addTarget(self, action:Selector("runOkayF:"), forControlEvents: .TouchUpInside)
    self.addSubview(btnOKay)
    funcOKay = funcDummyF    // ダミーの警告関数を設定
    // サイズとレイアウトの設定
    setBtnSizeF()
    setBtnPositionF()
}

// キャンセル処理:アニメーションで自分自身を隠す
func runCancelF(rSender:UIButton) {
    closeF()
}
// OK処理:変数に代入した関数を実行する(Viewのclose処理は、必要な場合にアプリ側で行う)
func runOkayF(rSender:UIButton) {
    funcOKay()
}
// OKボタンのダミー処理:未設定をメッセージで知らせる
private func funcDummyF() {
    setMsgF("内部エラー:OKボタンに処理が設定されていません。")
}

// OK処理で実行する関数を設定
func setFuncF(rFunc:() -> Void) {
    funcOKay = rFunc
}
// 2つのボタン名を設定
func setBtnNameF(rStrC:String, _ rStrO:String) {
    btnCancel.setTitle(rStrC, forState: .Normal)
    btnOKay.setTitle(rStrO, forState: .Normal)
}

初期化の処理では、まず2つのボタンを生成します。このときの大きさや表示位置は、仮の値です。初期化の最後に、レイアウトを設定する処理を実行するからです。

ボタンがタップされたときに実行する関数も、初期化の処理内で設定します。キャンセルボタンのほうは、このモーダルビューを閉じるだけの処理です。

OKボタンのほうは、関数を入れる変数funcOKayに入っている関数を実行します。この変数をOptional型にしたくなかったので、変数宣言の際に、何もしない処理(クロージャ)を入れました。

この変数のデータ型は、「() -> Void」にしてあります。これは「() -> ()」とも書けるのですが、Swiftに詳しくない人が少しでも分かりやすいようにと考え、あえてVoidを選んでいます。2種類以上の表記が可能な場合、初心者が少しでも理解しやすい方を選ぶのが、私の方針だからです。

この変数に入れる関数は、アプリ側で必ず入れるものです。入れ忘れを気付きやすいように、ダミーの関数を用意して、それを初期化の処理で入れています。このダミー関数を実行すると、メッセージ欄に「内部エラー:OKボタンに処理が設定されていません。」と表示されます。いつものエラーメッセージと少し違うのは、画面上に表示するメッセージなので、ユーザーが読む可能性があるからです。

当然ですが、OKボタンに関数を設定するメソッドも必要です。関数のデータ型を指定してあるため、とくにエラー検査はしていません。

ボタン名を自由に変えたい要望もあるでしょう。2つのボタンを一緒に変えるメソッドの形にしました。おそらく最初の1回だけ実行するメソッドなので、メソッドをまとめたほうが効率的と判断したからです。

 

続いて、ボタンのサイズとレイアウトを設定する機能です。上記の初期化処理の最後でも呼ばれます。最初に、レイアウト機能に関する考え方を取り上げましょう。

今回のモーダルビューを使う際には、ボタンの座標を細かく指定しないと決めました。アプリ側では大まかな指定だけして、ビューのライブラリ側で自動レイアウトする形です。

アプリ側が指定する値としては、ボタンの大きさ、水平方向の広がりの大きさ、垂直方向の広がりの大きさの3つです。それぞれで、7種類の値から選ぶ形にしました。

どちらが大きいのか簡単に識別できるように、服などのサイズ指定で使われているS、M、Lを用いてます。このままだと3種類しかないので、3Sから3Lまで使って7種類としました。中央の値がMですが、これが標準の位置を意味するわけではありません。並び順として、単純に真ん中の値というだけです。もっと良い表現方法があれば良いのですが、思い付きませんでした。

以上の考えをまとめて作ったのが、以下のSwiftコードです。

// ボタンの大きさ
enum ZmvBtnSize: Int {
    case size_3S = 0
    case size_2S = 1
    case size_S = 2
    case size_M = 3
    case size_L = 4
    case size_2L = 5
    case size_3L = 6
}
// ボタン位置のX軸(横方向)の広がり
enum ZmvBtnPosiX: Int {
    case posiX_3S = 0
    case posiX_2S = 1
    case posiX_S = 2
    case posiX_M = 3
    case posiX_L = 4
    case posiX_2L = 5
    case posiX_3L = 6
}
// ボタン位置のY軸(縦方向)の広がり
enum ZmvBtnPosiY: Int {
    case posiY_3S = 0
    case posiY_2S = 1
    case posiY_S = 2
    case posiY_M = 3
    case posiY_L = 4
    case posiY_2L = 5
    case posiY_3L = 6
}

見てのとおり、列挙型の値として作ってあります。7種類から選ぶ項目が3つもあるので、大きさを示すSMLだけでは間違えやすくなります。そうならないように、SMLの前に値の名前を加えました。

 

いよいよレイアウトの実行コードです。

自動レイアウト機能を付けたので、その指定方法を最初に説明する必要があります。指定できる項目が3つあって、それぞれ7種類の値の中から選べることを、ソースコードの先頭で説明しています。

その後に、どんなレイアウトになるかを続けます。レイアウトのイメージが頭の中に浮かぶように、構造を上手に表現する必要があります。いつものように縦軸と横軸で、レイアウトの順に並べる形で変数名などを書きました。

このようなことを考えながら作ったのが、次のSwiftコードです。レイアウト説明の箇所では、変数とインデックスの関係を伝えるためのコメントも付けてあります。

// ======================================== サイズとレイアウトの変更
// 3種類の設定項目があって、それぞれ7つの値から選べる
// メッセージ欄は同じ大きさのまま、上下の位置だけ変わる。いつもボタン直下に置かれて、位置の指定はできない
// ボタンの大きさが7種類、X(水平方向)軸の中央のボタン間隔が7種類、Y(垂直方向)軸の下の余白が7種類

// 上記を整理したレウアウト説明
// ーーーーーーーーーー レイアウト(X軸:横方向):左右対称
// 余白:余り(可変)
let W_BUTTON: [CGFloat] = [120, 140, 160, 200, 240, 280, 320]        // ボタン:キャンセル:W_BUTTON[numBtnSize]
let W_MARGIN_CENTER: [CGFloat] = [50, 100, 150, 200, 250, 300, 350]  // 余白:W_MARGIN_CENTER[numBtnPosiX]
// ボタン:OK:W_BUTTON[numBtnSize](キャンセルと同じ大きさ)
// 余白:余り(可変:上の可変と同じ大きさ)
// ーーーーーーーーーー レイアウト(Y軸:縦方向)
// 余白:余り(可変:このビューへ追加で貼り付けるUI部品は、ここに配置する)
let H_BUTTON: [CGFloat] = [40, 45, 50, 55, 60, 70, 80]               // ボタン:H_BUTTON[numBtnSize]
let H_MARGIN_BOTTOM: [CGFloat] = [350, 300, 250, 200, 150, 100, 50]  // 余白:H_MARGIN_BOTTOM[ZmvBtnPosiY]

// ボタン内の文字サイズ:ボタンサイズに連動
let TEXT_SIZE_BTN: [CGFloat] = [16, 18, 20, 24, 28, 32, 36]          // TEXT_SIZE_BTN[numBtnSize]

// 上記の設定値:配列のインデックス
var numBtnSize: ZmvBtnSize = .size_M
var numBtnPosiX: ZmvBtnPosiX = .posiX_2L
var numBtnPosiY: ZmvBtnPosiY = .posiY_2L

// ボタンのサイズを計算して設定
private func setBtnSizeF() {
    // キャンセルボタン
    let iWidht: CGFloat = W_BUTTON[numBtnSize.rawValue]
    let iHeight: CGFloat = H_BUTTON[numBtnSize.rawValue]
    let iSize: CGSize = CGSizeMake(iWidht, iHeight)
    btnCancel.frame.size = iSize
    btnCancel.titleLabel!.font = UIFont.systemFontOfSize(TEXT_SIZE_BTN[numBtnSize.rawValue])
    // OKボタン
    btnOKay.frame.size = iSize
    btnOKay.titleLabel!.font = UIFont.systemFontOfSize(TEXT_SIZE_BTN[numBtnSize.rawValue])
}
// ボタンとメッセージ欄の位置を計算して設定
private func setBtnPositionF() {
    // キャンセルボタン
    let iX1: CGFloat = (screenWidth - W_MARGIN_CENTER[numBtnPosiX.rawValue]) / 2 - W_BUTTON[numBtnSize.rawValue]
    let iY1: CGFloat = screenHeight - H_MARGIN_BOTTOM[numBtnPosiY.rawValue] - H_BUTTON[numBtnSize.rawValue]
    btnCancel.frame.origin = CGPointMake(iX1, iY1)
    // OKボタン
    let iX2: CGFloat = (screenWidth + W_MARGIN_CENTER[numBtnPosiX.rawValue]) / 2
    btnOKay.frame.origin = CGPointMake(iX2, iY1)
    // メッセージ欄:左右の中央で、ボタン直下(余白は10)に付ける
    let iWidth: CGFloat = lblMsg.frame.size.width
    let iX3: CGFloat = (screenWidth - iWidth) / 2
    let iY3: CGFloat = iY1 + H_BUTTON[numBtnSize.rawValue] + 10
    lblMsg.frame.origin = CGPointMake(iX3, iY3)
}
// ボタンのサイズや位置を設定(それぞれ用意した種類から選ぶ)
func setBtnLayoutF(rNumBtnSize:ZmvBtnSize, _ rNumPosiX:ZmvBtnPosiX, _ rNumPosiY:ZmvBtnPosiY) {
    numBtnSize = rNumBtnSize
    setBtnSizeF()       // ボタンのサイズを設定
    numBtnPosiX = rNumPosiX
    numBtnPosiY = rNumPosiY
    setBtnPositionF()   // ボタンの位置を再計算
}

7種類の値を入れた変数のインデックスも、別な変数(名前がnumで始まる3つの変数)として用意してあります。その初期値としては、ボタンの大きさは真ん中の値、ボタンの位置は一番広い値にしました。これらの値を変更しない場合、ビュー上に置いたUI部品と重なりにくいように配慮したためです。通常の使い方としては、3つの値を必ず設定するはずですが、知らないで使い始めたときのためにです。

上記ソースコードの一番最後が、3つの値を設定するメソッドです。最初に設定したまま使うのが一般的なので、まとめて設定できるようにと、1つのメソッドにまとめました。値を設定するとともに、ボタンの大きさやレイアウトを再計算しています。

ボタンサイズの計算処理では、1つの設定値をもとに、ボタンの幅と高さ、中に表示する文字の大きさを決めています。あらかじめ用意した配列の値から持ってくる形です。細かな組み合わせは選べません。あまり細かく指定できても面倒ですし、変な組み合わせでは見栄えが悪くなります。バランスの良い組み合わせを事前に用意し、その中から選ぶ形が一番使いやすいと考え、このような形にしました。

ボタンとメッセージ欄のレイアウト計算処理では、X軸とY軸で余白を与える形にしてあります。

X(横)軸は、2つのボタンが左右対称に配置され、ボタンの間の余白を配列から持ってきます。ボタン幅とスクリーン幅が決まってますから、残りの長さを左右に分割して、それぞれのボタンのX軸の座標が計算できます。

Y(縦)軸は、下の余白の大きさを配列から持ってきます。ボタン高さとスクリーン高さが決まってますから、残りが上側の余白となり、ボタンのY軸の座標が計算できます。左右対称ですから、2つのボタンが同じ値になります。

メッセージ欄の位置は、常にボタンの下に来るようにしました。X軸は中央に配置するため、自分の幅とスクリーン幅から計算します。ボタンの位置や大きさに関係ありません。Y軸は、ボタンの下端から10だけ離れた位置にしてあります。

 

以上で、モーダルビューのソースコードは終わりです。機能は単純ですが、いろいろと考えて作っているため、ソースコードの量は、最初に思っていたより多くなってしまいました。やはり、ライブラリとして作ると、どうしても増えてしまうようです。

今回のライブラリの肝は、簡単に使える点です。この種のライブラリの場合、実現するための技術的な難しさは、ほとんどありません。思い付いたら、すぐに作り終わるはずです。

でも、実際に使いやすく仕上がるかどうかは、別な問題です。細かな座標を指定して使うなら、最初から作っているのと同じで、ライブラリを使う意味がありません。大まかな指定をするだけで、ボタンが左右対称に配置され、ほど良い感じに仕上げてくれることこそ、この種のライブラリ作成で、労力を一番注ぐべき点だと思います。

今後、より大きなスクリーンサイズのiPadが登場したときも、値を入れている配列に、より大きな値を追加するだけで対応できるでしょう。今の7種類が、9種類や11種類に増える形で。そうした点も考慮しながら、今回のライブラリは作りました。

 

続いて、使い方を紹介します。これを作るきっかけとなった、フォトライブラリから写真を選ぶ機能のライブラリ(当ブログにて以前に紹介)と一緒に使ってみます。つまり、写真を選ぶモーダルビューとして使うわけです。

いつもiOS実験専用アプリで開発し、そこでテストも実行していますから、その中の一部を抜き出して来ました。見やすく整えるのが面倒なので、そのまま紹介します。

まず、OKボタンがタップされたときに実行する関数を、アプリ側で用意します。当然ですが「() -> Void」の形式で作ります。

// OK(伝達)ボタンをタップしたときに実行される関数
func okay13bF() {
    if photoSelector13.isSelectedF() {
        if let iImg: UIImage = photoSelector13.getImageF() {
            // ここに、画像登録の処理を入れる
            // (たとえば、アプリ内の変数へ画像を入れるための処理を呼び出すとか)
            setMsgF(4, "画像を登録しました。")         // アプリ側のメッセージ欄に表示
        } else {
            setMsgF(4, "画像の取得に失敗しました。")   // アプリ側のメッセージ欄に表示
        }
        view13.closeF()
    } else {
        view13.setMsgWTimerF("画像を選択してください。", .Caution) // モーダルビューに表示
     }
}

見てのとおり、この関数がやり取りする主な相手は、モーダルビューではなく、その上に貼り付けられた写真選択機能(photoSelector)です。写真が選択されていなければ、選択を促すメッセージをモーダルビュー側へ出して終了です。モーダルビューは閉じません。

逆に、写真が選択されていれば、if letでUIImageへ代入を試みます。もし成功すれば、画像登録処理を実行し、アプリ側のメッセージ欄にメッセージを出して、モーダルビューを閉じます。代入が失敗した場合は、何もせずに、アプリ側のメッセージ欄にメッセージを出して、モーダルビューを閉じます。どちらの場合にでも、モーダルビューは閉じられ、成功または失敗のメッセージが、アプリ側に残ります。

 

関数が用意できたので、モーダルビューを扱うコードを作ります。モーダルビューとPhotoSelectorのインスタンスを入れる変数を用意して、準備から表示までのSwiftコードは、以下のようになります。

// モーダルビューの使用例:フォトライブラリから写真を選ぶ
var view13: ZModalView!               // モーダルビューの変数
var photoSelector13: PhotoSelector!   // 写真選択ビューの変数

// モーダルビューを用意して設定
view13 = ZModalView()
view13.setupF()                                      // 初期化
view13.setFuncF(okay13bF)                            // 関数の設定
view13.setBtnNameF("キャンセル", "登録")             // ボタン名の設定
view13.setBtnLayoutF(.size_S, .posiX_3L, .posiY_M)   // ボタンのサイズとレイアウトの設定
self.view.addSubview(view13)                         // アプリのビューへ貼り付け
// 写真選択ビューを用意して、モーダルビューに貼り付ける
photoSelector13 = PhotoSelector()
photoSelector13.setupF(50, 100, 900, 400)
view13.addSubview(photoSelector13)                   // モーダルビューへ貼り付け
// モーダルビューを開始
view13.openF()

見てのとおりです。コメントを多めに付けたので、何をしているのか分かるでしょう。ボタンのサイズやレイアウトを決める3つの値の設定でも、SMLに項目名を加えた効果が出て、理解しやすくなっています。

 

モーダルビューを使う際には、その処理全体を関数として用意し、インスタンスを入れる変数も関数内に作ります。そうすれば、モーダルビューを使い終わったとき、変数も一緒に消えて、使用していたメモリーが解放されます。

このようにメモリーの使用と解放を意識した作り方が、他の言語と同様に、Swiftでも必要となりますね。

 

今回は、かなり簡単な機能と言えるモーダルビューのライブラリ化を紹介しました。簡単な機能だからこそ、手間をかけずに使える工夫が重要です。また、将来の拡張性にも配慮しておかないと、後からのメンテが大変になります。

こんな簡単な機能でもライブラリ化できる例として、面白かったのではないでしょうか。今回の投稿を読んで「もしかして、これってライブラリ化できるかも」と思ったアナタ、さっそく作り始めてください。アプリを作るより、ライブラリを作る方が、意外に面白かったりしますので。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

2015年6月1日月曜日

モーダルビューをライブラリ化(全体設計編+本編1)

数回前の投稿では、フォトライブラリから写真を選ぶビューの試作ライブラリを紹介しました。その使い方の例として、モーダルダイアログのように表示して選ばせる方法を挙げました。

その投稿後に考えたのですが、写真選択などの機能を持ったUI部品ビューを、モーダルダイアログのように使う方法は、汎用性が少しありそうです。そこで、モーダルダイアログのように使う機能を、独自ライブラリとして作ってみました。ビューの形で作ったので、モーダルビューと呼ぶことにします。かなり単純な機能ですが、その内容を紹介します。

 

いつものように全体設計から始めたのですが、今回は全体像をラフに決めて、一気に作り始めました。まずは、ラフに決めた全体像から。

汎用的に使えるモーダルビュー
・全体構造:UIViewのサブクラスとして作る
・表示中のビューの上に、このビューを表示して、下のビューを操作できなくする
・ビューを表示するときは、アニメーションで少しずつ上に出てくる:閉じるときは逆の動き
・このビューの中に、特定の機能を持ったビューを貼り付けて使う
・最低限のボタンを用意する:キャンセルとOKの2つ
 ・キャンセルボタンは、このビュー自身を閉じるだけ
 ・OKボタンは、アプリ側で用意した関数を実行する
・ボタンの大きさや表示位置は、アプリ側で変更できる
 ・大きさや位置を細かく指定せず、簡単に使える形が望ましい

以上のような内容です。読んで分かると思いますから、わざわざ説明するまでもないですね。

 

実際に作り進めるうちに、不足する要素がすぐに見付かりました。途中の経過にあまり意味はないので、不足要素を先に挙げてしまいます。

まず、メッセージ欄が必要になりました。写真を選ぶ前にOKボタンをタップしたときなど、「先に写真を選んでください」とメッセージで知らせる必要があります。そのメッセージは、アプリ側の関数で設定するため、このライブラリ側に設定メソッドを用意しました。

最初は、OKボタンをタップしたとき、アプリ側の関数を実行した後、このビューを自動的に閉じるように作りました。しかし、写真を選んでくださいというように、そのまま表示し続けたい場合もありました。そこで、OKボタンの処理では、ビューを閉じないようにして、閉じるタイミングはアプリ側の関数に任せました。

ボタンのサイズや位置は、細かく指定しない設計方針です。それを実現する具体的な方法として、ボタンの大きさ、縦位置、横位置のそれぞれで、7種類から選ぶ方式にしました。これで大抵の要望を満たすでしょうし、より細かな配置までは求めないでしょう。

その他、細かな要素もあるのですが、それはソースコードを説明する際に取り上げます。結果として、上記の全体設計の内容は、次のように変更になりました。

汎用的に使えるモーダルビュー
・全体構造:UIViewのサブクラスとして作る
・表示中のビューの上に、このビューを表示して、下のビューを操作できなくする
・ビューを表示するときは、アニメーションで少しずつ上に出てくる:閉じるときは逆の動き
・このビューの中に、特定の機能を持ったビューを貼り付けて使う
・最低限のボタンを用意する:キャンセルとOKの2つ
 ・キャンセルボタンは、このビュー自身を閉じるだけ
 ・OKボタンは、アプリ側で用意した関数を実行し、閉じる処理は含まない
・ボタンの大きさや表示位置は、アプリ側で変更できる
 ・大きさや位置を細かく指定せず、簡単に使える形が望ましい
 ・具体的には、ボタンのサイズ・縦位置・横位置を、ぞれぞれ7種類から選んでもらう
・メッセージ欄を用意して、アプリ側からメッセージを設定できる

というわけで、以上の機能を満たすライブラリに仕上がっています。

 

いつものように、UIViewのサブクラスとして作ります。まずは、入れ物と初期化の部分だけ。次のようなSwiftコードになりました。

// 
class ZModalView: UIView {
    // 画面サイズ
    var screenWidth: CGFloat!
    var screenHeight: CGFloat!

    // ======================================== 初期化
    func setupF() {
        let iSize: CGSize = UIScreen.mainScreen().bounds.size
        screenWidth = iSize.width
        screenHeight = iSize.height
        self.frame = CGRectMake(0, screenHeight, screenWidth, screenHeight)
        self.clipsToBounds = true // 領域外は表示させない
        self.backgroundColor = UIColor.whiteColor()
        self.alpha = 0.95
        // メッセージ欄やボタンの初期化
        setupLabelF()
        setupBtnF()
    }
}

初期化の処理では、最初にスクリーンのサイズを求めています。iPadのスクリーンサイズは、レチナかどうかの違いはあるものの、まだ1種類だけです。しかし、より広いスクリーンのiPadが噂されています。それが登場したとき、変更が最小限で済むようにと、スクリーンサイズを意識して作っています。

ここでは、画面全体を覆うビューの大きさを、スクリーンサイズと同じに設定しました。また、ビューの位置でも、スクリーンサイズから計算し、表示領域のすぐ下に置いて、初期状態ではビューが見えないようにしてあります。

透明度を0.95に設定しているのは、以前に作ったときの経験からです。ビューの背景色が白の場合、この0.95が、後ろのビューが微かに見える値なのです。

このライブラリはUIViewのサブクラスなので、UIViewの属性がそのまま使え、インスタンス生成した後でも簡単に変更可能です。変更が必要な場合は、生成したインスタンスで、UIViewと同じように属性を設定します。

初期化の最後では、このビューに貼り付けるメッセージ欄やボタンの初期化処理を呼び出します。これらの並び順にも注意が必要です。ボタンのようにユーザーが操作する部品は、後のほうで呼び出すことが大切です。

貼り付ける部品には大きさがあり、少し重なっている場合もあるでしょう。初期化の処理では、インスタンス生成してビューに貼り付けているので、その呼び出し順で重なります。ですから、ボタンのように操作するUI部品は、後のほうで呼び出すのが基本です。今回のように呼び出し順に配慮すると、ボタンの端がタップできないといった余計なトラブルを予防できます。

 

ビュー上に付けるUI部品を取り上げる前に、ビューのオープンとクローズを。

// ======================================== 表示と終了(アニメ付き)
var animeTime: NSTimeInterval = 0.5    // アニメーション時間(秒)
var visible: Bool = false

func setAnimeTimeF(rTime:NSTimeInterval) {
    if rTime < 0.1 || rTime > 5  { zSendErrMsgF("ERROR:ZMV-BPXT:アニメーション時間が不適切"); return }
    animeTime = rTime
}

// 表示
func openF() {
    if visible { zSendErrMsgF("ERROR:ZMV-OPN:すでに表示している"); return }
    visible = true
    UIView.animateWithDuration(animeTime, animations: {self.frame.origin.y = 0} )
}
// 終了
func closeF() {
    if !visible { zSendErrMsgF("ERROR:ZMV-CLS:すでに表示されていない"); return }
    visible = false
    UIView.animateWithDuration(animeTime, animations: {self.frame.origin.y = self.screenHeight} )
}

アニメーション時間を変数に持ち、初期値を0.5秒にしました。この0.5秒は、過去に作った経験から求めたものです。あくまで主観ですが、アニメーションがほどほど綺麗に見えながら、スムーズに動いている秒数なのです。0.3秒だと速すぎてアニメーションが良く見えず、0.7秒だと遅すぎてややイライラすると感じましたから。

当然ですが、アニメーション時間を変更するメソッドも用意して、好みの状態で使えるようにもしてあります。変な値を設定したときに気付けるようにと、値の検査を入れました。明らかに変な値だと判断したら、値を設定せず、エラーメッセージを出します。

下から出てくるアニメーションなので、隠れている状態でも、ビュー自体は表示したまま画面の範囲外に置いてあります。そこで、意味としての表示中かどうかを、変数visibleに記憶させています。この変数は、機能を実現させるためだけには不要なのですが、オープンとクローズの操作が間違って使われているかどうかを検査するために用意しました。オープンとクローズで検査し、オープン中にオープンさせるとか、間違った操作を見つけるのに使います。バグまたはバグの兆候を見付けるのが目的です。

 

続いて、メッセージ欄です。初期化の処理と、文字列を設定するメソッドが含まれます。

// ======================================== メッセージ欄
var lblMsg: UILabel!

// 初期化
private func setupLabelF() {
    lblMsg = zCreateLblF("", 18, ALIGN_CENTER, 50, 100, 800, 30)
    self.addSubview(lblMsg)
}
func setMsgF(rStr:String) {
    lblMsg.textColor = COLOR_BLACK
    lblMsg.text = rStr
}
func setMsgF(rStr:String, _ rMsgType:ZbfMsgType) {
    zBase.setMsgF(lblMsg, rStr, rMsgType)
}
func setMsgWTimerF(rStr:String, _ rMsgType:ZbfMsgType) {
    zBase.setMsgWTimerF(lblMsg, rStr, rMsgType, 5)
}

初期化で生成したときの表示位置は、仮の値です。全体のレイアウトを設定する処理の中で、このメッセージ欄も表示位置が設定されます。

メッセージ欄に文字列を入れるメソッドは、3つも用意しました。単純に黒い文字で表示されるタイプの他に、メッセージの種類(成功,注意,エラー)の属性で色分けされたタイプと、さらに一定時間で文字列が消えるタイプの3種類です。ライブラリなので、多めに用意して使い分けられるように配慮しました。以前に紹介した、独自ライブラリを使っています。

 

残りは、このライブラリの肝となる、ボタンおよびレイアウト変更の機能です。この部分が一番長いですし、おそらくコード説明全体の半分以上になると予想されるので、ここで一旦区切ります。続きは、次回の投稿にて。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)