2014年12月30日火曜日

設計しながらコーディングする方法

このブログでは、いくつかの機能を実現する方法を取り上げました。その際には必ず、最初に大まかな設計をしています。その具体的な方法を紹介します。

 

この大まかな設計作業ですが、紙に書いているわけではありません。実は、Xcode上で行なっています。それも、ソースコードを編集するテキストエディタ上でです。「えっ」と思った方が、いるかもしれませんね。でも、いろいろと試した結果、この方法が一番効率的だと判断したわけです。あくまで、私にとって一番効率的ですが。

 

では、具体的な手順を見ていきましょう。

まず最初に行なうのは、入れ物の作成です。そう、Swiftのソースコードを入れるファイルです。Xcode上で新規のSwiftファイルを追加します。

作成時には、これから作成する機能に適したファイル名を付けます。このファイル名も、私なりにルール化していますから、そのルールに合わせた名前を決めます。環境設定の機能を受け持つライブラリのソースコードなら、「BC_Pref.swift」ですね。

そのソースコードを、最初はどこに作るのでしょうか。私の場合は、以前の投稿で紹介した、iOS実験専用アプリを使います。ライブラリ化するソースコードは必ず、このアプリ内で作ります。Xcodeのメニューから、新規Swiftソースコードを追加して、準備は完了です。

 

入れ物が用意できたので、大まかな設計を始めます。思いついた機能や工夫を、テキストとして打ち込みます。ソースコードとして処理されるので、当然、コメントとして入力します。形式としては、箇条書きのメモです。

言葉をひととおり入力したら、今度は全体の構成を考えます。クラスとして仕上げるなら、必要となるクラス変数やメソッドを洗い出して、全体の形を想像します。

全体の形がほぼ決まったら、その内容をコメントの形で入力します。大まかには、次のような感じになります。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?
// データ型の一致をどうやって調べる?
// ====================================== 必要な仕様
// 全体を1つのクラスとして作る
// データ型ごとにメソッドを用意する:Int, Float, String, Boolの4種類
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// ====================================== 構成要素
// クラス変数:
// メソッド:初期化
// メソッド:ファイルから読み込み、ファイルへ書き出し
// メソッド:値の取得
// メソッド:値の設定

ここで大事なのが、区切り線です。たくさんの文字を入力しても、どこが区切りなのか明確に示すために、区切り用のコメント行を挿入します。コメント行の最後に、それ以降で入力する内容の見出しを付けます。この見出しが、入力したテキストからはみ出ているように見えると、区切りごとの内容が見やすくなります。

区切りの先頭には、必ず「検討すべき点」を入れます。ここは、解決していない問題点や、疑問に思っていること、機能として含めようか迷っていることなど、決まっていない内容を書きます。今後判断しなければならない内容なら、何でも記述するのが基本です。

 

クラスとして実現する場合は、まず最初にクラスだけ作ります。その中に、構成要素として挙げたコメント行を入れます。この時点で、クラス内の構成要素の並び順も決まります。

このような作業を続けながらも、気付いた点をコメントとして追加していきます。コメントは少しずつ増えていき、それに比例して仕様も細かくなります。ここまでの状態では、次のような感じになっているでしょう。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?
// ====================================== 必要な仕様
// データ型ごとにメソッドを用意する:Int, Float, String, Boolの4種類
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
    // クラス変数:設定値セット、保存ファイル名、バージョン番号
    // メソッド:初期化
    // メソッド:ファイルから読み込み(保存ファイルの読み込みも含む)
    // メソッド:ファイルへ書き出し
    // メソッド:値の取得(4種類)
    // メソッド:値の設定(4種類)
}

クラス変数やメソッドのコメントをクラス内へ移動させたので、実際のコードを書き始められる状態となりました。ここからの進め方としては、コメントをさらに追加して仕様を細かく詰めるか、メソッドなどの具体的な入れ物(Swiftコード)を先に作るか、どちらでも好きな形で進められます。

これ以降の作業でも同じなのですが、「検討すべき点」や「必要な仕様」の中に含まれるコメント行は、不要になった時点でどんどんと削除していきます。この段階では、クラスを作ったので、「必要な仕様」に含まれる「全体を1つのクラスとして作る」の行を削除しました。

 

今回は、先にSwiftコードの入れ物を作ってみましょう。おそらくですが、その手順ほうが、動いたときの状態を想像しやすい人が多そうですから。この段階では、メソッド名は決めますが、引数などは最小限しか入れません。その結果、次のようなソースコードになります。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// ====================================== 必要な仕様
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
    // クラス変数:設定値セット、保存ファイル名、バージョン番号
    // メソッド:初期化
    init() {
    }
    // メソッド:ファイルから読み込み(保存ファイルの読み込みも含む)
    func readPrefFileF() {
    }
    // メソッド:ファイルへ書き出し
    func writePrefFileF() {
    }
    // メソッド:値の取得(4種類)
    func getPrefF() -> Int {
        return 1 // dummy
    }
    func getPrefF() -> Float {
        return 1.0 // dummy
    }
    func getPrefF() -> String {
        return "dummy"
    }
    func getPrefF() -> Bool {
        return false // dummy
    }
    // メソッド:値の設定(4種類)
    func setPrefF(rData:Int) {
    }
    func setPrefF(rData:Float) {
    }
    func setPrefF(rData:String) {
    }
    func setPrefF(rData:Bool) {
    }
}

このまま入力を進めると、追加したソースコードやコメント行が増えるほど、メソッドの区切りが見づらくなってしまいます。それを防ぐために、メソッドの種類ごとの境目に、区切り用のコメント行を挿入します。その結果、次のようになります。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// ====================================== 必要な仕様
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
    // クラス変数:設定値セット、保存ファイル名、バージョン番号
    // ==================================== 初期化
    init() {
    }
    // ==================================== ファイルの読み書き
    // ファイルから読み込み(保存ファイルの読み込みも含む)
    func readPrefFileF() {
    }
    // ファイルへ書き出し
    func writePrefFileF() {
    }
    // ==================================== 値の取得(4種類)
    func getPrefF() -> Int {
        return 1 // dummy
    }
    func getPrefF() -> Float {
        return 1.0 // dummy
    }
    func getPrefF() -> String {
        return "dummy"
    }
    func getPrefF() -> Bool {
        return false // dummy
    }
    // ==================================== 値の設定(4種類)
    func setPrefF(rData:Int) {
    }
    func setPrefF(rData:Float) {
    }
    func setPrefF(rData:String) {
    }
    func setPrefF(rData:Bool) {
    }
}

見て分かるように、区切りコメント行の追加によって、前にあったコメント行の内容を吸収している箇所があります。記述する内容は、できるだけ区切り行に含めて、他のコメント行を減らすように心掛けます。

 

ここまでの段階でも、クラス内の大まかな構成が見えてきたと思います。ここからは、実際のコーディングを進める前に、細かな仕様を固める段階に入ります。大事なのは、クラス変数、メソッドの引数と戻り値、エラーチェックの内容、メソッド内に含める処理の4つです。これらを明らかにするように、細かな点を固めていきます。

まず最初に、クラス内の変数を決めます。この例では、キー付きの環境設定値セットを入れるために、データ型を決定しなければなりません。必要なら、簡単な実験を別にすることもあります。いろいろ調べた結果、今回はデータ型Dictionaryで大丈夫と結論付けました。

クラス内の変数が追加できたら、各メソッドに引数や戻り値を追加します。たまたま今回の例では、戻り値が追加済みなので、引数だけを追加することになります。さらに、エラーチェック内容と含める処理を、コメント行として追加します。

ここまでの状態で、次のようなソースコードになりました。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// 上記のデータ型チェックは、全データ型の組み合わせで結果を確認すること
// ====================================== 必要な仕様
// ====================================== 構成要素
class PrefController {
    // クラス変数:設定値セット、保存ファイル名、バージョン番号
    var dictPref : Dictionary<String,AnyObject> = [String:AnyObject]()
    var prefFName : String = "dummy.plist"
    var strVersion : String = ""
    // ==================================== 初期化
    init(_ rDictPref:Dictionary<String,AnyObject>, _ rFName:String, _ rVer:String) {
    }
    // ==================================== ファイルの読み書き
    // ファイルから読み込み(保存ファイルの読み込みも含む)
    // ファイルが存在するか、存在したらバージョン番号を調べる、一致したら値を採用
    func readPrefFileF() {
    }
    // ファイルへ書き出し
    // バージョン番号を追加する
    func writePrefFileF() {
    }
    // ==================================== 値の取得(4種類)
    // 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
    func getPrefF(rKey:String) -> Int {
        return 1 // dummy
    }
    func getPrefF(rKey:String) -> Float {
        return 1.0 // dummy
    }
    func getPrefF(rKey:String) -> String {
        return "dummy"
    }
    func getPrefF(rKey:String) -> Bool {
        return false // dummy
    }
    // ==================================== 値の設定(4種類)
    // 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
    func setPrefF(rKey:String, _ rInt:Int) {
    }
    func setPrefF(rKey:String, _ rFloat:Float) {
    }
    func setPrefF(rKey:String, _ rString:String) {
    }
    func setPrefF(rKey:String, _ rBool:Bool) {
    }
}

ここまで出来上がると、具体的な動きが想像しやすくなります。頭の中でプログラムを動かし、呼び出し側とのやり取りを空想実行してみます。

どの順番でメソッドが呼び出され、それぞれのメソッド内で何が行なわれ、処理結果として何が返されるのか。また、クラス内の変数が、どのように変化していくのか。さらには、間違った使い方をされたとき、クラッシュなどの大きな問題が発生しないのか。などなど様々な点で、仕様に欠点がないかを探します。

もし問題を発見したら、引数を加えたり変更したり、新しいメソッドを追加したり、エラーチェックの内容を書き加えたりします。変更した場合は、再び空想実行してみて、変更の正しさを確認します。これで大丈夫と思えるまで、以上のような検討を続けるわけです。

この段階で細かな仕様を固める理由は、全体を見通しやすいからです。引数や戻り値を含んだメソッド名だけが並び、ほとんどが画面内に収まるか、少しスクロールするだけで全部が見れます。そんな状態なので、相互の関係とかも含めた、全体での整合性などが思考しやすくなっています。

 

いろいろと考えていると、細かな疑問点や不明点が見付かるものです。

たとえば、取得メソッドでエラーが発生した場合、正しい値は返せません。どんな値を返すべきなのか、この段階で決めるのがベストだと思います。nilを返すのか、データ型に該当する何かの値を返すのか。検討して決めた内容を、該当する箇所へコメント行として追加します。次の例のように。

// 取得メソッドにコメント行を追加(該当部分だけ抜粋)

    // ==================================== 値の取得(4種類)
    // 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
    // エラー発生時には、nilではなく、ありそうもない値を返す:-99999とか、"ERROR!!!"とか
    func getPrefF(rKey:String) -> Int {
        return 1 // dummy
    }
    func getPrefF(rKey:String) -> Float {
        return 1.0 // dummy
    }
    func getPrefF(rKey:String) -> String {
        return "dummy"
    }
    func getPrefF(rKey:String) -> Bool {
        return false // dummy
    }

具体的な戻り値も、思いついたのならコメント行に記述します。このような形で、細かな仕様を少しずつ明らかにしていきます。

すべての疑問点や不明点が解消され、何もなくなったら、設計段階が終了です。あとは、メソッドの中身をコーディングしていくだけです。

 

ここまでで気付いた疑問点や不明点を解消済みですから、コーディングを開始したら、一気に書いてしまえます。設計段階で深く考えているほど、新しい疑問点や不明点は出てきません。

逆に、設計段階の考えが不十分だと、疑問点や不明点が新たに出てきます。そのような場合には、まず最初に、ソースコードの先頭部分に付けた「検討すべき点」へ、出てきた疑問点や不明点を、コメント行として書き加えます。それから、解消する方法を考え始めます。

基本的には、開始したコーディングを一旦中断して、疑問点や不明点を解消するのが先です。解消方法によっては、メソッドなどの構成が影響を受けるので、無駄な作業を行わないためです。ただし、解消方法がすぐに出そうもないときは、気分転換も兼ねて、コーディングを続けるという選択肢もあります。後から余計な修正が入っても構わないと、割り切りながら。

コーディングしていると、共通の処理が見えてきます。その場合は共通処理を関数として追加し、無駄なコードを書かないように工夫します。また、定義値なども積極的に追加し、間接参照を利用したソースコードに仕上げます。

コーディングが終わった時点のソースコードを、ここに載せても構わないのですが、長いので省略します。そのソースコードは、実際の処理を書き加えたメソッド、メソッドが共通で使う関数、メソッドで使う定義値の変数などが、追記された状態となります。具体的なコードは、数回前の投稿に含まれますから、そちらを参照してください。

 

ひととおりの機能を作り終えたら、本格的な機能確認を行ないます。機能確認というのは、実装した機能が具体的にどう動くのか、確認するための試験実行です。SDKの多くは、細かな仕様を書いておらず、不明確なことが多くて困りものです(書くのが大変なので、気持ちは分からないではないですが)。実際の動きがどうなるか、実装したコードを実行して、確かめるしかありません。

今回の例では、データ型を調べるために、「as演算子」と「if let ... as?」の2つを用いました。それらが実際にはどう動くのか、実行して確かめる必要があります。このような作業を、私は開発ツールの機能確認と呼んでいます。

その結果ですが、機能拡張のライブラリ化の投稿でも書いたとおりです。2つの記述方法とも、データ型が一致しているか調べる動作ではなく、値が代入可能かを調べる動作と判明しました。

期待どおりの動作結果でなかった場合には、そのことをソースコードに残しておく必要があります。今回の例では、該当するソースコードの箇所に、以下のようなコメントを残しました。

// 実際の実行結果などはコメントで残す(コメント部分のみ抜粋)

    // データ型の一致をチェックする機能の、重要な注意点
    // is演算子、if let ... as?のどちらも、代入可能かで判定される実験結果になった
    // Int, Float, Boolの全部の組み合わせで、相互に代入可能。そのため、同じデータ型と判定される
    // 上記2種類の処理は、データ型が同じかではなく、代入可能で判定しているのだろうか?
    // この実行結果は、Xcode 6.1.1、SDK iOS8.1にて確認

このように実行環境まで含め、少し長めのコメントになります。時間が経つと忘れがちな内容なので、細かく書いておくほど、後から助かるでしょう。

 

機能確認が終わり、その動作で問題があれば修正しなければなりません。逆に、その動作でも構わなければ、本格的なテストに移ります。テストの話は、長くなるので省略します。テストが終了すれば、機能的には出来上がりです。

私の場合は、iOS実験専用アプリを使って開発することが多く、テストのソースコードも、そのアプリに含めて保存します。定期的にバックアップしていますから、テストも含めて安全に保存されています。

最後の作業は、ソースコードの整理です。「検討すべき点」などの開発用のコメント行を削除して、仕上がりにふさわしいソースコードとして整えます。余計なコメントは削除して、必要最小限のコメントだけを残します。

私の書き方の特徴でもあるのですが、「ファイルの読み書き」や「値の取得(4種類)」といった、クラス内の区切り用のコメント行は削除しません。それが付いていたほうが、ソースコードの区切りが明確になって、時間が経ってから見たときに、内容を把握しやすくなります。とりあえず残すという位置付けではなく、絶対に必要な区切り線として位置付けていて、全部のソースコードに付けています。ただし、このブログに投稿するときは、ほとんど削除していますが。

 

ここで紹介したのは、ソースコードが1つだけの例です。複数のソースコードを作る場合でも、基本的な考え方は同じで、それぞれに「検討すべき点」などを作ります。

また、全体に関しての「検討すべき点」は、どれか1つのソースコードを選んで、それだけに「検討すべき点(全体編)」を付けます。もちろん使い方は同じです。

 

以上が、私が普段使っている開発方法です。1つのソースコードに、実際の命令ソースコードだけでなく、設計途中のメモをコメントとして加え、それを更新しながら、コーディングもやや並行気味に行ないます。コメントしたメモを命令コードに書き換え、コメントが減りながら命令コードが増えるという形です。最後は、最低限のコメントと、完成した命令コードが残ります。

ソースコードをやや並行気味に書いていますが、一番大事なのは、きっちりと設計すること。命令コードが大枠しかない段階、つまり全体を見渡しやすい段階で、空想実行しながら機能を深く掘り下げて、設計の質を高めることです。この段階で、さまざまな空想実行ができると、良いソースコードが作れます。意地悪な順序で実行させたり、無茶な引数を与えたり、いろいろと空想実行します。ただし、何にでも対応すると、余計なコードが増えますから、対応はほどほどにすべきでしょう。

別な面でも、ソースコードには気を配ります。区切り用のコメントで全体を見やすく整え、開発途中で気付いた注意点などもコメントとして残します。数年後の完全に忘れた頃に見ても、困らないようなソースコードになるようにも配慮します。将来の自分への親切ですね。これは、意外に大事です。

設計や実装の作業は、個人的な好みの部分が大きいでしょう。ここで紹介した方法は、その理由も説明しました。もしヒントとして利用できそうな部分があれば、自分なりのアレンジを加えながら、オリジナルの開発手順を作ってみてください。開発効率の向上だけでなく、出来上がったソースコードの質が上がるかも。

2014年12月28日日曜日

環境設定をライブラリ化(使用例編)

環境設定機能をライブラリ化する話の続きです。出来上がったSwiftコード(PrefControllerクラス)の使い方を紹介します。難しい点はないので、ちょっとした注意点が中心です。

 

アプリで使い始めるには、最低限の準備が必要です。環境設定値を保存するファイル名、バージョン番号、個々の環境設定値に使うキー、それぞれのデフォルト値です。デフォルト値はDictionaryとして、その他はStringとして用意します。具体的なSwiftコードは、次のようなものです。見やすくするために、環境設定値が4つ(扱えるデータが4種類なので)と少なめです。

// 環境設定クラスを使う(準備)
...
// 環境設定機能の変数
var zPref : PrefController!
// 環境設定のファイル名とバージョン番号
let PREF_FNAME : String = "sample_pref.plist"
let PREF_VER_NUM : String = "010001"
// 環境設定キーの定義
let PREF_KEY_SNmae_S : String = "ShopName"
let PREF_KEY_SId_I : String = "ShopId"
let PREF_KEY_GRank_B : String = "GoldRank"
let PREF_KEY_TRate_F : String = "TargetRate"
// 環境設定のデフォルト値
let dictPref : Dictionary<String,AnyObject> = [
    PREF_KEY_SNmae_S : "○○屋",
    PREF_KEY_SId_I : 999,
    PREF_KEY_GRank_B : false,
    PREF_KEY_TRate_F : 0.6 ]
...
// アプリの初期処理の中でインスタンス生成
zPref = PrefController(dictPref, PREF_FNAME, PREF_VER_NUM)
...

それぞれの値を定義した後、アプリの初期処理の中で、PrefControllerクラスのインスタンスを生成します。これで準備が整いました。もし初期設定ファイルが保存してあり、バージョン番号が同じであれば、デフォルト値が更新されます。

zPrefというのは、私なりの使い方です。zDateなどもあり、共通ライブラリのインスタンスを生成する際、アプリのどこからでも使える変数として用意します。この変数を通して、クラスのメソッドを利用します。

この準備で重要なのは、環境設定キーの定義です。以降の処理でも、ここで定義したキーを使います。もし定義値を使わずに環境設定値にアクセスすると、文字列のリテラルを指定することとなって、間違ったリテラルを指定しても、エラーかどうか分かりません。実行時に間違いだと気付くことになります。そうならないために、ここで定義した値を使い、コーディング時点でエラーを発見するわけです。決まったリテラル文字列を使う際に、実行時ではなくコンパイル時にエラーを発見するための、よく使われる工夫ですね。

定義に使うキーの変数名にも注目です。ここでは、変数名の最後の1文字で、データ型を表しています。「S, I, B, F」は、それぞれ「String, Int, Bool, Float」を意味してます。データ型が間違っていないか、変数名で注意しながらコーディングできます。

変数名が長くなるとか、データ型を含む変数名を間違えるとか、心配する必要はありません。Xcodeのテキストエディタには、入力支援機能があり、定義した変数も出てきます。「PREF」とタイプしたあたりで、一連の定義値名が並んでいるはずです。それを選んで入力すれば、データ型付きの変数名も間違えずに入力可能です。

 

環境設定の準備ができたので、いよいよ使う段階です。環境設定値の関係する個々の処理で、取得メソッドを用います。次のSwiftコードのように。

// 環境設定クラスを使う(個々の処理で値を取得)
...
let iShopId : Int = zPref.getPrefF(PREF_KEY_SId_I)
...
if zPref.getPrefF(PREF_KEY_GRank_B) {
    ...
}
...
let iRate : Float = zPref.getPrefF(PREF_KEY_TRate_F)
let iAmountLevel : Float = StdAmount * iRate
...

取得メソッドを使うときは、データ型の指定に注意を払わなければなりません。同じメソッド名で、戻り値の異なる4つがあります。そのどれが選ばれるのか、特定できる形で使わないと、余計なエラーを生んでしまいます。

基本的には、型を指定した変数に入れるということです。型が指定されていることで、間違ったメソッドが呼ばれることはありません。

変数以外でも、データ型が特定できる場合があります。たとえば、if文です。条件式はBool型と決まっているため、そこに参照メソッドを入れると、戻り値がBool型に特定されます。また、関数やメソッドの引数でも、型が特定されているので、同じように正しいメソッドが選ばれます。

計算式の場合は、少し注意が必要です。計算に使われるリテラル値や変数の型によって、特定しづらい場合もあります。そのようなときは、取得メソッドの値をいったん変数に入れてから、計算で使うほうが安全でしょう。

 

環境設定の値は、アプリごとに専用画面を用意して、ユーザーが変更可能に作ります。その処理では、すべての環境設定値を取得し、画面に表示します。ユーザーが画面上で変更した後、新しい値を受け取って設定メソッドで更新し、ファイルに保存する流れとなります。たとえば、次のようなSwiftコードに。

// 環境設定クラスを使う(環境設定画面)
...
// 環境設定値の取得
let iShopName : String = zPref.getPrefF(PREF_KEY_SNmae_S)
let iShopId : Int = zPref.getPrefF(PREF_KEY_SId_I)
let iGoldRank : Bool = zPref.getPrefF(PREF_KEY_GRank_B)
let iRate : Float = zPref.getPrefF(PREF_KEY_TRate_F)
...
// 環境設定値のファイルへの保存
zPref.setPrefF(PREF_KEY_SNmae_S, iShopName)
zPref.setPrefF(PREF_KEY_SId_I, iShopId)
zPref.setPrefF(PREF_KEY_GRank_B, iGoldRank)
zPref.setPrefF(PREF_KEY_TRate_F, iRate)
zPref.writePrefFileF()
...

これは、あくまで一例です。すべての環境設定値を更新する必要はありません。更新が必要な環境設定値だけ更新するのが、一般的でしょう。

 

バージョン番号の使い方にも、少しだけ触れます。ここで用いるバージョン番号は、String型です。文字列ですから、何でも構わないわけです。でも、やっぱり上手に使う方法はあります。

1つは、意味を持たせることです。ここに示した例では、6桁の数字を用いました。「010001」です。これは、バージョン「1.0.1」を意味していて、それぞれの数値に2桁を与えて文字列化したものです。もちろん「01.00.01」でも「1.0.1」でも構いません。好きな形を使いましょう。

もう1つの工夫は、アプリのバージョンに合わせる方法です。アプリのバージョンアップで、環境設定値も必ずバージョンアップするとは限りません。そうではなく、バージョンアップしないほうが多いでしょう。アプリの最新のバージョン番号ではなく、環境設定値を最後にバージョンアップしたときのアプリのバージョン番号を、環境設定のバージョン番号として用いるという考え方です。どのバージョンから同じなのか、明確に分かる利点があります。

ちなみに私の場合ですが、アプリのバージョン番号とは関係なく、環境設定独自のバージョン番号を付けています。特に大きな理由はありませんが、関係を気にしないほうが自由に付けられてよいと思っただけです。

 

以上で、環境設定機能のライブラリ化の話は終わりです。

今までは、アプリごとに環境設定クラス(ファイル名:MC_Pref.swift)を用意していましたが、これからは共通ライブラリ(ファイル名:BC_Pref.swift)をアプリに追加して、キーやデフォルト値を定義すれば済みます。

環境設定変更用の画面作りは同じですが、手間の何割かは減ると思います。独自ライブラリの種類が増えるほど、アプリ開発の手間が減りますね。

 

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

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)