たいていのアプリでは、実行結果やエラー内容などを伝えるために、短い文章でメッセージを表示します。とても単純な機能ですが、深く考えていくと、いろいろと工夫できる余地があります。メッセージ表示用の関数を独自ライブラリ用として作ったので、それとともに紹介します。
メッセージの表示で大切なのは「安心して使えるための心遣い」です。コンピュータ上で動作するアプリは、中でどのように処理されているのか、まったく見えません。タップや文字タイプの操作を受け付けていますが、それがどう処理されているのかは見えないのです。それだけに、ユーザーは多少の不安を感じながら操作していると思います。
その不安を少しでも解消するために、メッセージの上手な出し方が役立ちます。基本的な考え方は、何か操作したら、できるだけ即時に反応を返すことです。成功したのか失敗したのか、必ず返すようにします。するとユーザーは、自分の操作が受け付けられて処理されたと、安心するものなのです。
さらに上手な作り方は、単なる成功や失敗を伝えるのではなく、処理内容に関する情報を含めることです。「登録しました」よりも「1件を登録しました」とか「2件を登録しました」のほうが、より安心感をユーザーに与えられます。同時に、間違った操作を(まれにアプリのバグを)発見するきっかけにもなります。2件登録したと思ったのに、「1件を登録しました」と表示されたときとか。
さらなる改良も可能で、登録したデータの中身も表示するようにも作れます。ただし、詳しい情報を多く含めると、作るのが大変になっていきます。ほどほどで十分でしょう。私の場合は、件数を含めるぐらいで止めています。
メッセージの表示で気を付けたいのが、表示し終わったあとの処理です。そのまま表示しっぱなしでは、後で見たときに違和感を生じさせます。数秒間だけ表示した後、自動的に消すのが賢い作り方でしょう。
何秒間だけ表示するかは、表示する内容によって決まります。成功したのであれば、短い時間で構わないでしょう。逆にエラーを知らせるメッセージなら、成功よりは長めの表示時間が必要です。アプリ全体で標準化し、成功メッセージは何秒、注意メッセージは何秒、エラーメッセージは何秒と決めるとよいでしょう。ただし、使われる場面やメッセージの種類によっても変わりますから、アプリ内のメッセージを何種類かに分け、それぞれで成功やエラーの表示秒数を決めるのが良さそうです。場面ごとに使い心地が違うので、なかなか決めづらいとは思いますが。
表示時間以外にも、気を配りたい点があります。その1つが文字の色分けです。どんな色でも自由に使って構わないわけではありません。メッセージの内容に応じて、適した色を割り当てる必要があります。普段の生活で使われている色分けに合わせるのがベストです。
私の場合は、処理が成功すると緑色、ちょっとした注意を伝えるときは青色、エラーを示すときは赤色にしています。一般的な色のイメージに合わせた色分けだと思います。
このような色の割り当てでアプリを作ると、メッセージよりも色だけを見て、成功したか判断するようになります。ほとんどが緑色しか出ませんから、緑色で表示されれば安心して次の処理へ進めます。もし青色や赤色で表示されたら、いつもと違うと認識して、エラーメッセージを真剣に読むことになります。
今まで作ったアプリでは、次のような形でコーディングしていました。メッセージを表示するUILabelには、専用の関数を用意してアクセスします。お馴染みの間接参照で、メッセージを消す関数も用意します。表示した後で消す処理はNSTimerで実現し、数秒後に消す関数を呼び出す形でした。NSTimerで数秒後に呼び出す部分だけは、独自ライブラリの関数として作り、それを呼び出して使っていました。
メッセージを消す処理では、ちょっとした問題が発生します。指定した秒数後に単純に消してしまうと、次のメッセージが続いてい表示されるときに、それも消してしまうのです。具体的な動きで説明しましょう。最初のメッセージを表示して、5秒後に消す設定だとします。5秒経過する1秒前に別なメッセージが表示されると、その新しいメッセージを1秒後に消してしまうのです。
これを防ぐために、次のような形で作りました。表示するメッセージごとにidを付けて、メッセージを消す処理にもidを持たせます。消す処理の中では、消そうとしているメッセージのidが、自分が持っているのと同じidだったときだけ、メッセージを消すという形にします。すでに別なメッセージが表示されていれば、消さずに終了というわけです。
各メッセージのidは、乱数から生成させて重複を防いでいます。乱数を使えば、同じ処理から発生したメッセージでも異なるidになるため、間違って消すことはありません。メッセージ処理の内容は、大まかに以上のような感じです。
それを最近になって改良し、どのUILabelでも使えるメッセージ専用の関数に仕上げました。自分専用の独自ライブラリに入れて、次のアプリから使う予定です。メッセージ表示先のUILabelを指定できるため、どのメッセージ欄にも共通して使えます。
仕上がるまでの過程も紹介しましょう。最初は、どこからでも呼び出せるようにと、どのクラスにも所属しない関数(アプリ内のグローバル関数)として作ろうとしました。しかし、さっそく問題が発生します。NSTimerで関数を呼び出すときには、関数が所属するtargetを指定しなければなりません。クラスに所属しないグローバル関数だと、何に所属しているのか分かりません。いろいろ調べても判明しませんでした(知っている方がいたら教えてください)。
仕方がないので、ライブラリ用のクラスに所属する関数に切り替えました。メッセージ専用のクラスでも構わないのですが、メッセージ以外の様々な関数を入れた方が管理しやすそうです。クラスに所属しない関数は、ファイル名「BP_Base.swift」に入れています。これと似たような扱いで、これのクラス版としての意味を込め、ファイル名を「BC_Base.swift」としました。クラスに入れないと作れない関数の、入れ物クラスというわけです。
クラスに所属する関数として試すと、メッセージを消すための関数が起動しません。targetに指定したクラスには、そのような名前の関数がないと、実行時に怒られます。いくら確認しても、関数は確かにあります。コピーペーストしたので、関数のスペルも間違っていません。ネットで検索したところ、次のような原因が判明しました。targetとして指定するクラスは、NSObjectを継承していなければならないという条件があったのです。なるほど、NSTimerは、NSObjectに含まれる何かを使っているのですね。さっそく入れ物クラスを、NSObjectから継承するように指定しました。
以上のような流れで問題が解決し、無事にメッセージ関数が出来上がりました。
いよいよ関数の中身です。用意する関数は2つで、アプリから呼び出すsetMsgWTimerF関数と、指定された秒数後にメッセージを消すclearMsgWIdF関数です。具体的なSwiftコードは、次のとおりです。
// 仕方なくクラスに入れたライブラリ関数群(今のところメッセージ処理だけ)
// メッセージの種類の定義
enum MsgType : Int {
case Normal = 0
case OK = 1
case Caution = 2
case Error = 3
case Debug = 101
case InternalError = 102
}
// クラス本体
class BaseFunc : NSObject {
func setMsgWTimerF(rLabel:UILabel, _ rStrMsg:String, _ rType:MsgType, _ rSec:NSTimeInterval) {
if (rSec <= 0) { println("ERROR:BF_SMWT:rSecがゼロ以下"); return }
switch rType {
case .OK: rLabel.textColor = COLOR_OK
case .Caution: rLabel.textColor = COLOR_CAUTION
case .Error: rLabel.textColor = COLOR_ERROR
case .Debug: rLabel.textColor = COLOR_DEBUG
case .InternalError: rLabel.textColor = COLOR_INT_ERROR
default: rLabel.textColor = COLOR_NORMAL
}
rLabel.text = rStrMsg
// 乱数から得たidをセットして、タイマー開始
let iId : Int = getRandomF(100000)
rLabel.tag = iId
let iParam : NSDictionary = ["iId": iId, "iLabel": rLabel]
let iTimer : NSTimer = NSTimer.scheduledTimerWithTimeInterval(rSec, target:self, selector:Selector("clearMsgWIdF:"), userInfo:iParam, repeats:false)
NSRunLoop.currentRunLoop().addTimer(iTimer, forMode:NSRunLoopCommonModes)
}
func clearMsgWIdF(rTimer: NSTimer) {
let iUserInfo : NSDictionary = rTimer.userInfo as NSDictionary
let iId : Int = iUserInfo["iId"] as Int
let iLabel : UILabel = iUserInfo["iLabel"] as UILabel
// 同じidだったときだけ、メッセージを消す
if (iId == iLabel.tag) {
iLabel.text = " " // 半角の空白文字を5個
iLabel.textColor = COLOR_NORMAL
}
}
}
難しい部分はほとんどないので、要点だけ解説します。クラス内なのでメソッドと呼ぶべきなのですが、関数という扱いで使っていますから、これ以降も関数として説明します。
まず、メッセージを表示するsetMsgWTimerF関数から。引数のエラーチェックは、最低限だけです。何秒後に実行するかの秒数が、ゼロ以下だとエラーにしています。
COLOR_OKなどはUIColor型の色定義で、間接参照として用意しています。役割ごとに色を定義し、いろいろな箇所から参照することで、色を変える場合に一カ所の変更で済むようにしてあります。ここでは、デバッグ用と内部エラー用の色も別に割り当てて、使い分けています。
関数内で使っているgetRandomF関数は、乱数からidを得る関数です(ベタで書いても1行で済むのですが、乱数用の関数として用意することで、間接参照を実現しています)。最大値を指定して呼び出します。乱数なので、万が一ぐらいの確率で一致することも考えられます。しかし、一致したときの影響は、表示が少し変になる程度なので、実用上は問題ありません。得られたidは、UILabelのtagプロパティに入れておきます。別なメッセージで上書きされたら、自分のidとは異なるtag値に変わっているはずです。
こうして書きながら、あらためて考えると、乱数を使う必要はなかったように思います(汗)。もっと単純な方法でも可能でしょう。こういうことが、たまにありますよね。もう直すのが面倒なので、とりあえずは今のまま使います。後で直すかもしれませんが。
数秒後に実行されるclearMsgWIdF関数へ渡すデータは、NSDictionaryとして作り、NSTimerのuserInfoに設定します。単純な配列でも渡せるようですが、渡すデータの型が異なるので、入れ物としてNSDictionaryを用いました。
続いて、メッセージを消すclearMsgWIdF関数を。NSTimerのuserInfoから、引数を受け取ります。NSDictionaryにキャストしてから、それぞの値もキャストして取り出します。受け取ったUILabelの現在のtagプロパティが、同じく受け取ったidと比べて、同じ場合にだけメッセージをクリアーします。
こんなに簡単な作りですが、しっかりと動いてくれます。短時間に何個のメッセージを表示しても、間違ってクリアーされる心配はありません。
上記の関数を使う側のSwiftコードも紹介します。指定した秒数後にメッセージが消せ、役割に適した色でメッセージを表示できる処理が、たった1行で記述できます。しかも、どのラベルに表示するかも指定可能です。次のようなSwiftコードで。
// 自動消去メッセージの使用例
...
let zBase : BaseFunc = BaseFunc() // アプリの初期処理でインスタンス生成
...
class AnyViewCtrl : UIViewController {
...
let lblMsg : UILabel = createLabelF(...) // UILabelの生成
...
// 処理成功メッセージを表示
zBase.setMsgWTimerF(lblMsg, "登録されました。", .OK, 4.0)
...
// エラーメッセージを表示
zBase.setMsgWTimerF(lblMsg, "名前を入力してください。", .Error, 9.0)
...
// 内部エラーのメッセージを表示
zBase.setMsgWTimerF(lblMsg, "内部エラー:VAZ31:登録失敗", .InternalError, 36000.0)
...
}
見てのとおりですが、簡単に説明します。
まず、アプリの初期処理で、今回のクラスのインスタンスを生成します。現状ではメッセージ用の関数しかありませんが、そのうち別な共通処理を追加するつもりでいます。アプリに不可欠な機能をまとめたクラスなので、アプリの初期処理で生成するというわけです。アプリ内の共通処理という位置付けになり、インスタンスを入れた変数zBaseを経由して、用意した関数にアクセスします。インスタンスを生成していますが、経由するための土台の役割しか持っていません。その意味で「Base」という名前をつけました。前に「z」が付くのは私のルールで、アプリ共通の機能を使うときの接頭語のような役割です。
この例では、できるだけ見やすくする意味で、メッセージの内容を凝っていません。実際のアプリでは、登録件数を付けて表示するなど、メッセージの文字列を生成する処理が、関数を呼び出す前に付きます。
この例から分かるように、1行だけの呼び出しで、消去までの秒数や、メッセージの色、使用するUILabelが指定できています。
ちなみに、メッセージを消さない機能を付けていませんが、この内部エラーの例のように極端な秒数を指定すれば、消さないのと同じ効果が得られます。この例では36000秒(=10時間)を指定しています。これでも不安なら、もっと大きな値も指定できます。
当然ですが、長く表示するメッセージでも、何かのメッセージで上書きされると消えてしまいます。36000秒後に実行されるクリアー処理も、メッセージのidが異なるため、何もせずに終了するでしょう。
上記の使用例では、メッセージを消すまでの秒数を、直接の値で指定しています。数多くのメッセージを出すアプリでは、ある程度まで秒数を標準化したほうが良いと思います。次のようなSwiftコードで定義し、参照するとよいでしょう。
// メッセージを消すまでの秒数を定義(こちらはアプリ内で定義)
let TIME_I_MSG_NORMAL : NSTimeInterval = 4.0
let TIME_I_MSG_OK_S : NSTimeInterval = 4.0
let TIME_I_MSG_OK_L : NSTimeInterval = 6.0
let TIME_I_MSG_CAU_S : NSTimeInterval = 6.0
let TIME_I_MSG_CAU_L : NSTimeInterval = 10.0
let TIME_I_MSG_ERR_S : NSTimeInterval = 6.0
let TIME_I_MSG_ERR_M : NSTimeInterval = 9.0
let TIME_I_MSG_ERR_L : NSTimeInterval = 12.0
let TIME_I_MSG_LL : NSTimeInterval = 36000.0 // 10時間
let TIME_I_MSG_LLL : NSTimeInterval = 86400.0 // 24時間
この定義ですが、一応は目的別になっています。ただし、目的別に1つずつではありません。目的別に分けつつ、それぞれで短めと長めを用意しました。さらにエラーでは、3種類の長さを用意しています。このようなセットだと、目的別の分け方を取りつつ、柔軟に対応できると考えました。
定義の中で、最後の2つは特殊用途です。重大なエラーが出たとき、メッセージが消えないようにする目的で使うものです。どれぐらいの時間が適切なのか不明なため、とりあえず2種類を付けました。本当に適した数値は、使ってみてというか、アプリが本番で使われたときに判明すると思います。使われるためには、本番での重大なエラーの発生が必要で、その機会が来るかどうかは不明ですが。
上記の定義を使った場合、前述のSwiftコードは次のように変わります。
// 上記の秒数定義を使った例
...
// 処理成功メッセージを表示
zBase.setMsgWTimerF(lblMsg, "登録されました。", .OK, TIME_I_MSG_OK_S)
...
// エラーメッセージを表示
zBase.setMsgWTimerF(lblMsg, "名前を入力してください。", .Error, TIME_I_MSG_ERR_M)
...
// 内部エラーのメッセージを表示
zBase.setMsgWTimerF(lblMsg, "内部エラー:VAZ31:登録失敗", .InternalError, TIME_I_MSG_LL)
...
このように秒数定義を用意して参照すれば、同じ役割の秒数を変更するとき、一括でできて便利です。
今回のメッセージ用関数の改良で、メッセージ処理がライブラリ化できました。今後のアプリでは、この関数を多いに利用するでしょう。それによって、この関数ではカバーできないメッセージ処理が見えてきて、新たな関数の追加も視野に入ると思います。
使用頻度の高い処理をライブラリ化すると、その次にライブラリ化すべき対象が見えてくるという、好循環が期待できそうです。
(使用開発ツール:Xcode 6.0.1, SDK iOS 8.0)
0 件のコメント:
コメントを投稿