2014年10月28日火曜日

日付時刻も間接参照で(まとめ編)

日付時刻を間接参照で利用する方法に付いて、長く書いてしまいました。機能ごとに分けて書いたので、考え方は分かりやすくなりましたが、最終的に全部入りはどうなるの?という疑問が生じたかもしれません。整理した最終状態を示す意味で、まとめ編を書きます。

 

複数の機能を組み合わせる場合には、それらの関係を整理する必要があります。進行倍率変更の機能では、開始時刻をもっています。それに時間シフト機能を加えたとき、どのような仕様にすべきなのでしょうか。

一番大事なのは、時間シフト機能をどう加えるかです。時間シフト量も、進行倍率の影響を受けるべきなのでしょうか。その判断では、アプリを対話式で使う場合の見え方を考慮します。もしシフト量が倍率の影響を受けたら、わざわざ逆算してシフト量を指定しなければなりません。シフト量はそのままの値が反映されるべきでしょう。

時間進行倍率の開始時刻と、時間シフト量の関係はどうでしょうか。単純に加算する方法が直感的に分かりやすく、それで問題ないでしょう。つまり結論は、進行倍率変更の機能で求めた時刻に、時間シフト量を加えるということです。より詳しい計算方法は、開始時刻に、進行倍率を加味した経過時間を加算し、最後に時間シフト量を加算するという形になります。

 

さらに使いやすくするためには、もう1つの機能が必要でしょう。アプリが動作する日付を、任意の日付に変更する機能です。自由な日付を設定できると、大晦日から元旦への移行処理をテストするとか、目的の日付でのテストが好きなときに可能となります。

時間シフト機能でも実現可能ですが、実行時の日付との差を計算していたら、非常に面倒です。任意の日付と時刻を指定して使えるほうが、明らかに便利です。機能的には、2種類目のシフト機能と言えるでしょう。

実現方法ですが、今までの全機能の最後に、新しいシフト量を加算する形が一番美しいでしょう。時間のシフト量ですから、値の種類はNSDateのtimeIntervalとなります。設定したい任意の日付と、何かの日付を比較して、その差の分だけtimeInterval値を求めて、変数の保存しておきます。そして、現在の日付時刻の計算の際に、保存しておいたtimeInterval値を加算するというわけです。

何の値との差を利用したら良いでしょうか。今まで作った機能の中では、時間進行倍率の開始時刻が一番適してそうです。別な値を新たに用意する方法もありますが、全体の構造がより複雑化します。もう1つのシフト機能も、この開始時刻を使っているので、同じにしたほうが、全体の動作を理解しやすいでしょう。ということで、時間進行倍率の開始時刻との差を利用し、timeInterval値として保存する形とします。

 

機能が出揃ったので、整理しましょう。時間進行倍率の変更機能では、開始時刻を指定すると、そのときの現実の時刻が設定されます。続けて倍率係数を設定すれば、開始時刻からの時間経過が、倍率で乗算され、その結果が計算後の時刻となります。

続いて、時間シフト機能に設定されたtimeInterval値が、計算後の時刻に加算されます。最後に、任意の時刻との差であるtimeInterval値が、さらに加算されて最終結果の時刻となります。

 

これらの処理を、1つの関数にまとめましょう。今まで作ったswiftソースコードを組み合わせて、次のようにします。

// 変数の準備
var timeIntervalShift : Double = 0.0
var timeIntervalWarp : Double = 0.0
var dateStart : NSDate = NSDate()
var timeRetio : Double = 1.0

// 3つの日付時間機能を統合した関数
func getDateNow4TestF(rDateNow:NSDate) -> NSDate {
    // まず先に、進行倍率を変えたときの時刻を求める
    let realTimeInterval : NSTimeInterval = dateStart.timeIntervalSinceNow
    let vartualTimeInterval : NSTimeInterval = realTimeInterval * timeRetio * -1.0
    let vartualDate = dateStart.dateByAddingTimeInterval(vartualTimeInterval)
    // 最後に、2種類のシフト量を加算
    let dateShifted = vartualDate.dateByAddingTimeInterval(timeIntervalShift)
    let dateWarped = dateShifted.dateByAddingTimeInterval(timeIntervalWarp)
    return dateWarped
}
// これらは、DateControllerクラスの中に作る

2種類のシフト機能の名前ですが、シフト量を直接指定するほうを「Shift」と、任意の日付時刻を指定するほうを「Warp」としました。これを呼び出す形で、「getDateNowF」メソッドを作ります。

// 現在の時刻を得る
func getDateNowF() -> NSDate {
    let dateNow = NSDate()
    //return dateNow
    return getDateNow4TestF(dateNow)
}

別な関数に分けたのは、このメソッドを分かりやすくするためです。また、追加した機能を簡単に消せるようにと、関数を呼ばないreturnもコメントとして残しました。テスト後には、これらの機能を使えなくするために、コメント用の「//」を削除し、関数呼び出し側をコメント化すれば良いでしょう。

 

前回までの機能のメソッドも、一応挙げておきましょう。

// 時間のシフト量を設定
func setTimeIntervalF(rTimeInterval:Double) {
    timeIntervalShift = rTimeInterval
}
// 時間進行の開始時刻を、現在の時刻に設定
func setStartDateNowF() {
    dateStart = NSDate()
}
// 時間の進行倍率を設定
func setTimeRatioF(rRatio:Double) {
    timeRetio = rRatio
}

なお、メソッドの名前が、前回までと少し変わっています。

 

新しく追加した、日付の任意設定機能にも、メソッドが必要です。1つのメソッドとして作るよりも、日付と時刻からNSDateの値を作るメソッドと、その値を使ってtimeInterval値を設定するメソッドに分けたほうが、幅広く使えるでしょう。

// 時間進行倍率の開始時刻を、任意の時刻に設定
func setStartDateF(rDate:NSDate) {
    timeIntervalWarp = rDate.timeIntervalSinceDate(dateStart)
}
// 任意の日付でNSDate値を作る
func createDateF(rYear:Int, _ rMonth:Int, _ rDay:Int, _ rHour:Int, _ rMin:Int, _ rSec:Int) -> NSDate {
    var iCalendar : NSCalendar = NSCalendar.currentCalendar()
    var iCompo = NSDateComponents()
    iCompo.year = rYear
    iCompo.month = rMonth
    iCompo.day = rDay
    iCompo.hour = rHour
    iCompo.minute = rMin
    iCompo.second = rSec
    let iDate : NSDate = iCalendar.dateFromComponents(iCompo)!
    return iDate
}

これで機能的には十分ですが、あと1つだけメソッドを追加します。全部の設定をリセットして、何の機能も動いてない状態に戻すメソッドです。これがあると、テストのときに便利です。

// すべての機能をリセットする
func resetAllDateF() {
    timeIntervalShift = 0.0
    timeIntervalWarp = 0.0
    setStartDateNowF()
    timeRetio = 1.0
}

ここでも「= NSDate()」を増やさないようにと考え、「setStartDateNowF」メソッドを使いました。

以上が、ここまで説明した機能の総合版です。長く説明した割には、短いソースコードになっていますね。

 

新しい機能だけは、使い方を紹介しましょう。

// クラスを使う
let zDate = DateController() // 適切なタイミングでインスタンス生成
 ...
zDate.setStartDateNowF() // 念のため、開始時刻を現在に設定
iDate = zDate.createDateF(2014, 12, 31, 23, 50, 0)
zDate.setStartDateF(iDate) // 任意の日付時刻にシフト
 ...
labelDate.text = zDate.getDateStrNowF() // 必要な箇所で使う
 ...
zDate.setStartDateNowF() // 念のため、開始時刻を現在に設定
iDate = zDate.createDateF(2015, 1, 1, 0, 10, 0)
zDate.setStartDateF(iDate) // 任意の日付時刻にシフト
 ...
labelDate.text = zDate.getDateStrNowF() // 必要な箇所で使う
 ...
zDate.resetAllDateF() // 必要に応じて全部リセット

使うときは、1点だけ注意が必要です。時間進行倍率の開始時刻と関係しているため、進行倍率を使わない場合でも、開始時刻がいつの設定になっているのか意識する必要があります(何もしていなければ、インスタンスを生成した時刻です)。念のために、開始時刻を現在の時刻に設定してから、任意の日付時刻に変更すると良いでしょう。

今回のように、複数の機能を組み合わせて使えるように仕上げた場合は、互いの影響を考慮しながら使う必要があります。どのように計算して日付時刻が決まるのか、きっちりと頭に入れておくことが求められますね。間違えずに使えれば、非常に便利な機能です。

 

日付時刻の機能拡張に、かなりの回数を費やしてしまいました。一度作っておくと全部のアプリで使えますし、作るまでの考え方のほうが重要なので、これぐらい長く説明しても意味があると思います。日付時刻以外の機能でも、間接参照をどんどんと利用して、変更に対して柔軟で、しかもテストしやすいアプリを作りましょう。

 

(使用開発ツール:Xcode 6.0.1, SDK iOS 8.0)

2014年10月26日日曜日

日付時刻も間接参照で(続編:時間進行倍率を変更)

前回は、日付時刻の間接参照を利用し、時間をシフトすることで、テストが楽になることを紹介しました。実は、さらなる技があるのです。続編として紹介しましょう。

 

時間シフト以外の、簡単で実用的な例として面白いのが、時間の進み具合を変更する機能です。進み具合というのは、時間の経過速度のことです。

アプリで使っているマシンの時計は、1秒間に1秒だけ進みます。当たり前ですよね。これが狂っていたら大変なことになります。でも、それを「自由に」変えられたらどうでしょうか。1秒間に2秒だけ進むとか、逆に1秒間に0.5秒だけ進むとか。前者は速く進み、後者は遅く進んでいることとなります。

 

何に使うのか疑問に思う人もいるでしょう。でも、役立つ場合もあるのです。たとえば、1時間ごとに自動で、何かを処理するアプリがあったとします。1時間待てますか。もちろん、時間シフト機能を使って、1時間ごとに時刻をずらし、テストしても構わないでしょう。

でも、時間が通常より速く進められたら、どうでしょう。仮に3600倍速にしたら、1秒ごとに1時間だけ進みます。処理時間が必要なので、ちょっと速すぎますね。もう少し遅くして、10秒ごとに1時間だけ進んだらどうでしょう。240秒(=4分)で1日分の処理が確認できてしまいます。本来なら1時間ごとに始まる処理が、10秒ごとに始まって、たった4分で1日分が確認できるわけです。凄いと思いませんか?

 

さっそく、作りましょう。現在の日付時刻を取得するメソッドに、少し細工するだけです。必要な変数は2つです。開始時刻を記録した変数「dateStart」と、進行倍率を保持する変数「timeRetio」です。

動作は簡単です。準備として「dateStart」に開始時刻を記録し、「timeRetio」に倍率を設定しておきます。今の日付時刻が必要になったら、「getDateNowF」メソッドを呼び出します。その中では、現在の時刻と「dateStart」との時差を求め、それに倍率「timeRetio」を乗算します。乗算した値を「dateStart」に加えた時刻を返すというわけです。swiftで書くと、次のようになります。

// 時間進行の倍率を変える機能が付いた、現在の日付時刻を取得するメソッド
var dateStart : NSDate = NSDate()
var timeRetio : Double = 1.0
func getDateNowF() -> NSDate {
    let realTimeInterval : NSTimeInterval = dateStart.timeIntervalSinceNow
    let vartualTimeInterval : NSTimeInterval = realTimeInterval * timeRetio * -1.0
    let vartualDate = dateStart.dateByAddingTimeInterval(vartualTimeInterval)
return vartualDate
}

こんなに短いソースで、実現できてしまいました。

 

もちろん、これだけでは不十分ですね。進行倍率を設定するメソッドも必要です。

// 進行の倍率係数を設定するメソッド
func setTimeRatio(rRatio:Double) {
    timeRetio = rRatio
}

さらには、進行のスタートをリセットするメソッドもあると、いろいろ便利でしょう。

// 倍率進行の開始時刻をリセットするメソッド
func resetStartDate() {
    dateStart = NSDate() // 基準となる時刻を、現在の時刻に再設定
}

どちらのメソッドも、非常に簡単です。

ここで、あれれと気付いた人もいるでしょう。「NSDate()」が使われているため、「NSDate()」が一カ所ではなくなっているのです。この機能の実現には、「getDateNowF」メソッドを使うわけにもいかず、「NSDate()」しか選択肢がありません。日付取得にいろいろな機能を加えていくと、日付取得の「NSDate()」を一カ所だけに限定することができず、やむを得ず複数箇所になってしまいます。これは仕方ないでしょうね。コメントなどで、複数箇所あることを明示しておきましょう。

(余談:選択肢がないと書きましたが、「getDateNowF」メソッドの処理内容を逆補正すれば、「getDateNowF」メソッドを使って「NSDate()」と同等の処理を実現できます。でも、「getDateNowF」メソッドの変更と同時に、逆補正の処理も変更する必要が生じ、修正しずらい機能になってしまいます。だから実質的には選択肢になり得ません)

 

一応、使い方も紹介しておきますね。前回とほぼ同じですが、インスタンス生成直後に、時間の進行倍率を設定するコードが加わります。

// クラスを使う
let zDate = DateController() // 適切なタイミングでインスタンス生成
zDate.setTimeRatio(360.0)    // 10秒で1時間進む設定に変更
 ...
labelDate.text = zDate.getDateStrNowF() // 必要な箇所で使う
 ...
zDate.resetStartDate() // 必要に応じてリセット

使う側の内容は、前回と似た感じになってしまいました。一応はいろいろと考えましたが、良い例が思い浮かびませんでした(汗)。

 

swiftのソースを見て分かるように、時間の進行倍率は、現在の時刻を求めるときに使うだけで、ずっと計測しているわけではありません。このような仕組みなので、2種類以上の進行倍率を並行して使うことも可能です。

この場合も、使い方は簡単です。時刻を取得する直前に、必要な進行倍率に設定するだけです。進行倍率の設定と時刻取得を、すべてペアで使うということです。swiftで書くと、次のようになります。

// 2種類の進行倍率を使う
let zDate = DateController() // 適切なタイミングでインスタンス生成
 ...
zDate.setTimeRatio(360.0)
date360 = zDate.getDateNowF() // ケース1の必要な箇所で使う
 ...
zDate.setTimeRatio(180.0)
date180 = zDate.getDateNowF() // ケース2の必要な箇所で使う
 ...
zDate.setTimeRatio(360.0)
date360 = zDate.getDateNowF() // ケース1の必要な箇所で使う
 ...

見てのとおり、簡単ですね。

 

ここで紹介した時間の進行倍率を使ったテストでは、他の要素を考慮していません。アプリの処理内容によっては、時間以外の部分で何か用意する必要があるでしょう。「時間が速く進むことによる副作用」も考慮しながら、上手に使うことが求められます。

 

日付時刻を間接参照で作ると、いろいろと面白い機能が実現できます。その例として、少し変わった使い方を紹介しました。とにかく大事なのは、日付のような外部からの入力は、1カ所にまとめるということです。

2014年10月24日金曜日

日付時刻も間接参照で使う

大抵のアプリでは、日付や時刻を利用します。この使い方というか、内部での作り方によって、アプリの柔軟性を大きく向上させられます。とくに、日付に関係するテストの容易性を考えると、間接参照で作るのが一番であり、間接参照しかないと断言できます。

 

一般的なアプリでは、日付の処理を、使う箇所のソースコードにベタで書いてしまいがちです。たとえば、swiftなら次のように。

// ベタで書いた日付処理
let dateNow = NSDate()
labelDate.text = dateFormatter.stringFromDate(dateNow)

こうすると、現在の日付や時刻を得る処理が、いくつもの場所に散乱してしまいます。散乱するけど、何が問題なの???と思うでしょう。もちろん、現在の日付や時刻を単に表示するだけなら、おそらく何の問題もないでしょう。しかし、日付や時刻を使って特別な処理をしたり、日付の変わり目で特殊な処理が必要な場合は、このような作り方ではテストが大変になりがちです。何の話か意味不明だと感じているでしょうが、もうしばらくお付き合いください。

 

まず、日付の取得を間接参照で作りましょう。swiftで作るなら、次のような関数を用意します。

// 間接参照で現在の日付時刻を得る関数を、1つだけ用意する
func getDateNowF() -> NSDate {
    let dateNow = NSDate()
return dateNow
}

できるだけ短くする書き方になってませんが、その辺は気にしないでください。これを使う側は、次のようになります。

// 用意した関数を使うように変更した
//let dateNow = NSDate()
let dateNow = getDateNowF()
labelDate.text = dateFormatter.stringFromDate(dateNow)

修正前に「NSDate()」としていた部分が「getDateNowF()」に変わるだけです。アプリ内の全部の日付時刻処理で、この関数を使うように作ったらどうでしょう。日付を得るときは、この関数を必ず通ることになります。まさに、間接参照です。

 

では、何のために間接参照にしたのでしょう。主な理由は、アプリのテストです。日付を得る関数が1つだけあり、そこに小さな細工をするだけで、日付関連の面倒なテストが、格段に容易となります。

まずは、最初に用意した日付取得の関数に、細工をしてみましょう。日付をシフトさせ得る値の変数「dateInterval」を追加し、その値の分だけ日付がシフトするように、関数を書き換えます。

// 変数を追加
var timeInterval : Double = 0.0
// 書き換えた関数
func getDateNowF() -> NSDate {
    let dateNow = NSDate()
    //return dateNow
    let dateShifted = dateNow.dateByAddingTimeInterval(timeInterval)
return dateShifted
}

これだけでは、また足りません。シフトする変数に値をセットする関数も追加します。正の値を設定すれば時間が進み、負の値を設定すれば時間が前に戻ります。

// シフト値の変数に値をセットする関数
func setTimeIntervalF(rTimeInterval:Double) {
    timeInterval = rTimeInterval
}

これだけで、最低限の準備は整いました。あとは上手に使うだけです。

 

テスト用のソースコードの中で、シフト値を設定する関数を呼び出しても構わないのですが、iPadアプリではユーザーが操作しながらテストすることも多いでしょう。そうすれば、画面表示を確認しながらテストできますし。

その場合には、表示するViewに小さな機能を付け加えます。数値を入力するテキストフィールドと、そのフィールドの値をシフト値に設定するボタンです。テキストフィールドに秒数を入力し、ボタンをタップすれば、アプリを起動したままでシフト値が簡単に変えられます。秒数では使いづらいので、分数または時数を入力して、シフト値に変換してから設定する方法でも構いません。

シフト値が変更できたとしても、シフト結果の日付を確認したいはずです。その日付を表示する、小さなラベルも追加します。ボタンに付けた処理では、シフト値を設定した後、シフト後の現在の日付と時刻をラベルに表示します。ここまで作ると、まあまあ使いやすくなります。テキストフィールド、ボタン、表示ラベルの3つを、日付を使うViewの端に追加するわけです。

// シフト値を設定し、シフト後の日付時刻を表示するボタン用の関数例
func testBtnShiftF(rSender: UIButton) {
    let iDouble : Double = Double(textFieldShift!.text!.toInt()!)
    setTimeIntervalF(iDouble * 60 * 60) // 秒数ではなく、時数に変換して設定
    labelDateTime.text = getDateStrNowF() + " " + getTimeStrNowF()
}  // 最後の2つの関数は別に用意したもの

準備ができたので、テストを始めましょう。日付をまたがったとき、翌日への繰り越し処理が正常に動作するかのテストです。アプリを起動した時点では、シフト値がゼロです。アプリを通常のように使って、データを入力します。ある程度のデータが入力し終わったら、シフト機能の出番です。必要な分だけプラスする値を入力して、ボタンをタップします。翌日へと変化した日付を確認できました。続けて、繰り越し処理をテストします。正常に動作したら、テストは完了ですね。

 

賢いし人は、もう気付いたでしょう。マシンの日付をまったく変えずに、日付に関係するテストができています。しかも簡単に。さらには、自由な日付や時刻へ移動するのも、一瞬で出来てしまいます。もちろん、特別なツールも必要ありません。

マシンの日付を直接変更するのは、悪影響が大きすぎます。メールの送受信では間違った日付になります。色々なアプリで作成したファイルの日付も、すべて間違ったものになります。何から何まで、日付が狂った状態です。万が一、テスト終了後に戻し忘れたら、気付くまで大変な状態になったままです。誰もが、やりたくない方法でしょう。それが、日付を間接参照することで、まったく不要となります。

 

さて、間接参照の日付時間機能は、アプリ上でどのように組み込むべきでしょうか。どこからでも呼べるべきな機能ではありますが、クラスとして定義し、アプリ内で唯一のインスタンスとして生成するのが良いと思います。例えば、クラス名が「DateController」で、インスタンスの変数名を「zDate」として生成するとか。その場合、関数はメソッドに変わり、その呼び出し方は「zDate.getDateNowF()」となります。

// 日付時間に関する機能をクラスにまとめる
class DateController {
    func getDateNowF() { ... }
    func getDateStrNowF() { ... }
    func getTimeStrNowF() { ... }
     ...
}
// クラスを使う
let zDate = DateController() // 適切なタイミングでインスタンス生成
 ...
labelDate.text = zDate.getDateStrNowF() // 必要な箇所で使う

このクラス内では、日付や時刻に関係した機能を全部入れます。現在の日付をフォーマットした文字列として返す関数とか、いろいろあるでしょう。それらの関数の中では、最初に用意した「getDateNowF」メソッド(アプリ内で唯一の日付取得機能)を呼び出し、日付時刻がシフトされた結果を返します。日付や時刻の加工処理まで含めるので、クラス外から直接「getDateNowF」メソッドを呼び出すことは、めったにないはずです。

日付加工処理まで加えたクラスは、1つのswiftファイルとして作成し、そのまま他のアプリへ持っていって使えます。自分なりの標準ライブラリとして利用すれば、新しいアプリ開発を始める際にも役立つでしょう。こうして、アプリ開発がどんどん楽になっていきます。

あと、変数名を短めにすると、使うときに楽ですね。私の場合は、変数名の先頭に「z」を付けて、続きが大文字で始まる変数名が、アプリ内で唯一の汎用的な機能と決めています。「zDate」だけでなく、「zFiles」、「zCG」、「zMail」などがあります。共通ライブラリとして作っておくことで、変更なしに使えます。機能の追加は、たまにありますね。

 

今回は、日付を取り上げましたが、もっと大きな視点での考え方があります。日付というのは、システム(アプリ)から見たら、外部からの入力機能の1つといえます。上手なシステム設計では「外部との入出力を、一カ所にまとめたほうが良い」という考え方があります。一カ所にまとめることで、何かコントロールしたいときに、直す箇所が1つで済むための配慮です。

今回はテストでの利用を紹介しましたが、日付に関する何か問題が発生したときでも、同じように一カ所の修正で対処できる可能性が高まります。日付以外でも、外部とやり取りする機能は、できるだけ1つにまとめることを心がけたいものです。

2014年10月22日水曜日

間接参照を幅広く使う

前回は、swiftでUI部品を生成する関数の話をしました。その原点となる考え方を、他の話よりも先に紹介したほうが良いでしょう。その考え方とは「間接参照」です。柔軟性の高いソフトウェアを作る上で、おそらく一番大事な考え方だと思います。仮にオブジェクト指向を知らなくても、間接参照を上手に使えば、柔軟性の高いシステムが作れるほどですから。

 

UI部品を生成する関数ですが、別の視点から見てみましょう。通常、UI部品の生成を、使う箇所にベタで書いた場合、使う箇所とUI部品の生成コード(UI部品クラスのインスタンスを生成するコード)が直接つながっています。しかし、生成関数を使うと、使う箇所は生成関数につながり、生成関数が生成コードとつながります。つまり、使う箇所と生成コードの間に生成関数が挿入され、使う箇所と生成コードが、生成関数を通じて間接参照されていることになります。生成関数とは、間接参照を実現するための方法でもあります。

参照関係の間に関数が入ることと、関数内にソースコードを記述できることにより、単なる参照よりも柔軟な可能性が生まれます。UI部品のデフォルト値をまとめて変更できるだけでなく、新しい機能を加えたりが、関数部分の変更だけで実現できるのです。GUIの設計ツールなども、生成するUI部品を参照してはいますが、まとめて機能を変更することはできません。関数を用いた間接参照だから、可能になるわけです。

 

間接参照は、幅広く利用可能です。もっとも単純な、間接参照の例も挙げてみましょう。よく使う、色の定義です。文字の色、背景の色、枠線の色など、GUIでは必ず設定する属性の1つでしょう。一番ありがちな作り方は、ソースコードの中に、ベタで色を指定する方法です。swiftなら次のように。

labelMessage.textColor = UIColor.redColor()
labelMessage.text = "エラーが発生しました。"

このように作ると、何かの色をまとめて変えたいとき、修正箇所が数多くなってしまいます。さらに、修正箇所がどこにあるのか探すのは大変で、修正漏れが発生しやすくなります。

こんなときも役立つのが、間接参照です。色に名前をつけて定義し、それを参照する形で色を指定します。swiftなら「let」を使って次のようになります。

// どこかで事前に定義
let COLOR_BLACK = UIColor.blackColor()
let COLOR_WHITE = UIColor.whiteColor()
let COLOR_BLUE = UIColor.blueColor()
let COLOR_GREEN = UIColor.greenColor()
let COLOR_RED = UIColor.redColor()
let COLOR_CLEAR = UIColor.clearColor()
// 使う側
labelMessage.textColor = COLOR_RED

実は、このような作り方は悪い例です。色の名前のまま定義するだけでは、色を変更するときに、この定義を使っている箇所を書き直さなければなりません。「COLOR_RED」と書いてたのを、「COLOR_BLUE」に直すといったように。これでは、柔軟性の高い作り方ではありませんよね。

 

もっと賢い作り方があります。色の名前ではなく、色の役割の名前で定義する方法です。たとえば、注意レベルの違いを色分けするとき、次のように定義することができます。

// どこかで事前に定義
let COLOR_NORMAL = UIColor.blackColor()
let COLOR_OK = UIColor.greenColor()
let COLOR_CAUTION = UIColor.blueColor()
let COLOR_ERROR = UIColor.redColor()
// 使う側
labelMessage.textColor = COLOR_ERROR

このように作ると、どうでしょう。エラーの色を赤からオレンジ色に変えたいとき、「COLOR_ERROR」の色指定だけ修正すれば良く、「COLOR_ERROR」を使っている全箇所は触らずに済みます。エラーの色を定義し、それを間接参照する形で作ったからこそ、1カ所の変更で全部を変えられるようになったわけです。色の変更では、APIで用意された色だけでなく、RGB値による色指定も可能です。色の微妙な変更も、修正するのは一カ所だけです。

上記の作り方では、色の名前ではなく、色の使い分けの目的別に定義しています。通常の状態が「NORMAL」、処理成功が「OK」、ちょっとした注意を示すのが「CAUTION」、より重大なエラーを伝えるのが「ERROR」です。これら4つを組み合わせて使うことになります。

 

使い分けの目的別に作ると、同じ色(特に黒色)を何個も定義することになります。それでも構わないどころか、それが普通です。もう1つ、よく使う色の定義を挙げましょう。

let COLOR_SELECTED = UIColor.blueColor()
let COLOR_UNSELECTED = UIColor.blackColor()

名前を見て分かるように、選択状態と非選択状態の色を定義しています。ここの「COLOR_UNSELECTED」も、前述の「COLOR_NORMAL」も、色としては黒を指定していますが、使う目的は異なります。目的が異なるから、同じ色の指定なのに、別々に用意しているわけです。ソースコードの各箇所でどちらを使うのかは、使用する目的に合わせて選びます。

 

選択と非選択の色の定義では、どこに適用するかで使い分けることが重要です。何の選択に使うのかで、別々の名前を定義しておくと、後から何カ所も修正するといった面倒を防止できます。

// 一般的な選択と非選択の色
let COLOR_SELECTED = UIColor.blueColor()
let COLOR_UNSELECTED = UIColor.blackColor()
// 削除する場合の選択と非選択の色
let COLOR_DELETE_SELECTED = UIColor.redColor()
let COLOR_DELETE_UNSELECTED = UIColor.blackColor()

複数の色分けを、どのように使い分けたら良いかは、後々発生しそうな修正を予測しながら考えてみてください。

分ける場合の基本としては、UI部品の生成関数を複数作ったときと同じです。一緒に変わってほしくないものを分けるだけです。たとえ今は同じ値であっても、違う設定に変わりそうだと感じたなら、別々に用意するのが賢い選択でしょう。

 

色の指定は、多くの箇所に現れます。ここでは色の事前定義による作り方を紹介しましたが、すべての色指定を、事前の色定義にすべきとは思いません。前回のUI部品の生成関数のように、その関数自体が間接参照の考え方で作られている場合、その関数内の色しては、事前の色定義を使わなくても構わないでしょう。ただし、何かの目的で別な色と統一したい場合は、生成関数の中でも、事前の色定義を使うことがあります。

 

単純な色指定の話でも、長くなってしまいました。後から色を変えたいというのは、よくある話です。それだけに、今回紹介したような作り方は重要で、無駄な作業を減らしてくれます。

間接参照を使った作り方は、あまりにも有効なので、これから何度も登場すると思います。間接参照という説明が入ってなくても、間接参照の考え方で作られている場合も多いので、間接参照の考え方を頭の奥底にでも入れておくとよいかもです。

2014年10月21日火曜日

UI部品の生成を関数として用意する

設計方針で、Interface Builderもstoryboardも使わないと決めました。大きな理由の1つに、変更に対する柔軟性の確保があります。その実現には、UI部品を生成するための共通処理が必要です。

 

swiftの場合、具体的にはどうするのでしょう。考え方としては、どのUI部品でも、部品を使う側のソースコード上ではなく、別に用意した処理を通して生成することにします。swiftなら、関数が最適でしょう。UIButtonを生成する関数、UILabelを生成する関数など、部品の種類ごとに用意します。具体的にな例としては、つぎのようなコードになります。

func createLabelF(text: String, text_size: CGFloat, align: NSTextAlignment, 
        x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> UILabel {
    let iLabel: UILabel = UILabel(frame: CGRectMake(x, y, width, height))
    iLabel.backgroundColor = UIColor.clearColor()
    iLabel.text = text
    iLabel.font = UIFont(name: FONT_NAME_DEFAULT, size: text_size)
    iLabel.textColor = UIColor.blackColor()
    iLabel.textAlignment = align
    return iLabel
}
//(注:ここで「FONT_NAME_DEFAULT」は、別に定義した値です)

こうした関数で重要なのは、UI部品ごとのデフォルト設定を入れておくことです。デフォルト設定とは、一番使う設定のことです。決まった設定を全部入れておくことで、生成後に細かく設定する必要がなくなります。逆に、よく設定する項目は、引数に入れておきます。引数に入れておくことで、関数を呼び出した後、別に設定する必要がなくなります。

 

使い方は簡単です。たった1行だけで、UI部品を生成できます。次のように書くだけです。

let labelTitle = createLabelF("日付", 18, ALIGN_LEFT, 690, 320, 45, 25)
//(注:ここで「ALIGN_LEFT」は、別に定義した値です)

もしデフォルトと違う設定で作る場合も簡単です。生成したインスタンスに、必要な変更を加えるだけです。具体的な例は、次のようになります。

let labelTitle = createLabelF("日付", 18, ALIGN_LEFT, 690, 320, 45, 25)
labelTitle.layer.borderWidth = 1
//(注:ここで「ALIGN_LEFT」は、別に定義した値です)

このソースコードには、別な意味も含まれています。このラベルは、デフォルトのラベルとは違う設定で、borderWidthだけが変更されいていると分かるのです。

こうした関数を使わないで作る場合を考えてみてください。全部のソースコードに、ラベル生成をベタで書いた場合です。同じようなソースコードが各所に並んでるだけでなく、どれがデフォルトの設定か、またはデフォルトとはどこが違うのか、ソースコードから読み取るのは非常に困難です。共通の生成関数を作ることは、デフォルトを統一するだけでなく、デフォルトと何が違うのかまで明確にしてくれます。これも大きなメリットです。

 

UI部品ごとの生成関数は、いろいろな場面で使うはずです。ですから、どこからでも呼び出せる形にします。具体的には、1つのswiftソースファイルに集めて、プロジェクトに1個だけ用意します。私の場合は、ファイル名を「zz_base.swift」としてます。ファイル名の最初の3文字が、ソースファイルの分類となります。「zz_」は、プロジェクトで共通につかう機能を意味し、基本となる部品なので「base」としました。

このファイルは、関数を並べただけですから、どのswiftソースファイルからも関数名だけで呼び出せます。そうでないと使いにくいですから、わざとそうしているというわけです。

 

また、この一連の関数は、UI部品を使うアプリなら必ず入れます。1個用意しておくだけで、すべてのアプリで使えるというわけです。一度用意しておけば、ほぼすべてのアプリで共通に使えます。アプリのプロジェクトを生成したら、真っ先に入れるソースファイルとなります。アプリ共通で使えるライブラリと呼んでも良いでしょう。

 

ここからは応用の話です。UI部品ごとに1つの関数を用意するのは基本ですが、絶対に1つとは限りません。ボタンのデザインが2種類必要で、それぞれに別な役割や使いどころがあるとしたら、生成関数を2つ用意するのがベストです。それぞれ別々にデフォルト値を持ち、デフォルト値を変更してもお互いに干渉しません。分けた関数ごとに、デフォルトの変更は簡単ですし、それぞれ独立してデフォルトを変えられるのもメリットとなります。

また、別な拡張の仕方も容易になります。たとえば、特定の種類のボタンには、タップ中に別な表示や動きやをさせるといった場合です。UIButtonのサブクラスを作って、そこに表示や動きを記述します。該当する種類のボタン生成関数では、UIButtonの代わりにサブクラスのボタンを生成し、UIButtonボタンとして返します。このような方法なら、後からでもボタンの機能を容易に追加できます。変更するのは、サブクラスの追加と、ボタン生成関数だけです。ボタン生成を呼び出している側のソースコードを変更する必要はありません。もちろん、呼び出し側をまったく変更せず実現できる機能は限定されますが、変更への柔軟性が高いことは確かです。

 

以上のように、UI部品は必ず関数で生成するように作ると、変更への柔軟性は格段に高まります。また、一度作っておけば、どのアプリにも使い回しできて非常に便利です。UI部品の関数に限らず、このような複数アプリで共通で使えるライブラリが充実すると、まったく新規のアプリ開発でも、最初から3割ぐらい出来上がった状態で始めるのと同じです。

2014年10月20日月曜日

開発方針:Interface Builderもstoryboardも使わない

ブログのタイトルのとおり、Xcode+swiftでiPad上で動く業務アプリを開発しています。その際に気付いたことなどを、いろいろと書いてみます。一部の人には役に立つのではと思ったからです。これは良いと思った考え方やアイデアは、どんどん使ってください。

 

最初に作ったのは、Titanium mobileで作って運用中の業務アプリの移植でした。swiftとiOS APIを覚えるためにと、試しに移植してみることにしたわけです。このアプリでは、よく使われるUI部品(TableViewやWebViewも含む)、画面切り替え、ファイルの読み書き、オフスクリーンの描画、メール送信、ペーストボード、時間管理など、いろいろな要素が含まれていています。これを移植すれば、たいていの業務アプリが作れるようになると考えたからです。

実際、移植が終わった時点では、自分なりのライブラリが残り、これからのアプリ開発に広く役立つ基礎が整いました。また、移植したアプリは実際の業務で使われることとなり、今後はXcodeで開発することになりました。広い意味でも、移植してよかったです。

 

開発で一番大事なのが、設計方針です。設計方針といっても、何種類かの視点がありますが、まずは開発ツールの使い方です。何を使い、何を使わないかを決めなければなりません。xcodeでは、Interface Builderを使うか使わないか、storyboardを使うか使わないか、この2点が重要でしょう。結論としては、どちらも使わないで作ると決めました。

これらのツールを使うと、一見は作りやすく見えます。でも実際には、開発面では柔軟性が低い、つまり変更に弱いアプリに仕上がってしまいます。たとえば、全部のボタンを違う設定に変更したいとき、Interface Builderで個々に呼び出して変更したのでは、大変な手間になります。しかし、ソースコードでボタン生成の共通処理を作っておけば、その1カ所を変更するだけで、全てのボタンを一発で変えられます。このような柔軟性の高い作り方をするためには、できるだけソースコードで書いて作ることが重要なのです。

トラブル対処でも、できるだけソースコードで書いた方が安心です。ツールを使うと、そのツールの使い方が悪かったり、ツールにバグがあったりして、トラブルの原因が見えづらくなりがちです。ソースコードだけならば、原因は自分の作り方にあり、ソースコードだけを調べて原因追及できます。このほうが、はるかに効率的でしょう。

あと業務アプリでは、同じ種類のボタンや項目を、何個も並べることが多くあります。これもfor文で生成しながら配列に入れる処理が良く、ソースコードだけで作る方法に適しています。業務アプリこそ、ツールを使わない作り方に向いているのかもしれません。

 

ツールを使うと、明らかに良い面もあります。それはGUI設計で、最終的な形を見ながら作れる点です。これは捨てがたい魅力なのですが、なくても何とかなります。画面設計は、専用の空アプリを用意して対応します。1画面だけの中身なしアプリで、ソースコードにより部品を並べていきます。軽いアプリのため、シミュレーターを起動するのも短時間で済みます。ソースコードの表示位置や大きさを変更し、シミュレーターを起動して確認する作業を何回か繰り返すだけです。良いと思ったら、ソースコードを目的のアプリへコピーします。

このアプリのおかげで、GUI設計がだいぶ楽になりました。画面ごとにコピーして、必要な画面分だけ作りました。あとで生じるのは微調整だけなので、大きく困ることはありませんでした。

 

のっけから、あまり一般的ではない作り方だと感じたかもしれません。また、ツールを使わずに作るのは大変だと思うかもしれません。でも試してみると、それほど大変ではありませんでした。余計なトラブルに巻き込まれないためにも、柔軟性の高いアプリを作るためにも、Interface Builderやstoryboardを使わない作り方をお薦めします。

今後は、これらのツールを使わないで作るときのコツや、柔軟性の高い設計方法を書いていければと思っています。Titaniumのブログで書いたのと重複する内容でも、気にせずに書きます。こちらに書かないと、検索で見付からないでしょうから。