2015年3月31日火曜日

エラーメッセージ補助機能のライブラリ化

エラーメッセージ出力機能を独自ライブラリに加えたので、エラーメッセージの内容を改めて考えてみました。今まで作ったときに考えていたのは、エラーを知らせることだけです。今後は、時間が経過してから見た場合も考慮して、もっと良い内容でメッセージが出せないのか、考え直してみたというわけです。

考えた結果、エラーメッセージの補助機能を独自ライブラリに加えることにしました。エラーメッセージを後から見るときだけでなく、開発時のデバッグ時にも使える補助機能が作れました。単純な機能ですが、ここで紹介します。

 

先日作成した、エラーメッセージ出力機能のライブラリ化では、メッセージの頭に日付と時刻を加えます。これで、発生した時期が特定できるようになりました。

また、どのアプリが出しているのかの情報は、必要ありません。そのアプリのDocumentsフォルダへ保存するのですから、どのアプリかは明らかです。

 

エラーメッセージに、上記以外の情報は何か必要でしょうか。

そう考えて思い浮かんだのが、過去に作ったオンラインシステムでのトラブル対処です。大量のデータを扱うシステムだったので、エラーが発生したとき、どのデータで発生しているのか、なかなか特定できませんでした。特定するために、エラーメッセージを改良して、どのデータで発生したのか出力するようにしました。その結果、エラーの発生したデータが特定でき、システムの改良が容易になりました。

時間が経過してから見る、iPadアプリのエラーメッセージにも、同じような要望があると思います。どのような状況で発生したのか、どのデータで発生したのか、エラーの種類によっては詳しく知りたいはずです。その要望を満たすような機能があれば、非常に有益だと言えるでしょう。

 

そこで、状況やデータを示せるようなエラーメッセージ補助機能を、独自ライブラリに追加することにしました。状況やデータの値に関係なく、メッセージへ情報を簡単に加えられる機能です。どの情報を加えるかは、その補助機能を使う側が決めます。

基本として、メッセージに追加したい情報を設定する機能と、設定した情報をエラーメッセージに加える機能が必要です。これも、エラーメッセージ出力機能と同じように、アプリのどこからでも呼び出しやすいことが大事です。そのため、グローバル関数として作ることにしました。作る場所は、前回のエラーメッセージ出力機能と同じ「BP_Base.swift」です。

メッセージへ入れる情報に合わせて、データ構造を考える必要があります。今回は、何層かの階層として扱えたほうが良いと考えました。たとえば、1番目の階層には、画面(View)の名前を入れます。次の階層には、画面に含まれる処理名を入れます。さらに次の階層には、キーとなるデータの値を入れます。同様に、データの値を4階層ほど用意します。

4階層も付けたのは、4つの階層が必要になるという意味よりも、同じ階層で2つ以上の値を使うかもしれないと考えたからです。階層関係があるので使い方は制限されますが、階層の余裕を別な形で使えるようにとの配慮です。

階層で大事なのは、自動クリアー機能です。もし画面が変わったら、それ以下の階層に設定された値は意味がなくなります。そのため、第1階層の値が変更されたら、第2階層以下の値は自動クリアーします。同様に、第2階層の値が変更されたら、第3階層以下の値を自動クリアーします。このように、全ての階層で、下の階層を自動クリアーする機能を組み込みます。

 

完全な階層構造の値だけだと、より自由な使い方はできません。そこで、階層関係とは別の値を入れる場所も2つ用意しました。

ただし、こちらも自動クリアー機能がないと、使いやすくはなりません。そこで、1つの値は、第1階層の設定でクリアーさせます。もう1つの値は、第1階層および第2階層の設定でクリアーさせます。第2階層だけのクリアーでない理由は、すべての設定をクリアーする機能が必要で、それが第1階層の役目だと考えたからです。

 

こうして作ったのが、以下のグローバル関数です。実際のSwiftコードは次のとおりです。

// エラーメッセージの補助機能:画面やデータを特定するために、処理中の画面やデータを記録に残す
private var procKeyL1: String = ""  // 通常はView名を設定(V=XXX)
private var procKeyL2: String = ""  // 通常は処理の種類(P=XXX)
private var procKeyL3: String = ""  // 通常は処理データのレベル1
private var procKeyL4: String = ""  // 通常は処理データのレベル2
private var procKeyL5: String = ""  // 通常は処理データのレベル3
private var procKeyL6: String = ""  // 通常は処理データのレベル4
private var procKeyA1: String = ""  // 通常は処理データの属性(L1設定でクリアー)
private var procKeyA2: String = ""  // 通常は処理データの属性(L1設定とL2設定でクリアー)

// 処理中のキーを設定する
func zSetProcKeyL1F(rStr:String) {
    procKeyL1 = rStr
    zSetProcKeyL2F("")
    zSetProcKeyA1F("")
    zSetProcKeyA2F("")
}
func zSetProcKeyL2F(rStr:String) {
    procKeyL2 = rStr
    zSetProcKeyL3F("")
    zSetProcKeyA2F("")
}
func zSetProcKeyL3F(rStr:String) {
    procKeyL3 = rStr
    zSetProcKeyL4F("")
}
func zSetProcKeyL4F(rStr:String) {
    procKeyL4 = rStr
    zSetProcKeyL5F("")
}
func zSetProcKeyL5F(rStr:String) {
    procKeyL5 = rStr
    zSetProcKeyL6F("")
}
func zSetProcKeyL6F(rStr:String) {
    procKeyL6 = rStr
}
func zSetProcKeyA1F(rStr:String) {
    procKeyA1 = rStr
}
func zSetProcKeyA2F(rStr:String) {
    procKeyA2 = rStr
}

// 処理中のキーを得る
func zGetProcKeyF() -> String {
    let STR_DELIMIT: String = ","
    var iStr: String = procKeyL1
    if procKeyL2 != "" { iStr += STR_DELIMIT + procKeyL2 }
    if procKeyL3 != "" { iStr += STR_DELIMIT + procKeyL3 }
    if procKeyL4 != "" { iStr += STR_DELIMIT + procKeyL4 }
    if procKeyL5 != "" { iStr += STR_DELIMIT + procKeyL5 }
    if procKeyL6 != "" { iStr += STR_DELIMIT + procKeyL6 }
    if procKeyA1 != "" { iStr += STR_DELIMIT + procKeyA1 }
    if procKeyA2 != "" { iStr += STR_DELIMIT + procKeyA2 }
    if iStr == "" {
        return ""
    } else {
        return "(" + iStr + ")"
    }
}

難しい処理はしてませんので、見てのとおりの機能です。いつのもように要点だけ解説します。

エラーメッセージに表示する値(キー)は、8つの変数に入れます。6階層に作られたL1〜L6と、階層ではないA1とA2の8変数です。それぞれに設定関数が用意され、値を設定すると同時に、関連する自動クリアー機能が含まれています。値のクリアー機能は用意せず、空の文字列を入れることでクリアーと同じの役目をします。

階層構造の値の設定では、下の階層をクリアー(空文字の設定)を順番に呼び出す形で作っています。このように作っておくと、階層を増やすときの変更箇所が少なくて済みます。

階層とは関係ない2つの値(A1とA2)の設定では、クリアーする処理が含まれません。下の階層を持ってないからです。ただし、これらの値を自動クリアーする必要があるため、第1と第2の階層の設定処理で、それぞれの階層に応じたクリアー処理を含めています。

最後は、これらキーの値を文字列として取り出す処理(zGetProcKeyF関数)です。順番に追加するだけですが、追加する値が空でないときだけ、区切り文字を間に入れて追加しています。処理を良く見ると分かるのですが、第1階層が空で、第2階層以下のどれかが空でない場合は、最初に区切り文字が付いてしまいます。これに対処するとソースが汚くなるのと、第1階層を使わない状況は想定してないので、この形にしました。

この補助機能を使わない場合は、8つの変数が全て空です。その場合に、中身の入ってない括弧が表示されると醜いので、最後の処理では、中身が入っているときだけ括弧で囲んでいます。

 

これでエラーメッセージの補助機能は作り終わりました。ただし、このままだと、エラーメッセージに毎回追加する処理が必要です。そこで、前回紹介したエラーメッセージの処理を少し変更して、キーの値が自動的に追加するようにしました。以下のように、たった1行だけの変更です。

// 変更前
let iErrMsg: String = getStrDateNowF() + " " + rMsg
// 変更後
let iErrMsg: String = getStrDateNowF() + " " + rMsg + zGetProcKeyF()

前述のように、補助機能を使ってないときは空の文字列となるので、キー値の追加されない(つまり以前と同じ)エラーメッセージが出力されます。

 

すべて作り終わったので、使い方を簡単に紹介しましょう。

まずは、基本的なルールから。複数の値を一緒に表示するため、それぞれが何の値を表示しているのか理解しやすいように、形式を決めます。画面であればViewを意味するVをキー名として、「V=Menu」という形式で値を入れます。このように、キー名とキー値をペアにして等号でつなげた形を、私の使用ルールとしました。

第1階層のキーには、どの画面を動かしているかの情報を入れます。編集画面の場合は、次のような使い方になります。

// 編集画面に入った直後に
zSetProcKeyL1F("V=Edit")  // キーの設定
// この後に画面生成処理を続ける
...

// 画面を消す処理の最後にも
...
zSetProcKeyL1F("")  // キーのクリアー

この例のように、画面を意味するキー設定だけでなく、キー設定をクリアーする処理も最後に加えています。この形にする理由が2つあります。1つは、画面に関わる処理の範囲を明確に示し、画面外の処理で間違った表示をさせないためです。もう1つは、キー設定を全部の画面に付けるとは限らないので、クリアー処理を付けないと、別な画面で間違ったキーが設定されたままになってしまいます。全ての画面に付ければ、クリアー処理がなくても構わないのですが、他に依存するような使い方は好ましいとは言えません。トラブルの元になります。そのため、設定とクリアーの両方を付けるがベストな使い方と言えます。

このようなキー設定により、エラーメッセージが出されたとき、どの画面で動いているのか明らかになります。複数の画面で使われているような処理では、どの画面で使われたときに発生したエラーなのか分かると、原因の究明に大いに役立つでしょう。

 

続いて、第2階層の処理です。ボタンをタップしたとき呼ばれる関数の例を挙げてみました。

// 追加ボタンをタップしたときの処理
func AddDataF(rSender:UIButton) {
zSetProcKeyL2F("P=AddBtn")  // キーの設定
...
... // ここにタップ時の処理を入れる
...
zSetProcKeyL2F("")  // キーのクリアー
}

こちらでも画面の場合と同じように、キーのクリアーを入れています。また、キーの形式もルールに則り、「P=AddBtn」としています。

処理の途中でエラーメッセージが出されると、画面のキーとともに、処理の種類のキーも一緒に表示されます。どの画面の、どの処理でエラーが発生したのか判明すれば、原因の絞り込みがさらに容易になるでしょう。

 

これ以下の階層でも、基本的な使い方は同じです。処理する値ごとにキーを設定すれば、どの値の処理中にエラーが発生したのか、明らかにできます。たとえば、商品idごとにキーを設定するなら、値の形式を「ProdID=XXXXX」として、「XXXXX」の部分に商品idの値を入れることになります。キーの名前を極端に短くしないことが、後から見て理解しやすいコツでしょう。

原因究明に役立つのは、データのキーとなる値だけではありません。何件目のデータを処理しているのか、全部で何件のデータを処理しているのかといった、処理件数の関わる情報も役立つことがあります。その場合は「cnt=XXXXX」と入れることになります。過去のデバッグ経験と照らし合わせて、ヒントになりそうな情報を入れるのも上手な使い方と言えそうです。

階層とは関係のないA1とA2は、少し違う使い方が可能です。入力画面であれば、入力モードを記録するとか、入力エラーの有無を記録するとか、階層に関係のない独立した値なら何にでも使えます。エラー発生に関係しそうな情報を入れるのがコツでしょう。

 

以上のように使った場合、エラーメッセージを出すグローバル関数zSendErrMsgFの使い方は以前と同じですが、エラーメッセージの最後に、補助機能で設定した情報が括弧付きで加わります。具体的には、次のようになります。

// エラーメッセージを出力するソースコードは、以前と同じまま
zSendErrMsgF("ERROR:ZF_WF:ファイル書き出しが失敗しました。")
// エラーメッセージの出力例
2015/03/31 15:23:47 ERROR:ZF_WF:ファイル書き出しが失敗しました。(V=Edit,P=AddBtn,ProdID=S015037)

zSendErrMsgF関数により、前側に日付と時刻が、後ろ側に補助機能による付加情報が加わってメッセージが出力されます。もし補助機能を使っていなければ、後ろ側の括弧の部分が、まったく表示されません。もし一部の画面だけで使っている場合は、使ってない画面(補助機能が設定されてない画面)でも、後ろ側の括弧の部分が表示されません。あくまで、補助機能が設定されているときだけ、設定された情報を追加します。

 

当然のことですが、エラーメッセージの出る確率は、アプリの信頼性が高いほど少なくなります。作り手が優秀な場合、エラーメッセージはほとんど出ないはずです。

というわけなので、キーを細かく設定しても、無駄な処理を動かしているだけになります。実際には、多量のデータを扱う箇所に入れない限り、処理自体は軽いので、入れておいてもバッテリー消耗に影響がないでしょう。保険のために入れておくという感じでしょうか。

加えて、多少ですが、入れる手間も増えます。現実的には、全部の画面に入れるのではなく、大事な画面を選び、その中の重要な処理にだけ入れるというのが現実的だと思います。その場合、画面名と処理名は必須として、データのキーとなる値を入れるかどうかは悩みます。こちらも、難しい処理をしているときにだけ、念のために入れておくというのが現実的でしょうか。

最初入れてなくて、エラーが発見されたときに、入れたバージョンのアプリと入れ替えて様子を見るという使い方も可能です。臨機応変に活用するのも現実的な使い方でしょう。

 

ここまで、エラーメッセージの補助機能として紹介してきました。しかし、それ以外にも有効な使い方があります。開発中のデバッグです。

まず、この補助機能を、開発当初から入れておきます。主な画面と主な処理で。加えて、集中的にデバッグしたい処理では、データのキーとなる値を設定する処理も追加します。

面白いのは、ここからです。エラーメッセージが出るのを待つのではなく、条件文付きのprintln文を使って、zGetProcKeyF関数を呼び出すのです。すると、条件を満たしたときにメッセージが出て、実行中のキーの値が表示できます。println文を入れた箇所からは参照が困難な値も、この補助機能を使えば表示できるというわけです。

 

今回は、より良いエラーメッセージを考えている中で、思い付いた機能を作ってみました。この補助機能を使えば、発生状況が特定しづらいエラーの状況判明に、かなり役立つと思います。次に作るアプリや、既存アプリの改良時に、組み込んでみようと考えています。どのレベルまで入れるのか悩みながら、上手な使い方を模索してみます。

 

(使用開発ツール:Xcode 6.2, SDK iOS 8.1)

2015年3月24日火曜日

エラーメッセージ出力機能のライブラリ化

当ブログでは、いろいろな機能のライブラリ化を紹介しています。もちろん、ソースコードも公開無しながら。それらのコードの中では、エラー検出機能も含まれていて、検出時にはエラーメッセージを出力しています。通常は、println文を使って。

それでも構わないのですが、println文で出力したメッセージは、コンソールのログを見なければ確認できません。とくに、iPadの実機上で実行中のアプリなら、簡単には見れないのが欠点です。

そこで、エラーメッセージの出力方法を変更しようと考えました。コンソールにも一応出力しますが、その他として、Documentsフォルダ内のファイルにも書き出します。こうすると、以前に「ファイル閲覧機能のライブラリ化」で作った、Documentsフォルダ内のファイル閲覧機能で見ることが可能となります。閲覧機能を新たに作らなくても、エラーメッセージを見れるというわけです。

また、エラーメッセージの出力先を1つにまとめることで、間接参照を実現したことにもなります。println文以外の出力先を追加したり、別な出力先に変更したりが、1カ所の変更だけで可能です。また、エラーメッセージの内容に応じて、出力先を切り替える機能も、一カ所の変更で実現できます。つまり、今回の機能のライブラリ化は、エラーメッセージの出力方法を、間接参照で作ろうという意味でもあります。

 

いつものように最初は、作るべき機能の仕様を検討しましょう。実現方法としては、アプリ内のグローバル関数とします。いろいろなところから呼び出すので、簡単に呼び出せたほうが使いやすいでしょうから。この関数を入れるファイルは、UI部品を生成するグローバル関数が入っている「BP_Base.swift」としました。

大まかな機能は、次のようにします。呼び出し側からメッセージを受け取り、メッセージの先頭に現在の日付と時刻を追加して、コンソールと指定ファイルに出力します。既にファイルがあるか調べて、あればファイルの最後に追加し、なければファイルを新規で作ります。

時刻の文字列生成や、ファイルの読み書きは、すでに独自ライブラリとして作ったものがあります。でも、今回はそれを使いません。エラーメッセージの生成は、アプリの基本となる機能で、他のライブラリに依存するのは良くありません。依存すると、ライブラリの読み込み順序などにも影響され、余計な考慮が必要となります。まったく依存しないようにと、普通にコーディングしました。

 

将来の拡張も、最初から考慮すべき点です。今回の関数内でのエラーメッセージの扱い方が、1種類だけで済むとは限りません。将来的に、ファイルに出力しないメッセージとか、メールで送信するメッセージとか、新たな扱い方のメッセージが追加となる可能性もあります。どう処理するのか、呼び出し側で指定できるような機能の追加も必要になるでしょう。その場合、それまで作った呼び出し側のコードを変更しないように作っておくのがベストです。

最初に考えたのは、エラーメッセージの扱い方を示す番号を、Int型の引数として加える方法でした。最初はゼロを指定しておき、別な扱い方のメッセージが出てきたら、ゼロ以外の番号を指定するわけです。最初に作った呼び出し側は、全部がゼロを指定していますから、後から変更する必要はありません。変更が予想される機能の実現では、よく使われる方法でしょう。

でも、まったく変更しないときは、余計な引数が付いたままとなり、美しくありません。今回は、エラーメッセージが引数なので、文字列のデータ型です。ということは、いろいろな情報が含められるということです。もし新しい扱い方のメッセージを追加するときには、エラーメッセージの先頭に特殊な文字を入れることで対応可能です。たとえば、メールで送るエラーメッセージなら、メッセージの先頭を「@」にするとか。つまり、先頭の1文字か2文字を、コマンドとして利用するわけです。文字列型ならではの拡張方法です。

文字列の先頭にコマンドを入れる方法は、いろいろな応用が可能です。たとえば、先頭が「@」のとき、メールアドレスを続けるような仕様にして、メールアドレスの終了特殊文字も規定します。先頭「@」の次から、その終了特殊文字の前までが、メールアドレスの文字列として解釈され、終了特殊文字の後ろがエラーメッセ時として扱われるという仕様にも作れます。このように、引数付きのコマンドも実現可能なのです。まあ、実際に作るかは別な話ですけど。ともかく、何とかなる余地が残っていることが大切です。

 

いろいろと書きましたが、機能としては単純です。短時間で作れました。具体的には、次のようなSwiftコードになっています。

// エラーメッセージを出力するためのグローバル関数

let STR_FNAME_ERR = "AppErrMsg.txt"    // エラーメッセージのファイル名

func zSendErrMsgF(rMsg:String) {
    let iErrMsg: String = getStrDateNowF() + " " + rMsg
    println(iErrMsg)
    // エラーメッセージファイルへ出力(存在してなければ新規作成)
    var iStrWrite: String = ""    // 書き出す文字列
    let iPathDocs = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
    let iPathFile: String = iPathDocs.stringByAppendingPathComponent(STR_FNAME_ERR)
    let iFileManager: NSFileManager = NSFileManager.defaultManager()
    if iFileManager.fileExistsAtPath(iPathFile) {    // ファイルが存在したら、その内容を読み込む
        let iStrOld = NSString(contentsOfFile:iPathFile, encoding:NSUTF8StringEncoding, error:nil) as String
        iStrWrite = iStrOld
    }
    iStrWrite += iErrMsg + "\n"
    iStrWrite.writeToFile(iPathFile, atomically:true, encoding:NSUTF8StringEncoding, error:nil)
}
// 現在の日付時刻の文字列を返す
private func getStrDateNowF() -> String {
    let iDateFmtr = NSDateFormatter()
    iDateFmtr.locale = NSLocale(localeIdentifier:"ja_JP")
    iDateFmtr.dateFormat = "yyyy/MM/dd HH:mm:ss"
    return iDateFmtr.stringFromDate(NSDate())
}

見てすぐに分かるコードですが、簡単に説明します。

ファイルが存在するかは、きちんと調べています。読み込みが失敗したら存在しないという判断する方法は使っていません。存在したら読み込んで、新しいメッセージを追加します。メッセージごとに改行したいので、最後に改行文字を追加しています。

ファイルの読み書きでは、エラーチェックをしていません。エラーを発見したとしても、おそらく何もできないからです。エラーメッセージが消えるだけですし、普通の状態ではエラーが発生しないでしょうから、何もしなくて構わないと判断しました。

日付時刻の文字列を生成するgetStrDateNowF関数では、日付と時刻の桁が縦に揃うように、前ゼロ付きの書式を選びました。また、時刻の後ろに空白文字を入れるのですが、この関数内では入れずに、日付時刻を追加する行でわざわざ入れています。これには理由があります。エラーメッセージ出力の関数が拡張されるとき、日付時刻の文字列がどのように使われるのか不明です。文字列の最後に余計な空白付きで返ってくるのは、その空白が不要になったときに、この関数の変更が生じるでしょう。最後に空白を入れるのは、この関数の戻り値を使う側の都合であるため、使う側で付けるのが当然の作り方です。とても小さなことですが、このように考えて作ると、将来の余計な手間を増やさずに済みます。

 

関数名に関しても、少しだけ解説します。私の場合は、UI部品の生成などをグローバル関数として用意しています。ただし今までは、関数名を普通に付けていました。でも、グローバル関数と分かるような関数名を付けたほうが、後でメンテナンスするときに良いと思うようになりました。

そこで今回から、グローバル関数の名前は、先頭に「z」を付けることにしました。なぜ「g」ではなく「z」かという理由ですが、まず「g」はメソッド名の先頭文字に多く使われますから避けたいと考えました。逆に「z」は使われません。また、日付やファイル処理の機能をクラスとして用意し、そのグローバル変数の名前として、先頭に「z」を付けています。それと共通なら分かりやすいと考え、先頭に「z」を付けることとしました。この辺の考え方は、個人差が大きいと思います。

既存のグローバル関数は、今回のルールに従っていません。何かの機会に、同じルールで変更する予定です。

あと、動詞を何にするかで迷いました。とりあえず「Send」にしたのは、メッセージを送るという感じが、自分なりには動きに一番近いと思えたからです。これがベストという感じはしてませんが、とりあえず何か付けないといけないので。

 

使い方は、非常に簡単です。今までprintln文で書いていたものを、zSendErrMsgF関数を呼び出す形に変更するだけで済みます。言ってみれば、「println」の文字列を「zSendErrMsgF」の文字列で置き換えるだけです。次の例のように。

// エラーメッセージ出力の使用例
zSendErrMsgF("ERROR:GD_PT:商品テーブルの読み込みに失敗しました。")

極端な話、ソースコードの文字列検索&置き換え機能を使い、すべての「println」文字列を「zSendErrMsgF」で一括置き換えしても構わないぐらいです。まあ、実際には行ないませんけど。もし置き換え機能を使うとしたら、1つ1つ確認しながらですね。

 

今回の機能を作ったことで、今後のエラーメッセージ処理は、これを常に使うことになりそうです。次回以降の新作アプリとアプリ更新では、すべてのエラーメッセージをファイルへも出力するでしょう。

Documentsフォルダ内のファイルを削除しない限り、エラーメッセージは消えずに追加され続けますから、時間が経過した過去のエラーも後から見れます。これが一番大事な点で、エラーを見逃さない仕組みが用意できたとも言えるでしょう。これが、作った一番の理由かもしれません。

また何か思い付いたら、自分の独自ライブラリに機能を追加したいと思います。

 

(使用開発ツール:Xcode 6.2, SDK iOS 8.1)

2015年3月19日木曜日

起こりうる失敗(理不尽な更新)でXcode 6.2に入れ替えられた

他のOSと同じようにOSXも、アプリのバージョンアップやセキュリティ更新によるアップデートを、手助けする仕組みが備わっています。OSXのバージョンが進むごとに、アップデートの手間が減るように機能アップしてきました。今では、自動更新を選択することもできます。

Xcodeの最新版がMac App Storeに登録されるようになってからは、OSXのアップデートの対象にXcodeも含まれるようになりました。これって、本当に困りものなのです。私のように、少し枯れたバージョンを使う人にとっては。

 

で、また失敗してXcodeを更新してしまいました。気付いたときには更新が始まっていて、もう停止できませんでした。Xcode 6.1.1が6.2にバージョンアップされたというわけです。これが確か2回目の失敗です。

OSXのソフトウェア・アップデートは、画面の右上に知らせが表示されます。それを一旦停止させて、App Storeアプリのアップデート画面を呼び出し、更新内容を確認してからアップデートするのが、いつもの手順です。この操作方法なら、Xcodeなどの更新したくないソフトをアップデートせずに済ませられます。

でも、この方法は非常に危険です。何回も繰り返すうちに、いつか失敗して、更新したくないアプリを更新してしまうのです。まさに「起こりうる失敗」なのです。

今回は、忙しく作業している中でメッセージが表示され、間違って、更新するほうをクリックしてしまいました。操作ミスに気付いたのは、更新処理が行なわれている途中です。終わるまで待とうと思って眺めていると、いつもよりも更新時間が長めで終了しません。その時点で、Xcodeも更新されていることに気付きました。あーあ、やってしまったと。

 

そもそもXcodeが、OSXのアップデートに含まれていること自体が間違いです。Xcodeを更新するタイミングは、ユーザー自身が自分で決めるのが当たり前です。もし開発途中でXcodeが更新されてしまったら困るでしょう。今まで正常に動いていたコードが、突然と動かなくなる可能性もありますから。通常は、開発ツールのバージョンを決め、特別な理由がない限り、そのまま開発終了まで固定するのが普通です。もしかしてアップル社内では、Xcodeは最新版を必ず使えという、誰かの強い意見でもあるのでしょうか。まさに「理不尽な更新」と言えるでしょう。

人間はときどき失敗しますから、「起こりうる失敗」を含んだ更新方法なんて、数多く繰り返さなくても失敗が発生します。私も、すでに2回目も失敗しました。今後も、同じように失敗するでしょう。毎回注意する方法では、いつか失敗してしまいます。

何か防止する方法が必要でしょう。App Storeアプリのアップデート画面で、購入済みアプリの一覧が見られます。その画面に、特定のアプリだけ更新しない設定があれば良いのに。本当に困りものです。

 

最近のXcodeは、アプリの形をしたXcodeの中に、各種ツールを含んでいます。この構造に変更されたおかげで、Xcodeの複数バージョンを一緒に入れておけるようになりました。アプリケーションフォルダ内で、Xcodeアプリの名前を片方だけ変えて、共存させるだけで実現可能です。

でも、OSXのソフトウェア・アップデートでXcodeが更新されると、複数のXcodeは全部消えてしまい、最新のXcodeだけ残る状態になります。これも非常に困りものですね。

もしかして「アプリケーション」フォルダ以外へXcodeをインストールしておいたら、ソフトウェア・アップデートで消えないのでしょうか。でも、それで正常に動くかどうかのほうが心配です。一部の機能が動かないとき「もしかして、アプリケーションのフォルダに入れてないことが原因かも」と余計な心配が生じます。こんな不安を抱え込む方法なんて、やりたくないでしょう。

 

今はXcodeを使った急ぎの開発がないので、大きな問題は発生しません。せっかくなので、Xcode 6.2を使ってみましょうかね。最新バージョンを試すための試験マシンには、6.3ベータが入っているので、6.2を一緒に入れるのは面倒ですから。

 

とにかく、OSXのソフトウェア・アップデートでXcodeが更新されないようにしてほしいです。何か良い方法はないかと、いちおう検索しましたが、見付けられませんでした。もし回避方法を知っている人が居たら、ぜひとも教えてください。お願いします。

2015年3月17日火曜日

Optional型の使い方(改めて考えてみた)

Swiftの特徴の1つに、Optional型があります。どのような役割なのか、どのように動作するのかは、多くの詳しい人がブログなどで書いていますから、ここで改めて書く必要はないでしょう。

でも、どのように使うのが良いのかは、あまり深く考察されてないように感じています。それなのに、いろいろと悩む点が多いのではないでしょうか。私自身も、まだ悩み続けています。Swiftを最初に使い始めた頃に比べると、使い方が少しずつ変化してきてます。Swiftにも少しずつ慣れてきたところなので、Optional型の使い方を改めて考えてみました。じっくりと改めて考えることで、使い方の整理ができて、今後のプログラミングに役立つのではないかと思うからです。

なお、Optional型のような機能の使い方は、各人のプログラミング方針に大きく影響を受けやすいものでもあります。このような考え方もあるのかという意識で読んでください。ちなみに私は、Swiftの変数宣言で、データ型の指定を積極的に付ける方針でプログラミングするタイプ(コンパイラのチェック機能を積極的に利用して、実行前に間違いを見付ける派)であり、またInterface BuilderやStoryboardを使わない派でもあり、世間的には少数派だと認識しています。

 

正直なところ、最初のうちは深く考えずにOptional型を使っていました。APIでOptional型が返る場合は、そのままOptional型で受けたり、アンパックしてOptionalを外したり、その場その場で決めていました。明確な判断基準は持たず、何となく決めていたという感じです。

自分が作る関数でも、戻り値にnilが入る場合には、深く考えずにOptional型を使っていました。あまり疑問は持たず、そうするのが当たり前だと思っていました。

新しい言語やAPIを使い始めた当初は、細かい部分にまで気を回す余裕がないので、当然と言えば当然のことでしょう。

 

では、Swiftに慣れてきた現時点で改めて考えると、どのような使用基準が良いのでしょうか。一番大事な基準は、やっぱり「Optional型を、できるだけ使わない」でしょう。使い方の話なのに、いきなり「使わない」という基準が登場します。でも、それが極めて大事なことですから、やはり一番気にしなければなりません。

Optional型の変数では、指定された型の値ではなく、nilが入っている可能性があります。つまり、nilが入っている場合も常に考慮しながらプログラミングする必要が生じるのです。nilが入っていないか検査するとか、必要に応じて余計な処理を作らなければなりません。

とくに大変なのが、時間が経過してからメンテする場合でしょう。全体の構造から細かな機能まで、何もかも忘れてしまっています。もし一部の機能だけ修正が必要になったとき、そこにOptional型の変数が含まれていたら、nilの検査を入れるべきか判断するために、関係するソースコードを広く見なければなりません。もし面倒に感じたなら、nilの検査を一応入れておき、nilならエラーメッセージを出すといった、小手先の対処方法を考えるかもしれません。

ですから、Optional型の変数を使うのは、nilが入っている可能性がある狭い範囲に限定し、途中でnilが入ってないと判断できたら、Optional型の変数を使うのを止めて、Optional型でない変数に切り替えるのがベストな方法だと思います。

 

Optional型を使わないようにしようと思っても、使わなければならない場面はいくつもあります。その場面ごとに、できるだけ使わない方法を考えてみました。

 

Optional型が必須となる状況として真っ先に挙げられるのは、APIから受け取る値です。種類が多いのも特徴で、インスタンス生成が失敗したとき、nilを返すような仕様になっています。こうした値にOptional型が指定されていれば、Optional型の変数を使わなければなりません。

ただしSwiftには、このような状況を上手に記述できるようにと、if let文が用意されています。Optional型の変数で受け取るのではなく、Optional型でない変数で受け取るように記述できます。また、nilを受け取ったときの処理(非正常時の処理)も、一緒に記述できます。

// APIから得た値を、 Optional型でない変数で受け取る
...
// Optional型の値を受け取る
if let iNSDic: NSDictionary = NSDictionary(contentsOfFile:iPathFile) {
    // 正常時の処理
    ...
} else {
    // 非正常時の処理
    println("ERROR:ReadPlist")
    return
}
...

このような作り方すれば、Optional型の変数を使わなくて済みます。

逆に、if let文を使えない状況のときは、いったんOptional型の変数で受け取って、必要なところだけOptional型の変数を使うような作り方になります。その場合も、Optional型の変数をできるだけ使わないように作ります。

 

次は、UI部品などを入れる変数です。複数の箇所から参照されるUI部品は、変数名を付けて宣言します。最初の状態では空で、初期か処理の中でインスタンス生成するという流れになります。

理想的には、変数宣言した箇所でインスタンス生成するのが一番です。しかし、そのように作れないことも多くあります。関連するUI部品の変数宣言は並べておきたいし、初期化処理はまとめて別な場所に入れたいなど、それなりの理由があるからです。結果として、変数宣言とインスタンス生成が離れてしまいがちです。

このような場合には、「?」を付けた通常のOptional型ではなく、「!」を付けた特殊なOptional型(Implicitly Unwrapped optionals)の変数として宣言します。このタイプの変数は、変数の値を参照するとき、明示的にアンラップしなくて使える点です。Optional型でない変数のように扱えます。

// 宣言した後から初期化する変数は、「!」を付けた特殊なOptional型で作る

// 変数の宣言
var txfUserName: UITextField!

// まとめて初期化(インスタンス生成)の中に含める
...
txfUserName = createTxtFldF("12", 16, ALIGN_LEFT, 700, 460, 100, 30)
...

使う際の注意点としては、値を参照する前に、必ず初期化しなければならないことです。初期化する前に参照すると、実行時エラーとしてアプリが異常終了します。でも実際には、理解して使っている限り、初期化しないで参照するなんて起こりません。まれに初期か処理を付ける前に実行してしまったとかはありますが、異常終了したことで気付きます。この種のエラーは、きちんと作っている人にとって、心配してくて構わない部類のエラーと言えます。

 

Optional型の変数をアンラップする方法として、「?」と「!」の2種類が用意されています。違いは、変数の値がnilの場合の動作です。「?」ならnilが返り、「!」なら実行時エラーとなります。

通常の場合は、「!」で構わないと思います。アンラップして使う際には、nilではなく、インスタンスが入っている前提があります。それなのにnilが入っているというのは、プログラムまたはデータに問題がある証拠です。その場で実行時エラーを発生させたほうが、どの変数が悪かったのか明確になって調べやすいでしょう。

逆に「?」でアンラップするのは、nilが入っても構わない特殊な使い方です。該当する場合にだけ、「?」でのアンラップを使うことになります。

 

自分が作る関数やメソッドも、取り上げましょう。戻り値のある関数の場合、指定された型の戻り値を返せない状況が、どうしても発生します。その場合にnilを返して、呼び出し側にエラーだと伝えます。この形が、昔から(C言語などの時代から?)使われている一般的な方法でしょう。こうした考え方があり、その考えに沿ってAPIが作られているからこそ、Optional型が用意されていると言えます。

では、本当に仕方がないのでしょうか。自分が作る関数やメソッドでも、すべて同じ考え方で作って構わないのでしょうか。この点に、私は疑問を持ちました。そして、いろいろな状況を区別して考えました。その結果、本当に仕方がないときにだけnilを返し、それ以外はnil以外を返す形で作っても構わないのではないかと思い始めました。状況の違いを、具体的に示して説明しないと、何を言っているのか分からないと思いますので、いくつかの例を挙げてみます。

 

まず最初は、独自ライブラリの使い方を間違ったケースです。対象となる独自ライブラリのクラスは、UIViewを拡張したクラスで、UIViewの大きさ(幅と高さ)に最低値があります。その最低値よりも小さな値を指定して、インスタンス生成しようとした状況です。普通に考えると、指定値が間違ってますから、インスタンスは生成せず、nilを返す形の作り方になります。当然、戻り値はOptional型となります。

しかし、考え方を少し変えたらどうでしょう。最低値よりも小さな値を指定した場合は、エラーにせず、最低値に置き換えてインスタンス生成する仕様に変えたら。インスタンスが生成できるわけですから、nilを返す必要がなくなります。

ただし、そのままだとエラーに気付きません。目立つエラーメッセージをコンソールに表示して、気付いてもらいます。きっちりと開発したい人(私も該当します)なら、エラーメッセージを見逃さないで気付くはずです。

エラーを知らせるための、別な方法も考えられます。UIViewのように画面に表示するクラスなら、そのUIViewに付けてあるUILabelなどに、エラーメッセージを表示させるのも1つの手です。文字を赤くして目立たせるなども効果的でしょう。これなら、コンソールのメッセージを見なくても気付けます。

この例での考え方は、エラーにせずに救える場合は救ってあげて、nilを返さないというものです。救ってあげられる場合は限られていますから、適用は限定的でしょう。

 

似たようなケースとして、何かの値を文字列に変換する関数を考えます。変換対象のデータにエラーがあるとき、Stringの代わりにnilを返すことで、エラーを知らせるという仕様もアリでしょう。当然、戻り値はOptional型になります。

しかし、nilを返す代わりとして、エラーを意味する短い文字列(たとえば「ERROR!!!」とか)を返すとともに、コンソールにエラーメッセージを出す仕様も考えられます。こうすると、戻り値にOptional型を使わずに済みます。

データ型が文字列でなく数値の場合は、あり得ない値を返すことになります。整数なら-99999、浮動小数点なら-99999.9とかです。使用状況に応じて、あり得ない値を決める必要があるでしょう。

こちらの例での考え方は、該当するデータ型の変な値をとりあえず返し、エラーメッセージで知らせるというものです。当然ながら、最悪の場合はアプリが正常に動いてしまうので、エラーメッセージに気付く必要があります。

ただし、関数を呼び出す側が、正しく変換できたのか知りたい場合は、nilを返す必要があります。呼び出し側でnil検査をして、変換の成否を確認できるようにするためです。当然、戻り値にはOptional型を使うことになります。

 

ここまで2つのケースを挙げましたが、忘れてはならない大事な注意点があります。エラーが発生したとき発見しづらくなるようなら、こうした方法を使わずに、素直にOptional型を使うべきです。

少し具体的に説明します。文字列に変換する関数が、ファイル名に関係する関数だったとしましょう。そうすると、エラーが発生するかどうかは、使っている環境に依存します。念入りにテストしたとしても、予想外のファイル名が来た場合、エラーとして判断される可能性もあります。

このように特殊な実行時に発生すると、コンソールのエラーメッセージを見るという行為はできません。エラーが発生せずに何かの文字列が返り、そのまま実行が続いて、最終的には関係ない別な箇所でエラーが発生したりします。すると、最初のエラーまでたどり着くのが極めて大変になります。このような状況が予想される場合には、素直にOptional型を使ってnilを返すのが一番です。

逆にOptional型を使わないと判断して構わないのは、実行時にエラーに気付く可能性はゼロで、開発中にエラーに気付く状況しかあり得ないときだけです。ライブラリの使い方の間違いとか。それに該当するときだけ、Optional型を使わない作り方をすべきでしょう。

 

もう1つ、別な視点として、実行時エラーだけは避けたいという考え方もあります。表示する値が変になっても、実行時エラーで異常終了しないほうがよいという考えなら、エラーメッセージで知らせて、Optional型を使わない選択肢もアリでしょう。最終的には、作る人の考え方で決まります。

もちろん、この場合はプログラムの作り方も少し変わり、単にOptional型を使わないだけでは済みません。実行時エラーを発生させないのに加えて、エラーを発見しやすくしたり、関係する処理でも異常終了させない配慮が必要です。今回の話題とは別な話なので、この辺にしておきます。

 

それにしても、エラーのときにnilを返すという方法が、根本的に何とかならないのでしょうか。もっと別な形でエラーを返せるような、言語上の仕組みとか、API上の工夫とか、格段にエレガントな方法があっても良さそうに思います。

オブジェクト指向とか入ってきましたけど、根本的な部分では大きく改善されてないような感じですね。プログラミングの技術って、本当に進歩しているのか、疑問に思う部分でもあります。

 

ここまでOptional型の使い方というか、使わずに作る方法を考えてみました。読んで分かるように、これが常にベストという方法などありません。いろいろな視点で悩みながら得られた方法で、特定の視点では優れているものばかりです。最終的には、自分の考えに合った作り方を選ぶしかありません。

これから先も、Optional型に関しては悩みながら作ることになるでしょう。また何か面白い考え方がアイデアが思い付いたら、続編として紹介したいと思います。皆さんも、Optional型の上手な使い方がないか考えてみてください。

 

(使用開発ツール:Xcode 6.1.1, SDK iOS 8.1)

2015年3月4日水曜日

テキストファイル読み込みのライブラリ紹介

先日、「ファイル閲覧機能のライブラリ化(本編1)」で、読者の方から質問がありました。投稿には含まれてない、ファイルを読む処理に関してです。plistファイルにはテキスト形式でないものも含まれていますが、どのように除外しているかという質問内容でした。

その場での回答ですが、テキスト形式でないplistファイルは除外しておらず、その場合だけテキスト形式に変換してから、呼び出し側に返しているという内容を書きました。また、具体的な処理は書ききれないので、新たな投稿として近日中に書きますと約束しました。というわけで、この投稿にて紹介します。

 

ファイル閲覧機能から呼ばれているのは、Documentsフォルダ内のテキストファイルを読み込む汎用的な処理で、ファイル関係の独自ライブラリに含まれています。いろいろな拡張子のファイルを読み込み、ファイル内容をStringとして返します。ファイルの保存場所は決まっていますから、呼び出し側が渡すのは、ファイル名だけです。

ファイル名を受け取った読み込み処理では、Documentsフォルダのパスを得てから、ファイル名を加えたパスを生成します。そのパスを使って、テキストファイルとして読み込み、String変数に入れるのがメインの処理です。ファイルの読み込みには、NSStringクラスのcontentsOfFileを使っています。

具体的な処理内容は、次のようなSwiftコードになります。

// 指定されたテキストファイルを読み込む(Documentsディレクトリ内から)
func readFileDocsF(rFName:String) -> String? {
    // パスの生成
    let iPath = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
    let iPathFile: String = iPath.stringByAppendingPathComponent(rFName)
    // 一部の拡張子を特別に処理
    let iSuffix: String = (rFName as NSString).pathExtension
    if (iSuffix == "plist") {
        let iStr: String? = readPlistDocF(iPathFile)
        return iStr
    }
    // 残りの拡張子は普通に読み込む
    let iData = NSString(contentsOfFile:iPathFile, encoding:NSUTF8StringEncoding, error:nil) as String?
    return iData
}

見て分かるように、一部の拡張子(ここではplistのみ)だけ、別な処理を呼ぶ形になっています。ここに別な拡張子を加えることで、別な拡張子でも特別な処理に迂回することが可能です。

 

続いて、上記のソースコードから呼ばれる、拡張子がplistの場合の読み込み処理です。

plistの拡張子を持つファイルには、3つの形式があるようです。テキスト形式のXMLファイル、バイナリ形式のファイル、NeXT時代のファイルの3つです。このうち、NeXT時代のファイルは今は使われていないので、前の2つの形式だけに対応しました。

具体的な処理内容は、次のSwiftコードになります。

// plistファイルを読んでテキスト化する関数
private func readPlistDocF(rPathFile:String) -> String? {
    // バイナリーとして読み込み、最初の6文字を取り出す
    let iNSData: NSData? = NSData(contentsOfFile:rPathFile, options:.DataReadingMappedIfSafe, error:nil)
    if (iNSData == nil) { println("ERROR:PlistRead:NSData=nil"); return "" }
    let iCnt: Int = 6
    var aryByte: [UInt8] = [UInt8](count:iCnt, repeatedValue:0x0)
    iNSData!.getBytes(&aryByte, length:aryByte.count)
    var iStr: String? = NSString(bytes:aryByte, length:aryByte.count, encoding: NSUTF8StringEncoding) as String?
    // 取り出した6文字を判定して、その結果に適した方法で、ファイル全体をテキスト形式に変換する
    if (iStr == "<?xml ") {
        let iData = NSString(contentsOfFile:rPathFile, encoding:NSUTF8StringEncoding, error:nil) as String?
        return iData
    }
    if (iStr == "bplist") {
        let STR_ADD_HEADER: String = "■ 以下(2行目以降)は、ファイル内容を変換(description)した結果 ■\n"
        if let iNSDic: NSDictionary = NSDictionary(contentsOfFile:rPathFile) {
            return STR_ADD_HEADER + iNSDic.description
        }
        if let iNSArray: NSArray = NSArray(contentsOfFile:rPathFile) {
            return STR_ADD_HEADER + iNSArray.description
        }
        // 上記以外は表示不可に
        return "(plist:ファイルの内容は、表示できません)"
    }
    // どちらでもないときはエラーとして扱う
    println("ERROR:PlistRead:NSData=???")
    return ""
}

同じplist拡張子なのにファイル形式が複数あるので、まずはバイナリ形式として読み込みます。NSDataの形で読み込むと、バイナリ形式で読み込めます。読み込みが成功したら、先頭の6バイトをUTF8コードとして取り出します。

その6バイトの内容で、ファイル形式を判定しています。テキスト形式のXMLなら「<?xml 」に、バイナリ形式なら「bplist」になっているはずです。両方のファイル形式のフォーマットをネットで調べて確認しました。2つの形式のどちらでもない場合は、エラーメッセージを出し、空の文字列を返しています。テキスト形式のXMLと判定できたら、そのままの文字列を返します。

もう片方のバイナリ形式には、注意が必要です。iOSで扱うクラスのインスタンスを、シリアライズして保存されている場合がほとんどで、そのクラスも複数あります。それをどうやってテキスト形式に変換するのか、少し悩みました。

結果として、次のような形の処理にしました。NSDictionaryクラスのインスタンスとして、試しに読み込んでみます。もし該当するインスタンスであれば正常に読み込まれ、そうでなければnilとなります。読み込みが成功したときだけ、descriptionメソッドでテキスト形式に変換して返します。成功しなかった場合は、別なクラスで同様の処理を繰り返します。今のところ、NSDictionaryとNSArrayだけしか用意していません。

どのクラスのplistファイルなのか、もっとスマートな判定方法があれば良いのですが、いろいろ検索しても見付けられませんでした。

 

テキスト形式に変換して返す場合には、作り方というか、表示方法に注意が必要です。変換されたテキストを見る人に、「変換されていること」と「どのような変換をしたのか」を知らせる必要があります。何も知らせないで表示すると、表示されたままのテキスト形式で保存されたファイルだと、勘違いする可能性があるからです。

今回紹介した処理では、最初の1行にメッセージを表示する形にしました。プログラマーでない人にも、プログラマーにも理解できる形を意識したメッセージです。プログラマーでない人なら、よく分からないけど、何か変換した結果が2行目以降に表示されていると伝わります。さらにSwiftプログラマーなら、descriptionメソッドで変換したと分かるでしょう。Swiftを知らないプログラマーや、descriptionを知らないSwiftプログラマーでも、descriptionで検索して調べられるでしょう。

 

紹介した読み込み処理では、特定の拡張子(plist)で特別な読み方をしているものの、それ以外の拡張子をチェックしていません。そのまま読み込んだ場合、読み込むファイルがテキスト形式でないと、異常終了などが発生します。

拡張子を検査しない理由は、どんな拡張子でも読めるようにしておきたいからです。どの拡張子を読み込むべきか判断するのは、この読み込み処理を利用する側であるという考え方だからです。それは同時に、読み込めない拡張子は呼ばないとともに、逆に読み込めるなら未知の拡張子で使っても構わないという考え方でもあります。

今回の読み込み処理を改良するとしたら、plistのように特別な拡張子を選んで、テキスト形式に変換するための加工処理を加えることだけでしょう。加工する処理が複数になった場合は、バイナリ形式で読み込む処理を共通化して、利用しやすくする形になると思います。

 

ここで紹介した例では、テキスト形式以外のファイルも、テキスト形式に変換して表示するという発想が入っています。ライブラリを作る際には、このように利用範囲が広がる形にしておくと、いろいろと使えて便利です。複数のアプリで共通に使えるライブラリだからこそ、工夫したときの効果が大きいですから、自由な発想でライブラリの機能向上を目指したいものです。

 

(使用開発ツール:Xcode 6.1.1, SDK iOS 8.1)