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)

0 件のコメント:

コメントを投稿