2015年6月3日水曜日

モーダルビューをライブラリ化(本編2+使用例編)

モーダルビューをライブラリとして作った話の続きです。前回では、Swiftコードの説明が途中まででした。残りのソースコードを挙げながら、要点を説明します。さらに、簡単な使用例も紹介します。

 

いよいよ、肝となるボタンとレイアウトを取り上げます。ボタンはキャンセルとOKの2つがあり、基本的には次のように使います。

キャンセルボタンがタップされると、何もせずに、モーダルビューを閉じるだけです。

OKボタンがタップされると、設定された関数を実行します。初期状態では、このクラスの関数が設定されていますが、通常の使い方では、アプリ側で用意した関数を設定し、タップされたときに実行させます。

何かの登録処理でモーダルビューを使う際には、まだ何も選択されていない状態のとき、選択を促すメッセージを表示して、モーダルビューは開いたままにします。このように、開いたまま使い続ける場合もあり得るので、OKボタンの処理には、自動的に閉じる処理は付けていません。

OKボタンでモーダルビューを閉じたい場合には、アプリ側が設定した関数内から、閉じるメソッドを実行します。つまり、アプリ側の関数から、このクラスの閉じるメソッドを呼び出す形となります。

ボタンの初期化と、ボタンに関するメソッドは、次のようなSwiftコードになりました。

// ======================================== ボタン
var btnCancel: UIButton!
var btnOKay: UIButton!
var funcOKay: () -> Void = { () -> Void in return }

// 初期化
private func setupBtnF() {
    // キャンセルボタン
    btnCancel = zCreateBtnF("キャンセル", 20, 400, 550, 120, 50)
    btnCancel.addTarget(self, action:Selector("runCancelF:"), forControlEvents: .TouchUpInside)
    self.addSubview(btnCancel)
    // OKボタン
    btnOKay = zCreateBtnF("OK", 20, 600, 550, 120, 50)
    btnOKay.addTarget(self, action:Selector("runOkayF:"), forControlEvents: .TouchUpInside)
    self.addSubview(btnOKay)
    funcOKay = funcDummyF    // ダミーの警告関数を設定
    // サイズとレイアウトの設定
    setBtnSizeF()
    setBtnPositionF()
}

// キャンセル処理:アニメーションで自分自身を隠す
func runCancelF(rSender:UIButton) {
    closeF()
}
// OK処理:変数に代入した関数を実行する(Viewのclose処理は、必要な場合にアプリ側で行う)
func runOkayF(rSender:UIButton) {
    funcOKay()
}
// OKボタンのダミー処理:未設定をメッセージで知らせる
private func funcDummyF() {
    setMsgF("内部エラー:OKボタンに処理が設定されていません。")
}

// OK処理で実行する関数を設定
func setFuncF(rFunc:() -> Void) {
    funcOKay = rFunc
}
// 2つのボタン名を設定
func setBtnNameF(rStrC:String, _ rStrO:String) {
    btnCancel.setTitle(rStrC, forState: .Normal)
    btnOKay.setTitle(rStrO, forState: .Normal)
}

初期化の処理では、まず2つのボタンを生成します。このときの大きさや表示位置は、仮の値です。初期化の最後に、レイアウトを設定する処理を実行するからです。

ボタンがタップされたときに実行する関数も、初期化の処理内で設定します。キャンセルボタンのほうは、このモーダルビューを閉じるだけの処理です。

OKボタンのほうは、関数を入れる変数funcOKayに入っている関数を実行します。この変数をOptional型にしたくなかったので、変数宣言の際に、何もしない処理(クロージャ)を入れました。

この変数のデータ型は、「() -> Void」にしてあります。これは「() -> ()」とも書けるのですが、Swiftに詳しくない人が少しでも分かりやすいようにと考え、あえてVoidを選んでいます。2種類以上の表記が可能な場合、初心者が少しでも理解しやすい方を選ぶのが、私の方針だからです。

この変数に入れる関数は、アプリ側で必ず入れるものです。入れ忘れを気付きやすいように、ダミーの関数を用意して、それを初期化の処理で入れています。このダミー関数を実行すると、メッセージ欄に「内部エラー:OKボタンに処理が設定されていません。」と表示されます。いつものエラーメッセージと少し違うのは、画面上に表示するメッセージなので、ユーザーが読む可能性があるからです。

当然ですが、OKボタンに関数を設定するメソッドも必要です。関数のデータ型を指定してあるため、とくにエラー検査はしていません。

ボタン名を自由に変えたい要望もあるでしょう。2つのボタンを一緒に変えるメソッドの形にしました。おそらく最初の1回だけ実行するメソッドなので、メソッドをまとめたほうが効率的と判断したからです。

 

続いて、ボタンのサイズとレイアウトを設定する機能です。上記の初期化処理の最後でも呼ばれます。最初に、レイアウト機能に関する考え方を取り上げましょう。

今回のモーダルビューを使う際には、ボタンの座標を細かく指定しないと決めました。アプリ側では大まかな指定だけして、ビューのライブラリ側で自動レイアウトする形です。

アプリ側が指定する値としては、ボタンの大きさ、水平方向の広がりの大きさ、垂直方向の広がりの大きさの3つです。それぞれで、7種類の値から選ぶ形にしました。

どちらが大きいのか簡単に識別できるように、服などのサイズ指定で使われているS、M、Lを用いてます。このままだと3種類しかないので、3Sから3Lまで使って7種類としました。中央の値がMですが、これが標準の位置を意味するわけではありません。並び順として、単純に真ん中の値というだけです。もっと良い表現方法があれば良いのですが、思い付きませんでした。

以上の考えをまとめて作ったのが、以下のSwiftコードです。

// ボタンの大きさ
enum ZmvBtnSize: Int {
    case size_3S = 0
    case size_2S = 1
    case size_S = 2
    case size_M = 3
    case size_L = 4
    case size_2L = 5
    case size_3L = 6
}
// ボタン位置のX軸(横方向)の広がり
enum ZmvBtnPosiX: Int {
    case posiX_3S = 0
    case posiX_2S = 1
    case posiX_S = 2
    case posiX_M = 3
    case posiX_L = 4
    case posiX_2L = 5
    case posiX_3L = 6
}
// ボタン位置のY軸(縦方向)の広がり
enum ZmvBtnPosiY: Int {
    case posiY_3S = 0
    case posiY_2S = 1
    case posiY_S = 2
    case posiY_M = 3
    case posiY_L = 4
    case posiY_2L = 5
    case posiY_3L = 6
}

見てのとおり、列挙型の値として作ってあります。7種類から選ぶ項目が3つもあるので、大きさを示すSMLだけでは間違えやすくなります。そうならないように、SMLの前に値の名前を加えました。

 

いよいよレイアウトの実行コードです。

自動レイアウト機能を付けたので、その指定方法を最初に説明する必要があります。指定できる項目が3つあって、それぞれ7種類の値の中から選べることを、ソースコードの先頭で説明しています。

その後に、どんなレイアウトになるかを続けます。レイアウトのイメージが頭の中に浮かぶように、構造を上手に表現する必要があります。いつものように縦軸と横軸で、レイアウトの順に並べる形で変数名などを書きました。

このようなことを考えながら作ったのが、次のSwiftコードです。レイアウト説明の箇所では、変数とインデックスの関係を伝えるためのコメントも付けてあります。

// ======================================== サイズとレイアウトの変更
// 3種類の設定項目があって、それぞれ7つの値から選べる
// メッセージ欄は同じ大きさのまま、上下の位置だけ変わる。いつもボタン直下に置かれて、位置の指定はできない
// ボタンの大きさが7種類、X(水平方向)軸の中央のボタン間隔が7種類、Y(垂直方向)軸の下の余白が7種類

// 上記を整理したレウアウト説明
// ーーーーーーーーーー レイアウト(X軸:横方向):左右対称
// 余白:余り(可変)
let W_BUTTON: [CGFloat] = [120, 140, 160, 200, 240, 280, 320]        // ボタン:キャンセル:W_BUTTON[numBtnSize]
let W_MARGIN_CENTER: [CGFloat] = [50, 100, 150, 200, 250, 300, 350]  // 余白:W_MARGIN_CENTER[numBtnPosiX]
// ボタン:OK:W_BUTTON[numBtnSize](キャンセルと同じ大きさ)
// 余白:余り(可変:上の可変と同じ大きさ)
// ーーーーーーーーーー レイアウト(Y軸:縦方向)
// 余白:余り(可変:このビューへ追加で貼り付けるUI部品は、ここに配置する)
let H_BUTTON: [CGFloat] = [40, 45, 50, 55, 60, 70, 80]               // ボタン:H_BUTTON[numBtnSize]
let H_MARGIN_BOTTOM: [CGFloat] = [350, 300, 250, 200, 150, 100, 50]  // 余白:H_MARGIN_BOTTOM[ZmvBtnPosiY]

// ボタン内の文字サイズ:ボタンサイズに連動
let TEXT_SIZE_BTN: [CGFloat] = [16, 18, 20, 24, 28, 32, 36]          // TEXT_SIZE_BTN[numBtnSize]

// 上記の設定値:配列のインデックス
var numBtnSize: ZmvBtnSize = .size_M
var numBtnPosiX: ZmvBtnPosiX = .posiX_2L
var numBtnPosiY: ZmvBtnPosiY = .posiY_2L

// ボタンのサイズを計算して設定
private func setBtnSizeF() {
    // キャンセルボタン
    let iWidht: CGFloat = W_BUTTON[numBtnSize.rawValue]
    let iHeight: CGFloat = H_BUTTON[numBtnSize.rawValue]
    let iSize: CGSize = CGSizeMake(iWidht, iHeight)
    btnCancel.frame.size = iSize
    btnCancel.titleLabel!.font = UIFont.systemFontOfSize(TEXT_SIZE_BTN[numBtnSize.rawValue])
    // OKボタン
    btnOKay.frame.size = iSize
    btnOKay.titleLabel!.font = UIFont.systemFontOfSize(TEXT_SIZE_BTN[numBtnSize.rawValue])
}
// ボタンとメッセージ欄の位置を計算して設定
private func setBtnPositionF() {
    // キャンセルボタン
    let iX1: CGFloat = (screenWidth - W_MARGIN_CENTER[numBtnPosiX.rawValue]) / 2 - W_BUTTON[numBtnSize.rawValue]
    let iY1: CGFloat = screenHeight - H_MARGIN_BOTTOM[numBtnPosiY.rawValue] - H_BUTTON[numBtnSize.rawValue]
    btnCancel.frame.origin = CGPointMake(iX1, iY1)
    // OKボタン
    let iX2: CGFloat = (screenWidth + W_MARGIN_CENTER[numBtnPosiX.rawValue]) / 2
    btnOKay.frame.origin = CGPointMake(iX2, iY1)
    // メッセージ欄:左右の中央で、ボタン直下(余白は10)に付ける
    let iWidth: CGFloat = lblMsg.frame.size.width
    let iX3: CGFloat = (screenWidth - iWidth) / 2
    let iY3: CGFloat = iY1 + H_BUTTON[numBtnSize.rawValue] + 10
    lblMsg.frame.origin = CGPointMake(iX3, iY3)
}
// ボタンのサイズや位置を設定(それぞれ用意した種類から選ぶ)
func setBtnLayoutF(rNumBtnSize:ZmvBtnSize, _ rNumPosiX:ZmvBtnPosiX, _ rNumPosiY:ZmvBtnPosiY) {
    numBtnSize = rNumBtnSize
    setBtnSizeF()       // ボタンのサイズを設定
    numBtnPosiX = rNumPosiX
    numBtnPosiY = rNumPosiY
    setBtnPositionF()   // ボタンの位置を再計算
}

7種類の値を入れた変数のインデックスも、別な変数(名前がnumで始まる3つの変数)として用意してあります。その初期値としては、ボタンの大きさは真ん中の値、ボタンの位置は一番広い値にしました。これらの値を変更しない場合、ビュー上に置いたUI部品と重なりにくいように配慮したためです。通常の使い方としては、3つの値を必ず設定するはずですが、知らないで使い始めたときのためにです。

上記ソースコードの一番最後が、3つの値を設定するメソッドです。最初に設定したまま使うのが一般的なので、まとめて設定できるようにと、1つのメソッドにまとめました。値を設定するとともに、ボタンの大きさやレイアウトを再計算しています。

ボタンサイズの計算処理では、1つの設定値をもとに、ボタンの幅と高さ、中に表示する文字の大きさを決めています。あらかじめ用意した配列の値から持ってくる形です。細かな組み合わせは選べません。あまり細かく指定できても面倒ですし、変な組み合わせでは見栄えが悪くなります。バランスの良い組み合わせを事前に用意し、その中から選ぶ形が一番使いやすいと考え、このような形にしました。

ボタンとメッセージ欄のレイアウト計算処理では、X軸とY軸で余白を与える形にしてあります。

X(横)軸は、2つのボタンが左右対称に配置され、ボタンの間の余白を配列から持ってきます。ボタン幅とスクリーン幅が決まってますから、残りの長さを左右に分割して、それぞれのボタンのX軸の座標が計算できます。

Y(縦)軸は、下の余白の大きさを配列から持ってきます。ボタン高さとスクリーン高さが決まってますから、残りが上側の余白となり、ボタンのY軸の座標が計算できます。左右対称ですから、2つのボタンが同じ値になります。

メッセージ欄の位置は、常にボタンの下に来るようにしました。X軸は中央に配置するため、自分の幅とスクリーン幅から計算します。ボタンの位置や大きさに関係ありません。Y軸は、ボタンの下端から10だけ離れた位置にしてあります。

 

以上で、モーダルビューのソースコードは終わりです。機能は単純ですが、いろいろと考えて作っているため、ソースコードの量は、最初に思っていたより多くなってしまいました。やはり、ライブラリとして作ると、どうしても増えてしまうようです。

今回のライブラリの肝は、簡単に使える点です。この種のライブラリの場合、実現するための技術的な難しさは、ほとんどありません。思い付いたら、すぐに作り終わるはずです。

でも、実際に使いやすく仕上がるかどうかは、別な問題です。細かな座標を指定して使うなら、最初から作っているのと同じで、ライブラリを使う意味がありません。大まかな指定をするだけで、ボタンが左右対称に配置され、ほど良い感じに仕上げてくれることこそ、この種のライブラリ作成で、労力を一番注ぐべき点だと思います。

今後、より大きなスクリーンサイズのiPadが登場したときも、値を入れている配列に、より大きな値を追加するだけで対応できるでしょう。今の7種類が、9種類や11種類に増える形で。そうした点も考慮しながら、今回のライブラリは作りました。

 

続いて、使い方を紹介します。これを作るきっかけとなった、フォトライブラリから写真を選ぶ機能のライブラリ(当ブログにて以前に紹介)と一緒に使ってみます。つまり、写真を選ぶモーダルビューとして使うわけです。

いつもiOS実験専用アプリで開発し、そこでテストも実行していますから、その中の一部を抜き出して来ました。見やすく整えるのが面倒なので、そのまま紹介します。

まず、OKボタンがタップされたときに実行する関数を、アプリ側で用意します。当然ですが「() -> Void」の形式で作ります。

// OK(伝達)ボタンをタップしたときに実行される関数
func okay13bF() {
    if photoSelector13.isSelectedF() {
        if let iImg: UIImage = photoSelector13.getImageF() {
            // ここに、画像登録の処理を入れる
            // (たとえば、アプリ内の変数へ画像を入れるための処理を呼び出すとか)
            setMsgF(4, "画像を登録しました。")         // アプリ側のメッセージ欄に表示
        } else {
            setMsgF(4, "画像の取得に失敗しました。")   // アプリ側のメッセージ欄に表示
        }
        view13.closeF()
    } else {
        view13.setMsgWTimerF("画像を選択してください。", .Caution) // モーダルビューに表示
     }
}

見てのとおり、この関数がやり取りする主な相手は、モーダルビューではなく、その上に貼り付けられた写真選択機能(photoSelector)です。写真が選択されていなければ、選択を促すメッセージをモーダルビュー側へ出して終了です。モーダルビューは閉じません。

逆に、写真が選択されていれば、if letでUIImageへ代入を試みます。もし成功すれば、画像登録処理を実行し、アプリ側のメッセージ欄にメッセージを出して、モーダルビューを閉じます。代入が失敗した場合は、何もせずに、アプリ側のメッセージ欄にメッセージを出して、モーダルビューを閉じます。どちらの場合にでも、モーダルビューは閉じられ、成功または失敗のメッセージが、アプリ側に残ります。

 

関数が用意できたので、モーダルビューを扱うコードを作ります。モーダルビューとPhotoSelectorのインスタンスを入れる変数を用意して、準備から表示までのSwiftコードは、以下のようになります。

// モーダルビューの使用例:フォトライブラリから写真を選ぶ
var view13: ZModalView!               // モーダルビューの変数
var photoSelector13: PhotoSelector!   // 写真選択ビューの変数

// モーダルビューを用意して設定
view13 = ZModalView()
view13.setupF()                                      // 初期化
view13.setFuncF(okay13bF)                            // 関数の設定
view13.setBtnNameF("キャンセル", "登録")             // ボタン名の設定
view13.setBtnLayoutF(.size_S, .posiX_3L, .posiY_M)   // ボタンのサイズとレイアウトの設定
self.view.addSubview(view13)                         // アプリのビューへ貼り付け
// 写真選択ビューを用意して、モーダルビューに貼り付ける
photoSelector13 = PhotoSelector()
photoSelector13.setupF(50, 100, 900, 400)
view13.addSubview(photoSelector13)                   // モーダルビューへ貼り付け
// モーダルビューを開始
view13.openF()

見てのとおりです。コメントを多めに付けたので、何をしているのか分かるでしょう。ボタンのサイズやレイアウトを決める3つの値の設定でも、SMLに項目名を加えた効果が出て、理解しやすくなっています。

 

モーダルビューを使う際には、その処理全体を関数として用意し、インスタンスを入れる変数も関数内に作ります。そうすれば、モーダルビューを使い終わったとき、変数も一緒に消えて、使用していたメモリーが解放されます。

このようにメモリーの使用と解放を意識した作り方が、他の言語と同様に、Swiftでも必要となりますね。

 

今回は、かなり簡単な機能と言えるモーダルビューのライブラリ化を紹介しました。簡単な機能だからこそ、手間をかけずに使える工夫が重要です。また、将来の拡張性にも配慮しておかないと、後からのメンテが大変になります。

こんな簡単な機能でもライブラリ化できる例として、面白かったのではないでしょうか。今回の投稿を読んで「もしかして、これってライブラリ化できるかも」と思ったアナタ、さっそく作り始めてください。アプリを作るより、ライブラリを作る方が、意外に面白かったりしますので。

 

(使用開発ツール:Xcode 6.3.2, SDK iOS 8.3)

0 件のコメント:

コメントを投稿