2014年12月26日金曜日

環境設定をライブラリ化(本編)

環境設定機能をライブラリ化する話の続きです。大まかな仕様が決まったので、実際のソースコードを作る段階です。小さなクラスですから、手短に紹介します。

 

まず、ファイル名を決めます。どのアプリでも使える共通ライブラリで、Swiftのクラスとして作りますから、ファイル名の先頭は「BC」しかありません。環境設定を短い言葉で表すと「Pref」なので、ファイル名は「BC_Pref.swift」としました。このファイルを、いつものようにiOS実験専用アプリへ追加して作ります。

最初は、入れ物となるクラスですね。クラス名は、とりあえず「PrefController」としてみました。このブログでも紹介した、日付を扱うクラス「DateController」と同じ、自作の「Controller」シリーズの1つです。

// 環境設定機能のクラス
class PrefController {
    let PREF_KEY_VER : String = "Version"

    var dictPref : Dictionary<String,AnyObject> = [String:AnyObject]()
    var prefFName : String = "dummy.plist"
    var strVersion : String = ""

    // 初期化
    init(_ rDictPref:Dictionary<String,AnyObject>, _ rFName:String, _ rVer:String) {
        dictPref = rDictPref
        prefFName = rFName
        strVersion = rVer
        // 環境設定ファイルの存在を調べて、同じバージョンがあれば入れ替え
        readPrefFileF()
    }
}

キー付きの値を入れるので、入れ物としてSwiftのデータ型Dictionaryを使います。当然、キーのデータ型はStringです。値のほうが4種類あるので、データ型はAnyObjectとなり、Dictionaryに入れるデータの型は「String,AnyObject」としました。

初期化の処理では、デフォルトの環境設定値(データ型はDictionary)と、環境設定ファイル名、バージョンを示すStringの3つを受け取って、それぞれ変数へ設定します。その後、以前に保存された環境設定ファイルを読み込む関数(後述)を呼び出します。

なお、バージョン番号を示すString型の変数rVerの使い方は、このライブラリを使うときに説明します。

 

続いて、環境設定ファイルの読み込みメソッドと、書き出しメソッドです。具体的なSwiftコードは、次のとおりです。

// ファイル関係の処理
// ファイル名を含んだ、Docフォルダのパスを生成
private func getFilePathDocsF(rFName:String) -> String {
    let iPathDocs : AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
    let iPathFile : String = iPathDocs[0].stringByAppendingPathComponent(rFName)
    return iPathFile
}
// 環境設定ファイルを読み込む
func readPrefFileF() {
    // 環境設定ファイルの存在を調べる
    let iPathFile : String = getFilePathDocsF(prefFName)
    let iFileMgr : NSFileManager = NSFileManager.defaultManager()
    if !(iFileMgr.fileExistsAtPath(iPathFile)) { return }
    // ファイルが存在したので、読み込んでバージョン番号チェックして、同じバージョンなら入れ替え
    var dictPref2 : Dictionary = NSDictionary(contentsOfFile: iPathFile)!
    if let iObject : AnyObject = dictPref2[PREF_KEY_VER] {
        if let iStrVersion : String = iObject as? String {
            if iStrVersion == strVersion {
                dictPref = dictPref2 as Dictionary
            }
        }
    } else {
        println("ERROR:PRF_RPF:内部エラー:ファイルにVer情報なし")
    }
}
// 環境設定ファイルを書き出す
func writePrefFileF() {
    // Dicに、バージョン番号を付加(あれば上書き)
    dictPref[STR_KEY_VER] = strVersion
    // ファイルへ書き出す
    let iPathFile : String = getFilePathDocsF(prefFName)
    let iResult : Bool = (dictPref as NSDictionary).writeToFile(iPathFile, atomically: true)
    if !iResult { println("ERROR:PRF_WPF:ファイル書き失敗") }
}

最初のパス生成関数は、両方のメソッドで使う共通処理です。重複して書かないようにと、1つにまとめました。

環境設定ファイルを読み込むreadPrefFileFメソッドでは、ファイルが存在するか最初に調べています。存在しなければ終了し、存在すれば読み込んでいます。

Dictionaryとして読み込んだ内容に、バージョン番号の値があるか調べています。このような場合、Swiftではif-let文が便利です。成功すれば変数に値が入り、失敗すればエラーメッセージを出します。正しく使っていれば、存在しないはずはないのですが、念のために入れておいただけです。

続いて、同じくif-let文で、バージョン番号のStringを変数に入れています。しつこいですが、これも念のためです。最後に、アプリ側のバージョン番号と比較して、同じ場合には、環境設定値の変数に入れます。つまり、最初に持っていたデフォルト値に上書きするというわけです。

このような作り方だと、デフォルト値に戻す機能は作れません。しかし、Documentsフォルダにある環境設定ファイルを削除してから、アプリを起動すれば、いつでもデフォルト状態に戻せます。この使い方が可能なので、ここでは上書きする方法を選びました。

次は、環境設定ファイルを書き出すwritePrefFileFメソッドです。最初に、バージョン番号を付け加えています。もし既にバージョン番号がある場合もありますが、わざわざチェックする処理が面倒なので、追加または上書きのどちらも構わないとしました。この方法が、もっとも単純な処理内容となりますので。

ファイルへの書き出しでは、DictionaryをNSDictionaryにキャストして、書き出すメソッドを呼び出しています。書き出しに失敗した場合は、エラーメッセージを出しています。失敗したとしても、メッセージを出すぐらいしかできませんね。

書き出しのメソッドでは、Dictionaryの変数の値を、そのまま書き出しています。後述する設定値の変更メソッドが、Dictionaryの変数の中身を更新し、その結果を書き出すという形にしたからです。

 

いよいよ、環境設定値を取得するメソッドです。データ型が4種類あるので、メドッソも4つ用意します。コンパイラーの型チェックを利用するためです。メソッド名は同じまま、戻り値のデータ型が異なるメソッドとなります。Swiftコードは、次のとおりです。

// 環境設定値の取得
func getPrefF(rKey:String) -> Int {
    let iStrId : String = "ERROR:PRF_GPI:"
    if checkGetPrefF(1, iStrId, rKey) {
        return dictPref[rKey] as Int
    }
    return -999999      // エラー時の値
}
func getPrefF(rKey:String) -> Float {
    let iStrId : String = "ERROR:PRF_GPF:"
    if checkGetPrefF(2, iStrId, rKey) {
        return dictPref[rKey] as Float
    }
    return -999999.9    // エラー時の値
}
func getPrefF(rKey:String) -> String {
    let iStrId : String = "ERROR:PRF_GPS:"
    if checkGetPrefF(3, iStrId, rKey) {
        return dictPref[rKey] as String
    }
    return "ERROR!!!"  // エラー時の値
}
func getPrefF(rKey:String) -> Bool {
    let iStrId : String = "ERROR:PRF_GPB:"
    if checkGetPrefF(4, iStrId, rKey) {
        return dictPref[rKey] as Bool
    }
    return false       // エラー時の値
}
// エラーメッセージ
let MSG_ERR_TYPE : String = "ではない"
let MSG_ERR_KEY : String = "未登録キー"
let MSG_ERR_EMPTY : String = "Dicが空"
// 使い方が間違ってないか、チェックする共通関数
// チェック内容:Dicの要素がゼロ?、キーが存在するか?、値の型が正しいか?
private func checkGetPrefF(rNum:Int, _ rStrId:String, _ rKey:String) -> Bool {
    if !(dictPref.isEmpty) {
        if let iObject : AnyObject = dictPref[rKey] {
            switch rNum {
            case 1:
                if let iInt : Int = iObject as? Int {
                    return true
                } else {
                    println(rStrId + "Int" + MSG_ERR_TYPE)
                }
            case 2:
                if let iFloat : Float = iObject as? Float {
                    return true
                } else {
                    println(rStrId + "Float" + MSG_ERR_TYPE)
                }
            case 3:
                if let iStr : String = iObject as? String {
                    return true
                } else {
                    println(rStrId + "String" + MSG_ERR_TYPE)
                }
            case 4:
                if let iBool : Bool = iObject as? Bool {
                    return true
                } else {
                    println(rStrId + "Bool" + MSG_ERR_TYPE)
                }
            default: break
            }
        } else {
            println(rStrId + MSG_ERR_KEY)
        }
    } else {
        println(rStrId + MSG_ERR_EMPTY)
    }
    return false
}

4つのメソッドは、戻り値の型が違うだけで、処理の流れなどは同じです。どの部分で切り分けて共通化するのか迷いましたが、上のような形にしました。呼び出す側が1〜4の番号を指定して、switch文でデータ型を振り分けています。いろいろ悩んだのですが、この形が一番シンプルではないかと思いました。

ソースコードのコメントで書いているように、チェック内容は次の3つです。Dictionaryが空かどうか、指定したキーが存在するかどうか、指定した値の型が前の値の型と一致しているかどうかです。このうち後者2つで、if-let文を利用しています。変数への代入が成功すれば、チェックが通ったという処理です。

一番悩んだのは、エラー発生時の戻り値です。nilを返すという方法もありますが、おそらく呼び出し側で異常終了することになるでしょう。それが嫌いなので、データ型の値の中で、ありそうもない値を設定してみました。Boolは無理なので、falseにしましたが。こうすると異常終了しない可能性が高まります。エラーメッセージが出るので、それで気付くはずですから。このような部分は、作る人の好きずきなので、自分の作りたい形で構わないと思います。

3種類のエラーチェックのうち、データ型のチェックだけは完全ではありません。その辺の話は、最後に書きます。

 

最後は、環境設定の値を設定するメソッドです。こちらも戻り値のデータ型ごとに用意するので、メソッド名が同じまま4つあります。全体的には取得メソッドと似ていて、次のようなSwiftコードにしました。

// 環境設定値の設定
func setPrefF(rKey:String, _ rInt:Int) {
    if checkSetPrefF(1, rKey, "ERROR:PRF_SPI:") { dictPref[rKey] = rInt }
}
func setPrefF(rKey:String, _ rFloat:Float) {
    if checkSetPrefF(2, rKey, "ERROR:PRF_SPF:") { dictPref[rKey] = rFloat }
}
func setPrefF(rKey:String, _ rStr:String) {
    if checkSetPrefF(3, rKey, "ERROR:PRF_SPS:") { dictPref[rKey] = rStr }
}
func setPrefF(rKey:String, _ rBool:Bool) {
    if checkSetPrefF(4, rKey, "ERROR:PRF_SPB:") { dictPref[rKey] = rBool }
}
// キーの存在を確認する共通関数
private func checkSetPrefF(rNum:Int, _ rKey:String, _ rMsgId:String) -> Bool {
    if let iObject : AnyObject = dictPref[rKey] {
        switch rNum {
        case 1:
            if iObject is Int {
                return true
            } else {
                println(rMsgId + "Int" + MSG_ERR_TYPE)
            }
        case 2:
            if iObject is Float {
                return true
            } else {
                println(rMsgId + "Float" + MSG_ERR_TYPE)
            }
        case 3:
            if iObject is String {
                return true
            } else {
                println(rMsgId + "String" + MSG_ERR_TYPE)
            }
        case 4:
            if iObject is Bool {
                return true
            } else {
                println(rMsgId + "Bool" + MSG_ERR_TYPE)
            }
        default: break
        }
        return false
    } else {
        println(rMsgId + MSG_ERR_KEY)
        return false
    }
}

更新する前に読み込むはずなので、Dictionaryが空かどうかのチェックは含めませんでした。指定したキーが存在するかどうかと、指定した値の型が前の値の型と一致しているかを調べています。どちらもOKのときだけ値を更新し、それ以外ではエラーメッセージを表示します。

取得および設定のメソッドで、メッセージの言葉は共通です。文字列を定義して、間接参照する形で作っています。また、データ型ごとにエラーメッセージIDが異なるため、どのメソッドでエラーが出たのかも分かるようになっています。

 

後回しにした、データ型のチェックの話です。取得メソッドでは、「iInt : Int = iObject as? Int」のように、AnyObjectをキャストして代入を試みています。この方法だと、Int、Float、Boolのすべての組み合わせで、それぞれが代入できてしまいます。厳密には、エラーチェックとして機能していません。相手がStringのときだけ、エラーになります。代入するという方式がダメなのかと思いましたが、実はそうではなかったのです。

もう片方の設定メソッドでは、「if iObject is Int」のように、is演算子を用いて型チェックをしています。iObjectのデータ型はAnyObjectです。いろいろなデータ型の組み合わせを試しに実行してみたら、面白い結果となりました。iObjectに入れたデータ型がInt、Float、Boolのどれかであれば、「is Int」も「is Float」も「is Bool」も、すべてtrueになるのです。if-letによる代入と、同じ判定結果となりました。

今のところAnyObject型しか試していませんが、AnyObject型の変数に入れると、Int、Float、Boolのすべてでtrueになるようです。データ型を調べるというよりは、代入可能かどうかを調べるという仕様なのかもしれませんね。

とりあえずは、この形で行くことにしました。代入可能ということは、間違った使い方の一部を発見できなくなりますが、代入できずに異常終了することは避けられますから。

 

実際に作ってみると、最初に予想したよりは長いSwiftコードになってしまいました。やはり、処理の流れが同じでも、データ型が違うものを共通化するのは、シンプルには作りづらいということでしょう。何でも入れられる変数として作れば簡単ですが、それだと型チェックのメリットが消えます。少しぐらい長くなっても、できるだけ良い形のライブラリとして仕上げたいですからね。

なお、ここに掲載したSwiftコードは、私の使っているものと少し異なります。私のものは、ファイルの読み書きの部分が、共通ライブラリを呼び出す形になっています。今回の掲載では、ライブラリの呼び出し部分を、通常のファイルの読み書きに書き換えました。それにより、別なコードに依存しないコードになっています。

以上で、Swiftコードの紹介は終わりです。長くなりましたので、使用例は次回の投稿で。

 

(使用開発ツール:Xcode 6.1.1, SDK iOS 8.1)

0 件のコメント:

コメントを投稿