設計方針も決まりましたので、いよいよコードを書いていきます。まずは、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度は実行されたという証拠になりますから。これの発展形としては、空白の数とか、全角と半角の空白文字でも小さな違いを作れます。
ここまでの説明でも、かなり長くなってしまいました。いったん区切りたいと思います。続きは、次の投稿にて。