環境設定機能をライブラリ化する話の続きです。大まかな仕様が決まったので、実際のソースコードを作る段階です。小さなクラスですから、手短に紹介します。
まず、ファイル名を決めます。どのアプリでも使える共通ライブラリで、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 件のコメント:
コメントを投稿