2014年11月8日土曜日

Roltoへの印刷機能をswiftで書く(本編1)

設計方針も決まりましたので、いよいよコードを書いていきます。まずは、RoltoのSDKを使って、プリンターと直接やり取りするクラスから。クラス名は「RoltoController」としました。

// Roltoを制御するクラス(後述するコードが入る)
class RoltoController {

}

この中に、前回決めた機能を順番に追加していきます。

 

最初の機能は、プリンターの検索と登録です。RoltoのSDKには2つのクラスがあり、検索に利用するのはRoltoPrintDiscoverクラスです。そのsearchPrintersWithCallbackメソッドを使うわけですが、コールバック機能も含まれているため、少し作りにくくなっています。

// searchPrintersWithCallbackメソッドの使用例(RoltoControllerクラスに入らない、別のサンプル)
roltoDiscover.searchPrintersWithCallback({(rRoltoPrint) in
        self.aryRoltoPrint.append(rRoltoPrint)
    }, comletion:{() in
        self.doFinalProcF()
    }, duration: rSerchSec)

少し詳しく説明しましょう。メソッドを呼ぶと同時に、二つの実行ブロックを指定します。1つは、Roltoが見付かった度に呼ばれるブロックで、RoltoPrintクラスのインスタンスが、変数を介して渡されます。それを配列の中に追加していくのが、一般的な使い方です。もう1つは、検索が終わったときに呼ばれるブロックで、検索終了時の後処理を担当します。

このような形になっているのは、なぜでしょうか。それは時間がかかる処理だからです。ずっと待ちながらCPUを回すわけにもいきません。何か見付かったときと、終わったときの処理を記述することで、CPUを無駄に使わずに、必要なタイミングで処理が開始できるという仕組みなのです。

これ単体で考えると、良くできた仕組みなのですが、プリンター自体の機能だけをクラスとして作り、外から呼び出す場合には、非常に使いにくいことになります。プリンターのクラスは、利用する処理から呼ばれる側です。その呼ばれる側が、処理の始まるタイミングを握っているわけですから、立場が逆になってます。利用する側が関数を渡すという手も考えられますけど、構造が複雑になりがちで、プログラムとしては美しくないです(私は、単純な構造のプログラムを美しいと考えます。だからデリゲートも嫌いです)。

 

では、どうすれば良いのでしょう。実は、非常に良い方法があります。タイマー起動によって、定期的に問い合わせればいいのです。相手が1つだけのポーリングみたいなものです。1秒ぐらいの間隔で問い合わせれば、問題になるほどの遅延は発生しません。

具体的に紹介しましょう。まずは、次のような文字列定数を用意します。この問い合わせは、プリンターの検索以外でも使うので、プリンターの共通処理という意味を込めて「PRINTER_PROC」としました。それぞれの役割は、コメントにあるとおりです。この文字列のほかに、これを入れる変数も用意します。

// 問い合わせ結果を知らせる文字列定数
let PRINTER_PROC_GO : String = "GO"  // 処理を開始した
let PRINTER_PROC_OK : String = "OK"  // 処理が終了し、結果は成功
let PRINTER_PROC_NG : String = "NG"  // 処理が終了し、結果は失敗
// 呼び出す側でも使うので、クラスの外に出しておく
// (もしプリンターのスーパークラスがあるなら、そちらに入れる)

検索などの処理を開始する前に、変数に「GO」を入れます。それから何秒か経過して、終わったときに呼ばれるブロック(前述の2番目のブロック)では、処理の成否を判定して、成功なら「OK」を、失敗なら「NG」を変数に入れます。

この機能を呼び出す側では、まず最初に検索などのメソッドを普通に呼びます。その後、1秒ごとにタイマーで起動し、用意した変数の値を問い合わせます。「GO」が返ってきたら、また1秒後に問い合わせするようにセットします。「GO」でない場合は、「OK」か「NG」かを判定して処理するとともに、1秒後の問い合わせは行ないません。非常に簡単で、スッキリした処理です。

 

ただし、これだけだと何が起こったのか分かりません。「OK」でも「NG」でも、その中身が知りたいはずです。何が起こったかは、プリンター側(プログラム)が知っています。ただ、知っていたとしても、特別に何かできるわけではありません。起こった内容または原因を、呼び出した側に知らせることだけです。さらに、呼び出した側(プログラム)も、何かできるわけではありません。何かできるのは、アプリを操作している人間だけです。ですから、人間にわかる言葉で、処理結果やエラー原因を表示するしかないのです。

表示するのは、プリンターのクラスを呼び出した側ですから、プリンターのクラスは、処理結果やエラー原因を人間の言葉で(つまり文字列で)返すのが、現実的にできることとなります。

文字列を返すのも、処理結果を知らせる方法と同じにします。プリンター側のクラスでは、処理結果を示す変数をセットするのに続いて、人間に伝えるためのメッセージも、別に用意した変数に入れます。その文字列を、呼び出した側で受け取れば良いのです。呼び出す側のルールとしては、「OK」または「NG」が返ったら、決まった文字列を受け取るように問い合わせるだけです。「OK」または「NG」のどちらかしか返らないわけですから、メッセージを入れる変数は1だけ用意し、どちらの場合でも使う方法で構いません。

 

ここまでで、細かな部分まで仕様が固まりました。さっそくswiftのコードを作ります。まずは、処理結果とメッセージを入れる変数です。

// クラス内の変数
var strProcStatus : String = "" // 関数を呼び出す側に、処理の進行状態を伝える
var strMsgResult : String = ""  // 処理結果を示すメッセージ

続いて、プリンターを検索するメソッドのコードです。クラス内の最初のコードも含めました。

// Rolto SDKでの主要なクラス
var roltoP : RoltoPrint!
var roltoDiscover : RoltoPrintDiscover!
init() {    // クラスの初期化
    roltoP = nil
    roltoDiscover = RoltoPrintDiscover()
    strMsgResult = ""
}
// 見付かったRoltoPrintを入れる配列
var aryRoltoPrint : [RoltoPrint] = []
// プリンターの検出メソッド(検出秒数を指定して、使う側から呼ばれる)
func catchPrinterF(rSerchSec:NSInteger) {
    strProcStatus = PRINTER_PROC_GO    // 処理開始に設定
    roltoDiscover.searchPrintersWithCallback({(rRoltoPrint) in
            self.aryRoltoPrint.append(rRoltoPrint)
        }, comletion:{() in
            self.setResultMsgF()
        }, duration: rSerchSec)
    return
}
// 検索結果の判定関数(検索が終了したときに呼ばれる)
func setResultMsgF() {
    if (aryRoltoPrint.count == 0) {
        strMsgResult = "Roltoの接続に失敗しました。"
        strProcStatus = PRINTER_PROC_NG // 処理失敗に設定
        return
    }
    roltoP = aryRoltoPrint[0]
    let strPName : String = roltoP.printerName
    strMsgResult = "Rolto「\(strPName)」に接続しました。"
    strProcStatus = PRINTER_PROC_OK    // 処理成功に設定
}

終了処理のブロックは1つの関数にまとめ、外に出しています。コールバックを含むメソッドの部分は、どうしても見づらくなりがちです。それを防ぐ意味でも、全部を外に出して記述するように心掛けています。単純に見えるソースコードが好きなのです。

残りは、処理結果のメッセージを返す関数です。

// 処理結果を返すメソッド(使う側から呼ばれる)
func getMsgResultF(intFormat:Int) -> String {
    switch intFormat {
    case 0: return strMsgResult
    case 1: return "Rolto:" + strMsgResult
    default: return strMsgResult
    }
}

普通なら文字列を返すだけなのですが、見てのとおり、ちょっと小細工してあります。一応、フォーマット番号という意味の引数を付けました。通常は「0」で呼び出し、メッセージだけを返します。「1」を指定すれば、先頭にプリンター名が付いたメッセージを返します。今のところ「0」しか使っていません。

わざわざ番号を入れといたのは、将来の拡張を考えてのことです。メッセージの返し方を複数使い分ける状況が生じたとき、呼び出し側を変更せず、この関数だけ変更して済ませるためです。手間がかからないから配慮しておきました、という感じでしょうか。複数のプリンターで使う共通のインターフェースだから、拡張したときの手間減らしを強めに配慮した、という面もあります。

プリンターを検出した部分のコードを見て分かるように、最初に見付かったRoltoPrintを入れています。1つしか見付からなかった場合は、このままで構わないのですが、複数ある場合は選択してもらう必要があります。そのためのメソッドを2つ追加しました。

// Roltoの名前一覧を返す
func getPrinterListF() -> [String] {
    var aryPName : [String] = []
    for iRoltoPrint in aryRoltoPrint {
        aryPName.append(iRoltoPrint.printerName)
    }
    return aryPName
}
// 一覧の中から、特定のRoltoに設定
func selectPrinterF(rNum:Int) {
    if rNum < 0 {    // アプリ内部エラー
        println("ERROR:selectPrinterF:1")
        return
    }
    if rNum < aryRoltoPrint.count {
        roltoP = aryRoltoPrint[rNum]
        return
    }
    // アプリ内部エラー
    println("ERROR:selectPrinterF:2")
 }

この2つのメソッドですが、Roltoを1台しか持っていないので、2台以上ではテストできていません。^_^;

 

続いて、呼び出す側のswiftコードも紹介しましょう。主な機能を関数として用意し、必要な箇所から呼び出せるように作ってあります。

// 呼び出すクラスの変数
var printerCtrl : RoltoController! // プリンターのコントローラー
var intRecallCount : Int = 0       // 繰り返しカウント(回数をセットし、ゼロになるまで繰り返す)
   ...
printerCtrl = RoltoController()    // 適したタイミングでインスタンス生成
   ...
// プリンターを探して、見付かれば登録する関数(使うときは、これを1回だけ呼び出す)
var flagUsePrinter : Bool = false  // プリンター関係の処理中ならtrue
func findPrinterF() {
    if flagUsePrinter { return } // プリンター関係の処理中なら中止
    flagUsePrinter = true
    setMsgPrintF("プリンターを検索中です。")
    let iSec : Int = 5
    printerCtrl.catchPrinterF(iSec)
    intRecallCount = 12
    runIntervalF(1.0, self, "getProcResultF")
}
// 繰り返し呼ばれて、プリンターの処理状況を調べる関数
func getProcResultF() {
    let strProcStatus : String = printerCtrl.checkProcStatusF()
    switch strProcStatus {
    case "OK", "NG" :
        setMsgPrintF(printerCtrl.getMsgResultF(0))
        runIntervalF(5.0, self, "clearMsgPrintF")
        flagUsePrinter = false
    case "GO" :
        if (--intRecallCount < 1) { // 指定回数を済ませら、継続せず終了
            setMsgPrintF("プリンターから応答がないまま、待ち時間が過ぎました。")
            runIntervalF(5.0, self, "clearMsgPrintF")
            flagUsePrinter = false
            return
        }
        // さらに継続
        addMsgPrintF("。")
        runIntervalF(1.0, self, "getProcResultF")
    default :
        setMsgPrintF("アプリ内部エラー:PRT01")
        flagUsePrinter = false
    }
}

このコードを見ると関数だらけという印象ですが、処理内容はおおまかに伝わると思います。開始前には、プリンター関係で何か実行していないかを調べています。trueが返ってきたら何かを実行中なので、何もしないで終了します。

開始後の繰り返し処理の中では、checkProcStatusF関数で「OK」または「NG」なら、getMsgResultF関数でメッセージを得て表示します。そのメッセージは5秒後に消しています。

「GO」が返ったら、カウント数を減らして終了回数に達したかどうか見て、達していれば終了メッセージを表示して、5秒後に消します。終了回数に達していなければ、検索の進行を示す意味でメッセージの最後に「。」を追加して、自分自身を1秒後に呼び出します。メッセージ欄への「。」の追加が、プログレスバーと同じ役割を果たしているわけです。

checkProcStatusF関数で、3種類以外の文字列が返ってきたら、内部エラーであることをメッセージで伝えます。ユニークなエラーコードを付けて、どこから出たエラーなのかを特定できるようにしておきます。

 

上記で使っている関数の説明もしておきましょう。runIntervalF関数は、指定した秒数後に、特定の関数を実行させます。1秒後に自分自身を動かす処理だけではなく、表示したメッセージを5秒後に消す処理にも使っています。

// 指定した秒数だけ待って、特定の処理を開始する(共通ライブラリ内)
func runIntervalF(rSec:NSTimeInterval, rTarget:AnyObject, rNextFunc:String) {
    let iTimer = NSTimer(timeInterval:rSec, target:rTarget, selector:Selector(rNextFunc), userInfo:nil, repeats:false)
    NSRunLoop.currentRunLoop().addTimer(iTimer, forMode: NSRunLoopCommonModes)
}

この関数は、どのアプリでも使うため、どこからでも呼び出せる関数として最初から用意してあります。UI部品の生成関数と同じく共通ライブラリに入れてあり、アプリ開発の最初に追加するのが私の定番です。

メッセージの表示や消去でも、それぞれ関数を用意しています。順番に、メッセージの表示、メッセージの後ろへ追加、メッセージの消去です。このうちメッセージの表示では、時刻の文字列を前に追加します。

// メッセージの表示(3つとも、呼び出す側のクラス内)
func setMsgPrintF(rStrMsg:String) {
    let iStr = zDate.getStrTimeF()
    labelMsgPrint.text = iStr + ", " + rStrMsg
}
// メッセージの後ろに追加
func addMsgPrintF(rStrMsg:String) {
    labelMsgPrint.text! += rStrMsg
}
// メッセージの消去
func clearMsgPrintF() {
    labelMsgPrint.text = " "
}

これらの関数を用意しているのは、メッセージ表示を間接参照として作るためです。呼び出す側を何も触らず、これらの関数を変更するだけで、メッセージの表示先を変えたり、メッセージを加工して表示したりが可能となります。

あと細かいことですが、メッセージの消去では空白文字を入れてあります。初期化のときは空の文字列、クリアーでは空白文字という違いを意識してのことです。画面上は同じに見えてても、内部では小さな違いをあえて作っておき、トラブル時の解析に役立てるという考えからです。空白文字が入っていれば、クリアー処理が1度は実行されたという証拠になりますから。これの発展形としては、空白の数とか、全角と半角の空白文字でも小さな違いを作れます。

 

ここまでの説明でも、かなり長くなってしまいました。いったん区切りたいと思います。続きは、次の投稿にて。

0 件のコメント:

コメントを投稿