2015年6月11日木曜日

ファイルの読み書きも間接参照で(作成理由編)

アプリで作成したデータを保存する場合、ファイルとして書き出すことが多いでしょう。作成データだけでなく、アプリの設定内容なども、ファイルとして保存することが多いはずです。

こうしたファイルに関する読み書きは、独自ライブラリとして用意し、それを使うことで、似たようなコードを何度も書かなくて済みます。ここまでなら、ライブラリ化が好きな人だと普通に行っているでしょう。

では、さらに1歩進めて設計するとしたら、どのような構造になるでしょうか。その辺の検討結果と、実際に作ったクラスを紹介ます。

 

まずアプリの一般的な構造を考えましょう。アプリの中心部分では、そのアプリに固有の情報を扱っていて、画面に表示したり、入力を求めたりします。入力されたデータや、外部から取得したデータを保存するとき、ファイルへ書き出すのが一般的です。

保存先としては、ファイル以外でも可能です。たとえば、ネット上のサーバーとか。つまりファイルとは、情報の保存先の1つでしかありません。

いくつかの中の1つである以上、切り替え可能なように作るのが良い作り方でしょう。保存先を単純に切り替えるだけでなく、同じ情報を複数に保存するとか、その片方への保存は1日に1回だけとか。切り替え可能な形で作っておくことで、保存する機能の拡張が格段にやりやすくなります。

つまり、アプリの内部構造として、中心部分の処理から、保存する処理を分離したいわけです。中心部分の処理では、どこに保存するのか、どんな名前で保存するのか、まったく知らない形となります。保存データを作った際には、保存する処理にデータを渡すだけで、あとは知らないという感じです。

 

以上のように考えた結果、ファイルの読み書きを間接参照で作ることにしました。ファイルの読み書きの基本的な部分は、すでにライブラリ化してあります。現状のアプリでは、それを直接呼び出しているのですが、今後は間接参照に変更しようということです。

間接参照をどのように作るのかが、一番重要です。大事な機能としては、保存先を切り替え可能に作る点があります。つまり、アプリの中心的な処理では、どこへ保存するのか知らなくても動くように作り、保存する処理が複数あって、それらが交換可能になるという形です。

そこで、間接参照を実現するためのリンククラスを用意することにしました。アプリ側では、このリンククラスのインスタンスを生成し、そのインスタンスへアクセスすることで、ファイルへの読み書きを実現します。具体的には、このリンククラスが、ファイルの読み書きのライブラリを呼び出して処理します。今まではアプリが、ファイル関連のライブラリを直接呼び出していましたが、改良版では、アプリがリンククラスを呼び出し、そのリンククラスがファイル関連のライブラリを呼び出します。また、どこへ保存するか、どんな名前で保存するかはリンククラスが持ち、アプリの中心処理では持たない形となります。

アプリがリンククラスのインスタンスを生成する際に、保存場所(ディレクトリ)とファイル名を指定します。そのため、ファイルとして保存するデータの数だけ、インスタンスを生成することになります。1つのファイル用に1つのインスタンスを生成するというルールです。

リンククラスで重要なのは、交換可能に作ることです。ネット経由での保存など、ファイル以外の保存先も考えられます。どの保存先でも共通のメソッドを持たせ、保存先が変更されても、ソースコードの変更を最小限に抑えます。

その実現で大事なのは、メソッド名と引数のデータ型です。これらが同じであれば、別なクラスに切り替えたとしても、修正箇所はたった2箇所だけになります。たとえば、変数名prefStorageで今回のリンククラスをインスタンス生成した場合、インスタンスへのアクセスは変数名prefStorageとメソッド名だけしか使いません。もしメソッド名が全部同じで、ネット上へ保存するリンククラスを別に用意したとき、次の2箇所だけ変更すれば良くなります。インスタンス生成するコードのリンククラス名を変更し、インスタンス生成で指定するする初期化の値(ディレクトリ位置やファイル名)を変えるだけです。それ以外のコードである、変数prefStorageへアクセスする箇所のコードは、変更しなくて済みます。このような形であれば、交換可能の良い実現方法でしょう。

以上のように考えると、ファイルの読み書きを強く意識したメソッド名(readやwrite)は適しません。もっと一般的なloadやsaveが、より適していると言えます。そこでリンククラスでは、メソッド名にloadとsaveを使うことにしました。

 

過去に作ったアプリを見ると、単純なloadとsaveだけでは不足です。デフォルト値を入れたファイルがあり、それを呼び出す機能も必要です。そこで、loadを2つ用意します。単純なloadと、デフォルト値のloadです。

以上を整理すると、次のようになります。とりあえず、ファイル編だけです。

リンククラスの主な仕様(ファイル編)
・保存先の情報を持つ:ディレクトリとファイル名
・初期化:保存先ディレクトリとファイル名を設定する
・保存メソッド:値を受け取ってファイルに保存する
・ロード・メソッド:保存されている値をロードする
・デフォルト値のロード・メソッド:デフォルト値をロードする

機能が少ないので、かなり単純なクラスになるでしょう。

 

さて、ここまでの話では、保存するデータの中身について触れていませんでした。保存データのデータ型が違うと、同じ処理を使えません。たとえば、ファイルから読み込む処理は、文字列、バイナリー、NSDictionary、NSArrayで違います。ファイルへ書き出す処理も同様です。つまり、扱うデータ型ごとに、別々なクラスを用意しないといけないわけです。

リンククラスの種類(ファイル編)
・文字列(文字コードはUTF-8)
・NSDictionary
・NSArray
・バイナリー

構造が簡単なクラスで、しかもファイルの読み書きライブラリが既に作成済みなので、作るのは簡単です。

それぞれのクラスでは、扱うデータの型が決められています。そのおかげで、コンパイラの型チェック機能が有効に働き、ちょっとした間違いなどをコンパイル時点で発見できるようになります。

 

リンククラスの作成理由を簡単に紹介するつもりでしたが、書いてみたら予想より長くなってしまいました。いつもの全体設計編というレベルではないので、作成理由編とし、ここで一旦区切ります。具体的なSwiftコードは、次の投稿にて。

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)

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)