2014年11月29日土曜日

業務アプリはデータ保全が一番大事

業務で使用するアプリの開発は、一般的なアプリと少し違うと思います。少なくとも私は、違うと考えて開発してます。どのような点を重視しているのかというと、入力したデータを可能な限り守ることです。iPadという環境なので大したことはできませんが、できないながらも工夫した点を紹介します。

 

アプリを仕事で使うということは、仕事上のデータを扱うということです。扱うというと抽象的なので、もっと具体的に言うと、仕事上のデータを入力して保存して活用するということです。こうして入力したデータは、会社の財産です。そのため、入力されたデータが失われないことを、業務アプリでは一番重視しています。

データを保存する場所としては、RAMメモリー内よりもフラッシュメモリー内のほうが格段に安全です。RAMメモリー内なら、アプリが異常終了したときに、データが失われてしまいますから。こうした理由から、データを入力した直後に、ファイルへ保存する形で作っています。iPadのファイルはフラッシュメモリー内に保存されるため、機械的なアクセスもなく、ハードディスクよりも安全で高速です。

毎回保存されるのを気にするかもしれませんが、タップ操作でアプリへ入力するデータの量などは、たかが知れています。圧縮などせずにテキストで保存したとしても、データ量が少なく、一瞬で書き出されます。

 

ファイルへ書き出す場合にも注意が必要です。もし書き出し中にエラーが発生したら、そのファイルは正常に読めなくなるかもしれません。もし読めたとしても、途中までのデータしか残っていない可能性があります。

書き出しに失敗した場合を考慮すると、毎回異なるファイル名で書き出し、どんどんと追加する方法もありますが、ファイル数が増え過ぎて現実的ではありません。同じファイル名に書き出しながら、失敗へのリカバリー方法も考慮するという手段が現実的でしょう。

最初に考えたのは、仮のファイル名で書き出し、書き出しが成功したら本来のファイル名に変更する方法でした。でも、先にあったファイル(つまり前回に保存したファイル)を、どう扱えば良いのか悩みました。そして次のような方法を採用しました。

保存するファイル名は毎回同じで、新しく入力したデータを後ろに追加してから保存します。同名の古いファイルは、旧版として過去3世代ほど残しておく方法です。

過去3世代が残っている状態だと、最新ファイルに加えて、old1、old2、old3という3個のファイルが存在することになります。その状態で新しく保存する場合、具体的な実行手順は次のようになります。

3世代を残しながら更新する手順
・old3ファイルを削除する
・old2ファイルをold3に改名する
・old1ファイルをold2に改名する
・最新ファイルをold1に改名する
・新しい最新ファイルを書き出す

oldが存在しない状態(使い始めたときの状態など)もありますから、個々の処理では、改名する前に存在を確認し、存在すれば改名するという形になります。

 

過去3世代のファイルを残しておいても、書き出しが失敗したとき、元に戻す機能がないと無駄です。リカバリー処理の1つとして、過去3世代を1つずつ戻す処理が必要となります。具体的な実行手順は次のとおりです。

旧3世代を1つずつ戻すリカバリー処理
・最新ファイルが存在すれば削除する
・old1ファイルを最新名に改名する
・old2ファイルをold1に改名する
・old3ファイルをold2に改名する

これらでも個々の処理では、改名する前に存在を確認し、存在すれば改名するという形になります。

リカバリー手順は以上で構わないのですが、最新ファイルの書き出しに失敗しているか調べる方法が必要となります。最終的には、人の目で確認するのが一番でしょう。最新ファイルだけでなく、old1〜old3も含めて、中身を見れる機能も用意しました。

通常の作業で、これらのリカバリー処理を使うことはありません。アプリが突然と異常終了したとか、入力後の保存処理で成功メッセージが出なかったときなど、普段と違う表示になったときに利用するものです。調べる手順などをヘルプに書いておき、そのヘルプを読みながら、リカバリー処理を進めるという形にしてあります。

一番長く使われている業務アプリでも、Titanium mobile時代も含めて3年ぐらいしか経過してませんが、異常な状態になったことは一度もありません。そのため、これらリカバリーの処理は何度もテストしましたが、まだ本番では一度も使われたことがないのです。良いことといえば、良いことなのですが。

 

アプリのトラブルは、データ入力時に起こるとは限りません。予想もしないトラブルが、突然と発生することも考えられます。たとえば、iPadの調子が悪くなって、どのアプリも不安定な動きになったとかです。こんな状態だと、いつiPadが起動しなくなるのか不安でしょう。真っ先にすべきことは、iPad内の重要なデータを、他のマシンにコピーすることです。

iOSのアプリでは、Macと接続したとき、iTunesでバックアップが取れるようになっています。その際、アプリ側の設定で、iTunesからアプリの保存ファイルが見えて読めるようにもできます。Info.plistファイルの項目で、「Application supports iTunes file sharing」を追加してYESに設定するだけです。こうしておくと、もし近くにMacがあれば、iPadと接続して全ファイルをコピーできます。

近くにMacが無い場合も考慮し、ファイルを添付して送信できるメール機能も追加しました。ファイル一覧をテーブルビューに表示し、添付したいファイルを選んで、他のマシン宛に送信するだけです。

毎日使われている業務アプリでは、この送信機能を通常業務としても利用しています。iPadで入力した業務データは、1日単位でファイルに保存されています。それをメール機能でMacに送信し、Mac側の表計算アプリに取り込んで、業務分析などに利用しているのです。メール機能を付けたために、iPad側は入力中心になってしまいました。将来的な話ですが、iPadがパソコン的に使えるようになったら、分析業務もiPadでやりたいそうです。その前に、クラウド上の表計算機能が使えるようになって、iPadとクラウドだけで十分になりそうですけど。クラウド側だと、バックアップとしても安心ですし。

ちなみに、業務アプリ開発の相談を受けた場合、最初に提案するのがサーバーを立てて、iPadからはウェブブラウザで使う形です。クライアント側のOSやアプリに依存せず、サーバー側だけでシステム更新が可能だからです。でも、パソコン上の表計算アプリでバリバリとデータ解析するような人は、サーバーを立てずに、iPadで入力専用のカスタムアプリを使いがるようです。主に小さなお店のオーナーですけど。

 

アプリ側のトラブル対処機能だけでは、十分な安定性を確保できません。動作環境であるiPadの使い方も、安定性が増すように注意する必要があります。iPad環境をどのように整えたら良いのか、アプリ内のヘルプとして記述しました。

その内容は、一般的なパソコンの注意点と同じです。余計なアプリを、できるだけインストールおよび起動しないこと。フラッシュの空き容量を十分に確保すること。アプリは何日も連続して使えるように作ってありますが、できれば毎日の業務終了後にアプリも終了し、1日1回は再起動してクリーンな状態で使うこと、といった具合です。こういう大事な点を知らせておくと、余計なトラブルを起こさずに済みます。

iPadのように、部品のほとんどが電子化されている機器の場合は、初期不良を除くと、あまり壊れないのではないでしょうか。機器の故障よりも、落下させたなどの人為的なミスで壊れる可能性が高いでしょう。それも、iPadにカバーを付けることで、大きく壊れることは防げます。機械的な破損の場合は、液晶画面が割れたまま動き続ける可能性もあり、その状態でならデータをコピーできます。そういった面も含め、データを守ることの大切さをユーザーに伝えることも、大事な仕事ではないかと思います。

 

アプリが異常終了してもデータを守る話を書きましたが、そもそもアプリが異常終了しないように作るのが大前提です。アプリが安定して動作するように、トリッキーな作り方を避けるのはもちろん、動作が少し遅くなっても構わないので、安全な作り方を選択するように心がけています。また、iOSアプリの基本ルールである「使い終わった画面は終了してメモリーを開放する」という形で、メインメニュー以外の画面を作っています。

おかげさまで、データを失うようなトラブルは、まだ起こっていません。「手を抜かないで真面目に作っているほど、安定して動く」という、先人の言葉を参考にしながら開発しています。

2014年11月27日木曜日

swiftの変数は、積極的に型宣言してます

swiftの変数宣言には、型推論の機能が備わっています。代入する初期値の型により、自動的に変数の型が決められる機能です。これを利用すると、変数宣言のときに、型宣言を付ける必要性が大きく減ります。ソースコードを少しでも短く簡潔に書きたい人には、喜ばれる機能だと思います。

しかし、私は、できるだけ型宣言を付ける方針でコーディングしています。StringやIntのように基本的な型なら省略することもありますが、それ以外の型では、可能な限り型宣言を付けています。その理由を、少し書きます。

 

変数宣言で型も一緒に宣言するのは、iOSのAPIに含まれる様々な型を明示するためです。私の場合は、よく使う機能を独自ライブラリの形で作るので、細かなAPIを覚えることがほとんどありません。もちろん作るときには、おそらく他の人よりも詳しく、ウェブ上のAPI資料を見ます。でも、ライブラリとして作り終わると、ライブラリ内の関数またはメソッドを呼び出すだけなので、個々のAPIの処理内容はすぐに忘れてしまいます。覚える気も最初からありません。だからこそ、忘れても構わないように、後から思い出しやすい形でコーディングしているわけです。

APIに含まれる型を変数宣言に入れると、変数の型で検索しやすくなります。そのため、後で機能を拡張したい時や、バグが発生したときに調べやすくなります。完全に忘れてしまっても大丈夫です。

 

加えて、複数のメソッドを連続して書くのではなく、途中に一時的な変数を挿入するようにもしています。その変数はletで宣言し、変数の型も宣言に含めます。型を指定して途中に変数を使うと、自分が正しく理解しながらコーディングしているかの確認にもなります。swiftは型チェックが厳しい言語なので、間違っているとコンパイラーがエラーを知らせてくれるからです。

APIの中身に詳しくないまま自動変換を使っていると、コンパイルエラーが消えないとか、コンパイルは通ったものの、実行させると正常に動作しないといった状況が生じることもあります。原因を探るために、途中に変数を入れたりするでしょう。

ところが、最初から途中で変数を使い、しかも型宣言までしていると、その変数が何の型なのかは明白です。たいていは実行前にコンパイルエラーとなり、自分の理解が間違っていることを知ることができます。API資料を見直して、正しく理解する作業に移れます。無駄な実行や、遠回りのデバッグを大きく減らせる手段でもあるのです。

最近のコンパイラーは最適化の機能が凄いので、途中に変数を入れても遅くなりません。もし遅くなったとしても、通常の動作に影響するほどは遅くなりません。安心して、途中に変数を入れています。

当たり前のことですが、変数の型が正しいだけでは、処理内容をきちんと理解していることにはなりません。でも、変数の型すら間違っているなら、正しい理解の最低限もクリアーしていないことを意味します。変数の型が正しいのか確認することは、最低限をクリアーしていることの確認でもあります。

途中に変数を入れる目的は、もう1つあります。将来、自分以外の開発者がアプリをメンテする可能性を考慮しての判断です。メンテできる開発者を少しでも増やすには、処理内容が理解しやすいコーディングが欠かせません。途中に変数を入れて、その型を明示することで、APIの内容を解析しやすく仕上げられます。メンテする必要が生じたとき、優秀な開発者を確保できるとは限りませんから。

 

iOSのAPIには、中身は同じなのに型が違うものが数多くあります。StirngとNSString、FloatとCGFloatなどは分かりやすい例でしょう。これらの型は、同じように見えても、使えるメソッドなどが違っています。NSStringにはdoubleValueメソッドがあり、Doubleに変換するのが便利といった具合に。違いを知って、上手に使い分ける必要があります。

これらとは違って、中身が分かりにくい型もあります。NSTimeIntervalの中身がDoubleというのはAPI資料を見ないと知り得ません。きちんと理解してコーディングするためには、NSTimeIntervalの中身がDoubleと知っておくのがベストです。中身の型が違っていると、自動的に変換してくれなかったり、変数の種類による精度の違いからバグが生じたりもします。

余計なトラブルを事前に避けるためには、中身の型を意識しながらコーディングし、中身の型ができるだけ見えるような形で途中の変数を使います。この場合、途中の変数が基本的な型であっても、変数宣言の中に、あえて型宣言を入れます。または、明示的にキャストを使ったりもします。

 

私の勝手な推測ですが、swiftの開発者は、厳密な型宣言や型チェックを重視したかったのではないでしょうか。Optionalなんかも、その一環と思えます。でも、それだけだと、ものぐさな開発者(失礼)に見向きもされないため、仕方なく型推論の機能を付けたのではないかと。それは現実的な解であり、選択肢が多いほうが良いだけに、何の文句もありません。

私のように、どんなに注意深く実行しても人間はミスすると思っている人は、可能な限り多くのことをコンパイラーに頼ります。変数の型を積極的に宣言するコーディング方法は、そのための一番の方法だと考えています。実際に何度も、実行前にミスを見付けてもらっています。今後も、変数宣言に型を入れ続けるでしょう。

 

私の開発スタンスは、APIの細かな名前などは基本的に覚えない(もちろん大まかな仕組みは理解しておく)、忘れても思い出せるようにコーディングするというものです。それを実現するためのコーディングとして、途中で挿入する変数を増やし、その変数宣言で型を明示するようにしています。

このようなスタンスは、覚えるのが苦手な人にも適していると思います。自分に向いているスタンスだと感じた方は、swiftでも積極的に型宣言しましょう。

2014年11月20日木曜日

管理しやすい分類情報付きファイル名を使おう

アプリを開発するとき、ソースコードだけでも何個ものファイルを作るでしょう。その名前(ファイル名のことですが)を、どのように付けていますか。機能を表す言葉を名前に付けるのは当然として、他に何か工夫してますか。

アプリの規模が大きくなるほど、ソースコードのファイル数が増えます。多くなったファイルを管理するために、どのような工夫ができるでしょうか。いろいろな工夫が考えられる中、今回はファイル名を使った工夫を取り上げてみます。

実は今まで、ファイル名の命名ルールに関してだけでも、何度かの改良を加えてきました。使用するOSや言語や開発ツールによっても異なるため、次々に良くしていったわけではなくて、使用環境ごとに合わせながら変えてきたわけです。ここで紹介するのは、Xcode+swiftという開発環境に合わせた工夫です。それでもオブジェクト指向の言語に共通する工夫だと思いますから、考え方は参考になるでしょう。興味のある方は読んでください。

 

ファイル管理のために、ソースコードのファイル名を工夫する方法では、ファイル名に分類情報を含めることで実現します。分類情報といっても、難しいことではありません。私の場合は、大まかな種類を示すために、先頭の2文字に役割を持たせるのが中心です。

具体的には、ファイル名の先頭に2文字の略号を加える方法で実現しています。実際に使っている略号は、今のところ、以下の5種類です。

// ファイル名の先頭に付ける主な分類情報
・AC:特定の条件(装置など)に依存する共通ライブラリのクラス(複数アプリで利用)
・BC:共通ライブラリのクラス(複数アプリで利用)
・BP:共通ライブラリの関数や定義値など(複数アプリで利用)
・MC:アプリで使うモデルのクラス
・VC:アプリで使うビューのクラス

見て分かるように、上から3つが自作の共通ライブラリで、どのアプリにでも使えるように作っています。何かのアプリを開発し始めたとき、必要なものを選んでプロジェクトに追加します。もちろん、開発途中に追加することもあります。

「BC」は、日付処理やファイル処理といった共通機能を、クラスとして実現したライブラリです。続く「BP」は、クラス以外の形で実現した共通ライブラリです。UI部品の生成関数や、目的別の色定義など、どのアプリでも使いそうな内容を入れています。

「AC」は、どのアプリでも共通に使えるライブラリの中でも、特定の装置に依存するとか、特定の環境だけで動くとか、何かの条件があるものが対象です。以前に紹介した、小型プリンタRoltoを制御するクラスなどが、この分類に入ります。

下から2つが、アプリごとに作成するクラスです。「MC」は、扱うモデルごとに作るクラスで、「VC」は、表示する画面ごとに作るクラスです。複数のモデルを使って、印刷用のレイアウト画像を生成するクラスなどは、「MC」に入れます。

5つのうち4つが、クラスとして作成するソースコードです。各ソースコードの中には、大きなクラスだけでなく、関連する小さなクラスも入れます。小さなクラスを独立したファイルとして保存すると、数が急激に増えてしまいます。それが嫌いなので、ファイル数を増やさないようにと、関連するものは一緒に入れています。

 

先頭に略号を付けるという話だけでは、どんな感じなのか想像しづらいでしょう。ということで、実際のファイル名に使った場合の例を挙げてみます。長くならないように、一部を抜粋した感じのファイル名一覧です。

// ファイル名一覧の一例(抜粋版)
・AC_Print_Net.swift
・AC_Print_Rolto.swift
・BC_Date.swift
・BC_DrawCG.swift
・BC_Files.swift
・BP_Base.swift
・MC_Pref.swift
・MC_Products.swift
・MC_Sales.swift
・VC_Env_Brows.swift
・VC_Env_Manag.swift
・VC_Main.swift
・VC_Pref_Edit.swift
・VC_Prod_Edit.swift
・VC_Sales_Brows.swift
・VC_Sales_Edit.swift
・VC_Sales_Graph.swift
・VC_Sales_Input.swift

見てのとおり、ファイル名を並べただけでも、きっちりと分類されている感じに見えます。また、先頭に付けた略号の変わり目では、非常に弱くですが、水平の区切り線があるようにも見えます。もちろん、そのような効果も狙っての命名ルールなのです。

先頭の略号に続けて、半角のアンダースコアを入れています。この1文字が、全体を見やすくするとともに、区切り線が見えるようにもしているのです。ファイル名の中に入れた2番目のアンダースコアも同様で、言葉の区切りを明確に見せています。

 

ファイル名の付け方では、先頭の略号以外にも、いくつかのルールを決めています。まず、名前を長くしないことです。短くて内容が推測できるように、長い言葉は略して付けるように決めています。

先頭の略号以外では、2段階の分類としての役割を持たせています。とくに画面ごとのクラスは、同じデータを扱いながら、入力用や閲覧用やメンテ用などの複数画面を作ります。それらの画面を区別しながら、同じデータを扱うことも表さなければなりません。扱うデータの種類を中分類に、役割の違いを小分類として命名すれば、先頭の略号を大分類として、3段階の分類階層が実現できます。

こうした階層による分類は、Xcode以外でファイルを扱うときにも役立ちます。Xcodeのプロジェクト上では、好きな並び順に動かせますが、Finderなどのファイル管理ソフト上では、ファイル名による並び順に変わります。そんなときも、階層で分類したファイル名なら、きっちりと区分けされた形でファイル名が並ぶのです。オープンダイアログでも同じで、探しているファイルが見付けやすくなります。

3階層の分類を使うのは、おもに「AC」と「VC」です。「AC」では、中分類に「Print」と付けることで、印刷関係のクラスだとが表現できます。「AC」での中分類は、共通ライブラリとして決まった言葉を用いることが大原則です。もう一方の「VC」では、中分類にモデルの種類を付けることで、1つのモデルを扱う複数画面を1つに束ねられます。また、どの「MC」と関係しているのかも明確に表せます。複数のモデルを扱う「VC」の場合は、複数のモデル名を短縮して、中分類に入れることになります。

複数ある「VC」の中でも、「VC_Main」だけは特別です。名前から分かるように、最初に表示される画面です。最初に表示されるわけですから、アプリ全体に関係する初期処理なども入れます。また、アプリで使う共通の定義値なども宣言します。これらを別に分けても構わないのですが、メイン画面に入れることで、初期処理中に何か重大なトラブルがあった場合に、トラブル内容の細かな表示が作りやすいメリットが生まれます。

今までやったことはないのですが、外から持ってきたソースコードのファイルを追加する場合のルールも決めています。そのようなファイルは名前を変えずに、元のまま追加するだけす。先頭の略号が付いていないことで、自分が作成したソースコードではないと区別できます。また、元のままのファイル名を残すことで、どこから来たのか探しやすくもなります。

 

先頭に付ける略号が、今の形に決まるまでは、何度か変更がありました。XcodeとSwiftを使ってアプリを作っていく中で、前に使っていたルールを変更しながら、今は上記の形になりました。つい最近のことです。

少し前までは、「AC」と「BC」の区別はなく、どちらも「ZC」としていました。また「BP」は、「ZZ」としていました。「Z」で始まる言葉が少ないので、ライブラリは「Z」で始めるのは良いかなと考えてのことです。でも、先頭に付ける略号なので、一般の言葉との重複を気にする必要はないと考え始めました。そこで、「Base」の頭文字を使った「B」に落ち着いたという流れです。「B」から分離した「A」は、名前でソートしたときに「B」の近くに並ぶ言葉を探し、思い浮かんだ「Add」の頭文字から取りました(深い意味はありません)。

このブログの何回か前に、「ZZ_Base」という記述が出てきたと思います。今は「BP_Base」に変えています。ようやく今回の形で、一応落ち着いたかなという感じですね。今後は、大きく変えないでしょう。略号の追加はあると思いますが。

 

Xcode上で作るswiftソースコードのファイル名は、クラス名と同一にする必要はありません。それよりも、自分が使いやすい(または管理しやすい)名前をつけることで、間違いを少しでも減らしたり、再利用する際に扱いやすくするほうが大事でしょう。そのような視点でファイル名の付け方を考えると、いろいろなアイデアが出てくるのではないでしょうか。この機会に、ファイル名に加える分類情報を考えて、自分なりに扱いやすい命名方法を見付けてください。

2014年11月17日月曜日

iOS実験専用アプリを活用しよう(4)

iOS実験専用アプリを作って活用する話の続きで、今回が最後です。書き残した話をいくつか追加します。細かい話ですが、意外に重要だったりもします。

 

40個のボタンを持つ実験専用アプリですが、機能をどんどん追加すると使い切ってしまいます。そうなったら、新しい画面を追加して開き、さらに40個のボタンを追加して、などと考える人もいると思います。でも、お勧めしません。機能が増えるほど、コンパイル時間も長くなり、シミュレーターを起動するまで待たされます。軽く使えるのが大事ですから、使い切った場合は、新しい実験専用アプリを用意するというのがお勧めです。

開発経験が豊富な人ほど、こういった実験専用アプリは多く使うと思います。該当する人は、最初から複数の実験専用アプリを用意して、ジャンルごとに使い分ける方法をお勧めします。たとえば、UI部品の実験専用アプリ、グラフィック関係の実験専用アプリ、ファイルの読み書きの実験専用アプリ、日付や文字列などの基本データの実験専用アプリという具合に。どれにも入れられない機能のために、その他の実験専用アプリも追加するとよいでしょう。

ジャンル分けには大きな意味があります。目的の機能が、単に探しやすいだけではありません。過去に作った機能を組み合わせて新機能を作るとか、関連性が強い機能が集まっているだけに、新しい機能を作りやすい環境にもなり得るからです。また、デバッグやテスト用に付加する機能も、共通で使えたりします。

ジャンル分けしたら、各アプリの画面表示にもジャンル分けを明記します。ラベルに入れる文字列で、ジャンルを明確に示しましょう。間違ったジャンルのアプリへ追加しないためにも。

 

複数の実験専用アプリをジャンルごとで作り分けるなら、それぞれの機能に適した細かなアレンジが可能です。文字列を処理する実験が多いなら、ラベルの数を増やすと使い勝手が向上します。今は1行に5個のラベルを入れていますが、あと1行か2行ぐらい増やして、ラベルの数を数倍にすれば、複雑な機能でもラベルを追加する必要がなくなります。

また、文字列を手で入力して実験したい場合もあるでしょう。ラベルとともにテキストフィールドを用意すると、各機能で追加しなくて済みます。テキストフィールドの場合は、入力した値をゲットする関数も必要となります。

このように、ジャンルに適した改良を加えると、より使いやすい実験専用アプリに仕上がります。何が必要かは、機能を作っているうちに明らかになるので、少しずつ改良していくとよいでしょう。

 

実験専用アプリには、40個のボタンがあるわけですが、10個ずつ4行に分かれています。自由に使って構わないのですが、簡単な分類を先に決めておくことをお勧めします。似た機能を並んで配置でき、ばらけて点在するのを避けられるからです。

例えば、文字列や数値などの基本データ用の実験専用アプリを用意したとします。4行のうち、1行目が文字列、2行目が文字列以外のデータ、3行目は文字列と他のデータの両方を使う場合、4行目は未定というように決めます。

分類は、データの種類だけとは限りません。データの扱い方で分類する手もあります。1行目は値の変換、2行目は値の加工、3行目は値のチェック、4行目はその他、というのもアリです。過去の経験から、どのような機能を作ることが多いのか、それらをどんな視点で分類したら整理しやすいのか、などと考えて決めます。

最初に決める分類は、あくまで仮決めです。作っていくうちに、変更しても構わないでしょう。まあ、最終的に点在気味になってしまっても、あまり気にしないことです。少し気持ちが悪いだけで、探せないことはないですから。

 

過去に作った機能を拡張するとか、既存の機能を少し改良したいこともあるでしょう。そんな場合は、前の作成物をそのまま残しておき、拡張版は新しいボタンに追加する作り方がお勧めです。前のままで使いたい場合もあるでしょう。また、拡張してみてダメだったときでも、前の状態に戻す作業は不要です。拡張版や改良版は、前の作成物を残したまま作り始めるのが基本です。

実際の作業では、前に作ったソースコードをそのままコピーして、新しいボタンのところへペーストします。ペースト先はボタン番号が違いますから、注意が必要です。コピーしたコードをそのままペーストした後、古い番号の箇所を新しい番号に書き直します。この作業だけで問題なく動くはずです。

改良を始める前に、必ず行ったほうがよいのが、改良前の状態のままでの動作確認です。コピー&ペーストをミスしている可能性がありますから、最低限の確認をするということです。

動作確認が終わったら、さっそく作り始めましょう。最初の状態での動作確認が終わっているだけに、安心して作り進むことができるはずです。

 

新しい機能の実験を追加して、必ず成功するとは限りません。なかなか成功しないときは、時間が経ってから再開するのが効率的でしょう。しかし、長く時間が経過して開くと、どんな問題が発生していたのか、すぐには思い出せません。

しばらく放置すると決めたら、どんな問題点があったのか、どこまで試したのか、ソースコードのコメントとして説明を残しましょう。短い説明でも残しておくと、状況が短時間で思い出せます。コメントの先頭には、「失敗中」などの未完成だと示す言葉を入れるのを忘れずに。

さらに明確にするために、ボタン名も変えておきます。私の場合は、「x」で始まるのが機能の停止中のルールなので、「xx」で始まるのが失敗中というルールにしています。停止中として「x」を使っているので、さらに別な文字を使わなくて済むようにと「xx」を選びました。こうした命名ルールは、好きな形に決めてください。

ここまで4回に分けて、実験専用アプリを上手に作って使う方法を紹介しました。一番大事なのは考え方で、紹介したアプリは一例に過ぎません。解説した考え方を参考にしながら、どんどんとアレンジして、自分にとって一番使いやすい実験専用アプリを作ってください。実験専用アプリは自分の財産になりますから、バックアップも忘れずにね。

2014年11月15日土曜日

iOS実験専用アプリを活用しよう(3)

iOS実験専用アプリを作って活用する話の続きです。前回は、簡単な機能の実験を取り上げました。続きとして、もう少し複雑な機能を作るときの注意点を解説します。今回のswiftコードも、一部を除いてViewControllerクラスに入れます。

 

各ボタンに用意してある2つの関数のうち、多くのコードを追加するのは、タップされたときに動かすほうの関数(名前がtestで始まる関数)です。行数が多くなると困るのが、機能をオフする場合。全体を囲むコメントで何とかなりますが、2カ所の入力が必要で、使いやすくはありません。やっぱり、オフする際には1行だけコメントに切り替える方法(2つの半角スラッシュ)が一番です。

1行だけでオンオフする方法としては、return文を追加する手も使えます。実行したいコードの先頭にreturn文を入れ、それをコメントにすると以降が実行されるという具合に。逆に、return文をコメントでなくすると、オフの状態という使い方です。ただし、return文が使える状態では、コンパイラーから注意が出てしまいます。return文以降が絶対に実行されないよ、という注意で、例の黄色い表示です。気分が良くないので、使いたくないのが普通でしょう。

これらとは違う方法を採用します。ボタンに追加する機能が2行以上になるときには、それ全体を関数として外に出します。ボタンのタップで起動する関数内では、外に出した関数を呼び出す形にします。こうすると、呼び出す部分が1行になり、、機能のオンオフが簡単になります。具体的なswiftコードは、機能をオフした状態では次のようになります。

// ボタンタップで動作する機能を関数として外に出し、それをオフした状態
// ====================================== ボタン15
func startBtn15F() {
    aryButton[15].setTitle("x総数計算", forState: .Normal)
}
func testBtn15F(rSender:UIButton) {
    setMsgF(1, "タップ15")
    //run15F() // <------------------------ コメントにしてオフ状態
}
func run15F() {
    ...
    getTotal15F()
    ...
}
func getTotal15F() {
    ...
}
// ====================================== ボタン16

ここで大事なのは、追加した関数名です。他の機能と関数名が重複しないように、関数名の一部にボタン番号を必ず入れます。ボタン15に追加する機能の場合なら、関数名を「run15F」や「getTotal15F」にするといった具合に。このようにルール化しておけば、関数名の重複を気にする必要がなくなります。

もちろん、例外もあります。作り終わったとき、関数をそのまま持っていけるように作りたい場合もあるでしょう。そんなときは、最終的に使う関数名を付ます。当然、関数名が重複しないように注意しながらですが。

 

作る機能が複雑になるほど、関数の外に変数を用意する必要性が増します。変数を使う場合も、簡単な注意点があります。まず変数名ですが、関数名と同じように、ボタン番号を必ず含めます。このルールに決めると、変数名の重複を気にする必要がなくなります。

次に変数宣言の挿入場所です。追加する機能だけで使う変数ですから、関係する関数の近くが最適です。これも自分なりのルールを決めたほうがよいでしょう。私の場合は、最初に用意された2つの関数よりも前に置くと決めています。最初に変数宣言があれば「この機能は、関数の外に出した変数を使っている」と気付きやすくなるからです。とにかく、自分の好みのルールで統一することが大事です。

私のルールで作った場合の具体的なswiftコードは、次のようになります。

// 関数の外で変数を使う場合の例
// ====================================== ボタン16
var iAVPlayer16:AVAudioPlayer!

func startBtn16F() {
    aryButton[16].setTitle("AIFF再生", forState: .Normal)
}
func testBtn16F(rSender:UIButton) {
    setMsgF(1, "タップ16")
    iAVPlayer16 = playSoundResF("Yen", "aiff", 1.0)
    iAVPlayer16.play()
}
func playSoundResF(rFName:String, _ rSuffix:String, _ rVol:Float) -> AVAudioPlayer {
    var iAVPlayer:AVAudioPlayer!
    let fileURL:NSURL = NSBundle.mainBundle().URLForResource(rFName, withExtension:rSuffix)!
    iAVPlayer = AVAudioPlayer(contentsOfURL:fileURL, error:nil)
    iAVPlayer.volume = rVol
    return iAVPlayer
}
// ====================================== ボタン17

この例のtestBtn16F関数では、コメント化する場合の対象が2行になっています。連続した2行ならコメントに変更しやすいので、わざと残してみました。実験専用アプリですから、ある程度までなら、緩く作っても構わないと思います。

 

画面上にUI部品を追加し、それに値を入れたり参照する場合も、関数の外に出します。置き場所は、変数と同じルールで構わないでしょう。具体的なSwiftコードは、次のようになります。

// UI部品(ここではUITextField)を追加する例
// ====================================== ボタン17
var txfTimeIntv:UITextField!

func startBtn17F() {
    aryButton[17].setTitle("HandCtrl", forState: .Normal) // <----- ボタン名変更もお忘れなく
    txfTimeIntv = createTxtFldF("12", 16, ALIGN_LEFT, 700, 60, 100, 30)
    viewBase.addSubview(txfTimeIntv!)  // <--------- オフするときは、この行をコメント化
}
func testBtn17F(rSender:UIButton) {
    setMsgF(1, "タップ17")
    run17F()  // <--------- オフするときは、当然ですが、こちらもコメント化
}
func run17F() {
    ...
    let iDouble:Double = Double(txfTimeIntv!.text!.toInt()!)
    ...
}
// ====================================== ボタン18

画面上にUI部品を追加した場合も、機能をオフしたいときがあります。そのときは、UI部品の生成はそのままにしておき、ビューに加えている箇所だけコメントにします。具体的には、addSubviewメソッドの箇所だけコメント化するということです。

この方法の利点は、続くコードへの影響がほとんどないことです。UI部品を生成して設定するところまでは残っていますから、そのUI部品を使うコードがコンパイルエラーになることはありません。

 

作りたい機能が大きくなると、独立したクラスとして作る場合もあるでしょう。その際には、追加するクラスを別なswiftソースコードに入れ、そのクラスを利用するコードだけをViewControllerクラスに入れます。

// クラスを使う側のコード(ViewController.swift)
// ====================================== ボタン18
func startBtn18F() {
    aryButton[18].setTitle("HandCtrl", forState: .Normal)
}
func testBtn18F(rSender:UIButton) {
    setMsgF(1, "タップ18")
    run18F()
}
func run18F() {
    let handCtrl = HandController()
    ...
}
// ====================================== ボタン19
// クラスは別なソースコードに分けて追加(HandController.swift)
class HandController {
    ...
}

クラスとして作る場合は、重複しないクラス名を付けるはずですから、ボタン番号を気にしないで付けても大丈夫でしょう。そのクラスを呼び出す関数だけ、ボタン番号を付けることになります。

 

続いて、デバッグ時に役立つ使い方も紹介しましょう。期待したとおりに動かない場合は、どこを通っているか調べたり、途中での値を表示したり、プログラムが動く様子を探ろうとするでしょう。ブレークポイントを設定して動き止め、途中の状態を調べる方法も、よく使われます。

それとは別に、println文を使って、動きの流れを記録したり、途中の変数を記録する方法もあります。println文で出力した内容は、デバッガで見ることになるため、画面の切り替えが生じます。少し手間ですね。その代わりに使うのが、用意したラベルへのメッセージ表示です。setMsgF関数とaddMsgF関数を組み合わせて、通った箇所を表示させます。

具体的な例を見たほうが分かりやすいでしょう。次のようなswiftコードになります。

// 
// ====================================== ボタン20
func startBtn20F() {
    aryButton[20].setTitle("HandCtrl", forState: .Normal)
}
func testBtn20F(rSender: UIButton) {
    setMsgF(1, "タップ20")
    run20F()
}
func run20F() {
    setMsgF(0, "通過:0,")
    ...
    if flagVX {
        addMsgF(0, "2b,")
        ...
        return
    }
    addMsgF(0, "2a,")
    ...
    addMsgF(0, "4,")
    ...
    switch (n) {
    case 1:
        addMsgF(0, "61,")
        ...
    case 2:
        addMsgF(0, "62,")
        ...
    case 3:
        addMsgF(0, "63,")
        ...
    default:
        addMsgF(0, "69,")
        ...
    }
    ...
    addMsgF(0, "9,")
}
// ====================================== ボタン21

かなり入れ過ぎですね。何種類かの例を含めたので入れ過ぎました。実際に使う場合は、適度な数を入れてください。

使い方の基本としては、最初にsetMsgF関数を使い、前の実行で残っている文字列を消しながら、新しい文字列を入れます。それ以降ではaddMsgF関数を使い、文字列を後ろに追加していきます。if文やswitch文のところでは、両方または見たいほうにaddMsgF関数を付けて、どちらに行ったのかを調べます。タップした直後に、動きがラベルに表示されるので、表示を切り替える必要はありません。もしテキスト入力欄を追加し、そこに文字列を入力してはタップするようなテストだと、値によって通る箇所の違いが容易に見えて助かります。

ちなみに、最初に表示するsetMsgF関数にだけ、先頭に「通過」という文字列を入れています。これは、何年後かにソースコードを見たときや動かしてみたとき、「通過した箇所を表示しているのだな」と理解させるためです。「少しの手間で、数年後の自分へ親切にする」という方針の一環です。

 

デバッグ時の途中の値の表示も、同様に簡単です。表示するラベル番号を選び、setMsgF関数やaddMsgF関数を追加するだけです。

// 実行途中の値もラベルに表示
    ...
    setMsgF(4, "iStr=\(iStr)")
    ...
    if (n > 5) { addMsgF(4, ",iStr2=\(iStr2)") }
    ...
 

値の表示でも、できるだけ1行で書くことが大事です。1行だけだと、先頭に「//」を追加してコメントに変更しやすいですから。

私の場合、コメント化した行は、使い終わっても残すようにしています。後から機能を変更したり、再テストするときに、その行が役立つこともあるからです。本番用のソースコードではないので、コードを綺麗に保つ必要はないでしょう。

 

ここまで、実験専用アプリの使い方を詳しく説明しました。いろいろな注意点も細かく取り上げたので、すべて理解すれば、かなりの使い手になると思います。次の投稿では、書き残した点などを書く予定です。

 

(使用開発ツール:Xcode 6.0.1, SDK iOS 8.0)

2014年11月13日木曜日

iOS実験専用アプリを活用しよう(2)

iOS実験専用アプリを作って活用する話の続きです。この話のキモとなる、ボタンの使い方を紹介します。正確には、ボタンに機能を加えて、テストする方法の紹介ですね。今回のソースコードも、前回と同様にViewControllerクラスに入れます。

 

40個も作ったボタンには、それぞれに2つの関数を用意します。何の実験機能も付けていない、空の状態で用意する関数です。最初の2つ分だけを切り出したswiftコードは、次のようになります。

// ====================================== ボタン0
func startBtn0F() {
    aryButton[0].setTitle("B0", forState: .Normal)
}
func testBtn0F(rSender: UIButton) {
    setMsgF(1, "タップ0")
}
// ====================================== ボタン1
func startBtn1F() {
    aryButton[1].setTitle("B1", forState: .Normal)
}
func testBtn1F(rSender: UIButton) {
    setMsgF(1, "タップ1")
}

それぞれ2つの関数には、ボタン番号(0〜39)が含まれていて、番号で区別されます。名前がstartで始まる関数は、アプリが起動したときに呼ばれるもので、開始前の準備処理を記述します。残りの、名前がtestで始まる関数は、ボタンをタップしたときの処理を記述します。こちらの関数は、前回、ボタンを生成した処理の最後のほうで、各ボタンに割り当てられています。

コメント行が右に長く伸びているのは、実験のソースコードを追加したときでも、ボタンの区切りが明確に見えるようにと考えてのことです。ソースコードを短く切り分ける工夫はしていないので、どうしても長くなります。でも、こうして区切りを明確にしておくと、ぜんぜん使いづらくはなりません。

 

準備処理の関数は、アプリを起動したときに実行しなければなりません。また、前回紹介したボタンやラベルの生成関数も、アプリの起動時に実行しなければなりません。viewDidLoadをオーバーライドする箇所へ入れて、アプリ起動時に実行させます。具体的なswiftコードは次のとおりです。

// 
override func viewDidLoad() {
    super.viewDidLoad()
    zDate = DateController()  // 日付機能の開始
    zFiles = BaseFiles()      // ファイル管理機能を開始

    startBasic1()    // 基本部品1の生成
    startBasic2()    // 基本部品2の生成
    startTestF()      // テスト用ボタンの生成

    // ボタンの前処理(startBtnXXF関数)を順番に実行
    for i in 0...39 {
        let iFuncName = "startBtn" + String(i) + "F"
        let iName = "n" + String(i)
        NSNotificationCenter.defaultCenter().addObserver(self, selector:Selector(iFuncName), name:iName, object:nil)
        let iNotif = NSNotification(name:iName, object:self)
        NSNotificationCenter.defaultCenter().postNotification(iNotif)
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }
}

一番最初に、日付管理やファイル管理といった独自ライブラリーのインスタンスを生成させています。この実験専用アプリにも、いつもの独自ライブラリーを使っているというわけです。

続いて、前回紹介した3つの生成関数を順番に実行します。これが実行し終わった段階で、ラベルやボタンが表示できる状態となります。40個のボタンそれぞれにも、今回紹介した2番目の関数(名前がtestで始まる関数)がリンクされています。

最後に、今回紹介した最初の関数(名前がstartで始まる関数)を、順番に実行しなければなりません。startBtn0F()からstartBtn39f()まで40個すべてを書いても構わないのですが、40行もあると格好が悪いでしょう。また、あまりにも芸がなさ過ぎです。というわけで、別な方法で実行させました。40個の関数名の文字列をfor文で順番に生成し、その文字列を渡して実行してもらう方法です。NSNotificationを使います。関数名の文字列ごとに、名前をつけて登録した後で実行させ、登録を削除するという手順です。もっと簡単な方法があればいいのですが、私が調べた限りでは、この方法しか見付かりませんでした。

以上で、実験専用アプリが出来上がりました。あとは、上手に活用するだけです。

 

では、実際に実験内容を追加してみましょう。最初は簡単な内容ほど理解しやすいと思いますので、ラベルに現在時間を表示する実験を選びます。実験するほどの内容ではないですが、ボタンの使い方を知るには適しています。最初のボタンとなる、ゼロ番のボタンに追加しましょう。

起動時の処理も使いたいので、起動時に現在時刻をラベル5に表示する機能を作ります。startBtn0F関数に1行を追加するだけです。さらには、何の実験をしているのか、画面から見えなければなりません。そのため、ボタンの名前(タイトル)も書き換えます。

続いて、タップしたときの機能も追加します。こちらもラベル5に、現在の時刻を表示しましょう。起動時に表示した時刻を、タップして上書きする動きとなります。以上を実現するswiftコードは、次のようになります。

// ボタン0に、実験機能(現在時刻を表示)を追加
func startBtn0F() {
    aryButton[0].setTitle("現在時刻", forState: .Normal)
    setMsgF(5, zDate.getStrTimeF())
}
func testBtn0F(rSender: UIButton) {
    setMsgF(1, "タップ0")
    setMsgF(5, zDate.getStrTimeF())
}

書き換えたのは3カ所です。startBtn0F関数では、文字列「B0」を「現在時刻」に書き換え、時刻を表示するための1行を追加しました。もう1つのtestBtn0F関数でも、時刻を表示するための1行をしてます。とても簡単ですね。

これを実行すると、ボタン0の表示が「現在時刻」に変わり、何かの実験機能が割り当てられていると知ることができます。そのボタンをタップすれば、ラベル5の現在時刻が更新できます。これで実験は成功に終わりました。

 

実験のために追加したコードのほかに、ラベル1へメッセージを表示する機能が、最初から付いていました。しかも、表示するメッセージの前に、実行時の時刻(時分秒)を加えて表示するようになっています。表示するメッセージは「タップ0」で、タップした操作であること、タップされたボタンの番号がゼロであることを示しています。

実際にタップすると、現在の時刻と一緒に「タップ0」が表示されます。タップする度に時刻が更新され、同じく「タップ0」が表示されます。タップされたことと、その時刻が毎回表示されているのです。

何のために、こんな機能があるのでしょうか。正常に動いてないとき、念のための確認ができる機能なのです。追加した機能を実験したとき、実験用のビュー上に何の変化もなかったとします。タップが正常に受け付けられていれば、その時刻と「タップ0」が表示されいているはずです。再びタップすれば、時刻の表示が更新されるでしょう。この動きから、基本的な機能は動いていると確認できます。逆に、再びタップしたときに時刻が更新されなければ、基本的な動作が停止していると判断できます。

新しい機能を実験するわけですから、最初は何か問題が発生するでしょう。そのとき、基本的な動作が動いているかどうか確認できれば、探す範囲を少しは絞り込めます。調べるために余計な機能の追加をせず、最低限の確認ができるようにと用意した機能なのです。実験専用アプリならではの工夫といえます。

 

最大40個もの実験を付けられますから、早々にラベルの数が足りなくなるでしょう。ラベルの数をどんどん増やす手もありますが、使える画面の面積は限られていますし、賢い方法とも思えません。実験が終わったボタンは、その機能をオフするほうが賢い選択でしょう。

追加した行をコメントにしてしまえば、簡単にオフできます。ただし、ボタン名の変更はオフすることが難しいですね。また、最初の「B0」に戻してしまうと、このボタンに機能が割り当てられていることが伝えられません。そこで、機能を表すボタン名を残しながら、オフしていることも伝えられるボタン名に変更します。たとえば、「現在時刻」から「x現在時刻」に変更するとか。ボタン名の先頭に「x」が付いているものは機能がオフ状態だと、自分なりにルールを決めてしまうのです。

このようにして機能をオフした状態は、次のようなソースコードになります。

// ボタン0に追加した実験機能をオフ状態にする
func startBtn0F() {
    aryButton[0].setTitle("x現在時刻", forState: .Normal)
    //setMsgF(5, zDate.getStrTimeF())
}
func testBtn0F(rSender: UIButton) {
    setMsgF(1, "タップ0")
    //setMsgF(5, zDate.getStrTimeF())
}

実験機能はオフされたので、この状態でボタンをタップすると、ラベル1に時刻付きの「タップ0」が表示されます。当然のことですが、ソースコードを修正して、コメントでない状態に戻せば、いつでも同じ実験を繰り返せます。

 

1つの機能を実験するのに、複数のボタンを使う場合もあります。そんなときは、隣り合ったボタンを利用します。大事なのは、複数のボタンが関係していると、アプリを起動した状態で明確に示すことです。余計な手間をかけないで示す方法としては、ボタン名を利用するのが一番でしょう。小文字のアルファベット1文字を決め、両方のボタン名の最初に付ければ、関係性は明確に伝わります。

具体的な例を見るのが一番でしょう。まだ機能を追加してない状態ですが、swiftのコードは次のようになります。

// ボタン2つをペアで使う場合(ボタン名だけ変更し、機能を加える前の状態)
// ====================================== ボタン8
func startBtn8F() {
    aryButton[8].setTitle("s画動_前", forState: .Normal)
}
func testBtn8F(rSender: UIButton) {
    setMsgF(1, "タップ8")
}
// ==================== ボタン9
func startBtn9F() {
    aryButton[9].setTitle("s画動_次", forState: .Normal)
}
func testBtn9F(rSender: UIButton) {
    setMsgF(1, "タップ9")
}
// ====================================== ボタン10

見て分かるようにソースコード上では、ボタン名だけではなく、区切りコメントの長さでも関係性を示しています。後ろ側の区切りコメントの長さを半分程度に縮めて、2つのボタンがペアで使われていることを表します。この例では、ボタン9の区切り行が短いので、ボタン8とボタン9がペアで使われていると分かります。

ボタン名の先頭に付ける文字ですが、機能停止用として「x」を使っている場合は、それ以外の文字を選ぶことになります。隣り合っていることが条件なので、離れているボタンであれば、同じ文字を2カ所以上で使っても構いません。ボタン8とボタン9のペアで先頭に「s」を付け、これらから離れたボタン21とボタン22のペアでも先頭に「s」を付けるといった具合に。

機能をオフするときの変更は、単独で使っているボタンと同じです。先頭に「x」を付けるルールなら、上記のボタンを「xs画動_前」と「xs画動_次」に変更することになります。このように停止した状態でも、2つのボタンがペアで使われていることが表現できます。

 

予想外に長くなってしまいました。さらなる続きは次の投稿にて。

 

(使用開発ツール:Xcode 6.0.1, SDK iOS 8.0)

2014年11月12日水曜日

iOS実験専用アプリを活用しよう(1)

皆さんは、初めて作る機能などを試すとき、どのような方法を用いてますか。swiftだから、playgroungですか。でも、UI部品を使う機能などは試せませんよね。私の場合は、実験専用のアプリを用意して、その中で様々な機能を作りながら試しています。このアプリを作ってから、playgroungはほとんど使わなくなりました。そんな実験専用アプリも、少しずつ改良したおかげで、どんどん使いやすくなってきました。アプリの中で使っている工夫も面白いのではないかと思い、皆さんにも紹介します。

 

最初に、専用アプリを用意する理由を取り上げます。まず良い点として、アプリ上で機能を作ると、最終的に使う形と非常に近い構造で作れる点が挙げられます。関数の形で作れば、またはクラスとして作れば、ほとんど直さずに目的のアプリへ持っていけます。

2番目の利点は、テスト環境まで含めて、どんどんと蓄積できることです。試しに作った機能を、後から改良したいと思ったとき、テスト環境まで含めて残っていると、すぐに改良を始められます。前の状態がそのまま残っているので、改良する部分だけを作れば済みます。逆に、新しい機能を試す度に新アプリを作っていたのでは、管理できず、必要なときに見付からないこともあり得るでしょう。でも、いくつもの機能をどんどん追加できる実験専用アプリなら、探す必要はありません。

いくつもの実験ソースコードを作ったとき、管理しやすいのも利点です。新しく作りたい機能が見付かったとき、実験専用アプリに追加していきます。すると、どんどんと機能が蓄積され、1つのアプリに様々な機能を保存できていることになります。探している機能が、どこに付いているのかも明確に示せます。もちろん、様々な機能を一緒に追加しても動くように、実験専用アプリを作る必要があります。まあ、その部分こそ、今回のキモなのですが。

新しい機能を簡単に試せることも、非常に重要です。実験専用アプリでは、追加する機能を試すとき、アプリ起動時に実行する処理や、ボタンをタップしたときに実行する処理など、処理を書く場所などの大枠が最初から用意されています。また、メッセージを表示するUILabelも複数用意してあります。機能を試すために、ボタンやラベルを追加する作業はほとんど不要です。こういった工夫をすることで、新しい機能を素早く作って試せます。

 

様々な利点を挙げたけど、どんな形をしたアプリなの?と疑問を持たれたことと思います。簡単に説明すると、ボタンが40個あり、メッセージ欄も6個あり、自由に使えるView領域を持ったアプリです。何の機能も追加していない、まっさら状態の画面表示は、次のようになっています(シミュレーター上のメニュー機能で画像をコピーしたため、余白が少し多めに追加されたようです。実際の画面表示では、中央の大きな長方形が、画面の左右両端に接しています)。同じ理由で、上下の余白も実際より広くなっています。

画面表示だけで全部を説明することは無理ですが、まずは画面表示を知ってもらわないと、説明が始められません。

 

画面の構成要素を、上から順番に紹介しましょう。一番上の左側にアプリのタイトルがあります。その右にはメッセージ用ラベルがあって、メッセージをクリアーするボタンが並びます。

その下には、合計20個のボタンが並んでいて、それぞれに機能を割り当てて使います。使い方が大事なのですが、それは後で解説します。一番下にもボタンが20個あり、使い方は上のボタンと同じです。上下に半分ずつ分けたのには、ちゃんとした理由があります。追加する機能によっては、テキストフィールドなどのUI部品が必要となります。その場合、ボタンが上下に分かれていると、その近い方に追加でき、追加したUI部品が増えたときに、ボタンとの関係性が理解しやすく配置できます。

上側のボタンの直下には、メッセージ用ラベルが5つあります。長さが違うものを用意して、必要な文字量に応じて使い分けます。5つのうち、一番左側のラベルだけは、決まった機能が割り当てられていて、通常は使えません。

中央にある大きな長方形が、自由に使えるUIViewの領域です。この上に様々なUI部品を配置して、テストすることになります。ただし、操作ボタンとメッセージ欄は既にありますから、それ以外のUI部品が対象です。

 

では、具体的な作り方として、swiftのコードを紹介します。実験をサポートする機能の前に、40個のボタンを生成する方法でも。これだけの数のボタンを作るとき、1つ1つをInterface Builderで描いていたら、大きさを揃えるのも含めて、非常に大変ですよね。私の場合は、Interface Builderを使わずに、UI部品の生成関数を使ってます。

実験専用アプリなので、ソースコードが雑です。その辺を気にせずに見てください。また、これから紹介するソースコードは、すべてViewControllerクラスに入れます。

UIButtonのインスタンスを入れる配列を用意して、20個ずつ2回に分けてボタンを生成しています。配置する座標を計算し、UI部品の生成関数に与えながら生成した後、配列に入れています。2回に分けたのは、配置場所が2つに分かれているからです。

// ボタンを入れる配列
var aryButton : [UIButton] = []
// テスト用のボタン生成
func startTestF() {
    // ボタンのサイズの基本値
    let iW : CGFloat = 95.0
    let iH : CGFloat = 30.0
    // ボタンの位置の基本値
    let iX1 : CGFloat = 15.0
    let addX1 : CGFloat = 100.0
    let iY1 : CGFloat = 60.0
    let addY1 : CGFloat = 40.0
    // 上段のボタン生成
    for i in 1...2 {
        let iY = iY1 + (addY1 * CGFloat(i - 1))
        for j in 1...10 {
            let iX = iX1 + (addX1 * CGFloat(j - 1))
            let iButton = createBtnF("Dummy", 14, iX, iY, iW, iH)
            aryButton.append(iButton)
        }
    }
    // 下段のボタン生成
    let iY2 : CGFloat = 690.0 - 80.0
    for i in 3...4 {
        let iY = iY2 + (addY1 * CGFloat(i - 1))
        for j in 1...10 {
            let iX = iX1 + (addX1 * CGFloat(j - 1))
            let iButton = createBtnF("Dummy", 15, iX, iY, iW, iH)
            aryButton.append(iButton)
        }
    }
    // 全部のボタンに設定(タイトル、アクション、表示基盤)
    for i in 0...39 {
        let iTitle = String(i)
        aryButton[i].setTitle(iTitle, forState: .Normal)
        let iStr = "testBtn" + String(i) + "F:"
        aryButton[i].addTarget(self, action:Selector(iStr), forControlEvents: .TouchUpInside)
        self.view.addSubview(aryButton[i])
    }
}

ボタンを生成して配列に入れた後は、40個のボタンをまとめて処理してます。タイトルをつけ、タップしたときに呼ばれる関数をセットし、ビューに加えてます。間接参照となるビューは使ってません。実験専用アプリだから、手抜きでもOKです。

このような場合こそ、UI部品を生成する関数が大活躍します。生成自体は1行でできますから、for文で回して一括生成しても短いソースコードで済みます。それぞれに配置座標を与え、配列に加えていきます。配列に入れてしまえば、同様にfor文で一括処理できます。

for文で一括生成する方法だと、後からの修正も簡単です。ボタンの大きさを変えたり、全体を上に移動したり、間隔を少し広げたり、文字サイズを変えたりが、1つか2つの数値を変更するだけでできます。何度か実行しながら修正すれば、好みの状態を得られるでしょう。ビジュアル系ツールには難しい芸当です。

 

続きとして、ボタンの使い方を書きたいのですが、それが当テーマのキモのため、長くなりそうなので、その前にメッセージ用ラベルを取り上げます。ボタンと同様に、UI部品の生成関数を使って作ってます。次のようなswiftコードです。行数が増えたのは、ラベルにグレーの枠線を付けたからです。ラベルの区切りを見やすくするために必要だと思いました。

// タイトルと最上部ラベルの生成
var lblMsg : UILabel!
var btnClesr : UIButton!
func startBasic1() {
    let lblTitle = createLblF("iOS各種実験アプリ", 30, ALIGN_LEFT, 20, 10, 300, 40)
    self.view.addSubview(lblTitle)
    lblMsg = createLblF("", 18, ALIGN_LEFT, 290, 15, 655, 30)
    lblMsg.layer.borderWidth = 1
    lblMsg.layer.borderColor = UIColor.lightGrayColor().CGColor
    self.view.addSubview(lblMsg)
    btnClesr = createBtnF("Clear", 16, 950, 15, 60, 30)
    btnClesr.addTarget(self, action:"clearMsgBF:", forControlEvents: .TouchUpInside)
    self.view.addSubview(btnClesr)
}
func clearMsgBF(rSender: UIButton) {
    clearMsgF(0)
}
// ラベルを生成(一番上のlblMsgラベルの生成は含まず)
var lblMsg1 : UILabel!
var lblMsg2 : UILabel!
var lblMsg3 : UILabel!
var lblMsg4 : UILabel!
var lblMsg5 : UILabel!
var viewBase : UIView!
func startBasic2() {
    lblMsg1 = createLblF("", 14, ALIGN_LEFT, 10, 140, 125, 20)
    lblMsg1.layer.borderWidth = 1
    lblMsg1.layer.borderColor = UIColor.lightGrayColor().CGColor
    self.view.addSubview(lblMsg1)
    lblMsg2 = createLblF("", 14, ALIGN_LEFT, 140, 140, 75, 20)
    lblMsg2.layer.borderWidth = 1
    lblMsg2.layer.borderColor = UIColor.lightGrayColor().CGColor
    self.view.addSubview(lblMsg2)
    lblMsg3 = createLblF("", 14, ALIGN_LEFT, 220, 140, 125, 20)
    lblMsg3.layer.borderWidth = 1
    lblMsg3.layer.borderColor = UIColor.lightGrayColor().CGColor
    self.view.addSubview(lblMsg3)
    lblMsg4 = createLblF("", 14, ALIGN_LEFT, 350, 140, 245, 20)
    lblMsg4.layer.borderWidth = 1
    lblMsg4.layer.borderColor = UIColor.lightGrayColor().CGColor
    self.view.addSubview(lblMsg4)
    lblMsg5 = createLblF("", 14, ALIGN_LEFT, 600, 140, 420, 20)
    lblMsg5.layer.borderWidth = 1
    lblMsg5.layer.borderColor = UIColor.lightGrayColor().CGColor
    self.view.addSubview(lblMsg5)
    viewBase = createViewF(0, 165, 1024, 515)
    viewBase.layer.borderWidth = 1
    viewBase.layer.borderColor = UIColor.lightGrayColor().CGColor
    self.view.addSubview(viewBase)
}
func setMsgF(rNum:Int, _ rStr:String) {
    switch (rNum) {
    case 0:
        lblMsg.text = rStr
    case 1:
        let strTime = zDate.getStrTimeF()
        lblMsg1.text = strTime + " " + rStr
    case 2:
        lblMsg2.text = rStr
    case 3:
        lblMsg3.text = rStr
    case 4:
        lblMsg4.text = rStr
    case 5:
        lblMsg5.text = rStr
    default:
        lblMsg.text = "ERROR:setMsgF"
    }
}
func addMsgF(rNum:Int, _ rStr:String) {
    switch (rNum) {
    case 0:
        lblMsg.text! += rStr
    case 1:
        lblMsg1.text! += rStr
    case 2:
        lblMsg2.text! += rStr
    case 3:
        lblMsg3.text! += rStr
    case 4:
        lblMsg4.text! += rStr
    case 5:
        lblMsg5.text! += rStr
    default:
        lblMsg.text = "ERROR:addMsgF"
    }
}
func clearMsgF(rNum:Int) {
    switch (rNum) {
    case 0:
        lblMsg.text = " "
    case 1:
        lblMsg1.text = " "
    case 2:
        lblMsg2.text = " "
    case 3:
        lblMsg3.text = " "
    case 4:
        lblMsg4.text = " "
    case 5:
        lblMsg5.text = " "
    default:
        lblMsg.text = "ERROR:clearMsgF"
    }
}

ソースコードの数値を見てわかるように、5つのラベルはどれも長さが違っています。表示する文字列の長さに応じて、適切な長さのラベルを選んで利用します。一応、限られた表示面積をもっとも有効に使えそうな、長さの組み合わせを選びました。なお、テスト途中で手動による内容クリアーが必要な場合は、最上位の消去機能付きラベルを利用します。

ラベル用の関数は共通で3つを付けました。単純に値を入れる、値を追加する、クリアーするの3つです。このうち文字列の最後に文字列を追加する関数は長いラベルで使い、状況の変化を見るのに便利です。

ラベルに値をセットする関数と、クリアーする関数を用意しました。すべてのラベルで共通で使います。ラベルに入れる値ですが、生成時は空の文字列で、クリアーすると空白文字が入ります。クリアーが1度でも実行されていれば、空ではなく空白文字が入るので、空の状態と区別できます。実験アプリなので、空白文字でクリアーする必要もなく、代わりに何かの文字(半角ピリオドとか)を入れる手もありますね。

前述のように、lblMsg1だけが特別で、文字列の前に時刻(時分秒)を追加しています。これが実験のとき、かなり役立つのです。使い方の説明のときに、詳しく解説します。

UIViewのviewBaseを付けた理由も簡単に触れましょう。これがなくてself.viewに付けるとなると、上部のボタンやラベルの分だけ座標位置を計算しなければなりません。縦軸の位置の値がゼロから始められるように、viewBaseを用意しました。また、枠線を表示することで、UI部品を付けられる範囲を示す意味もあります。

 

長くなってきたので、続きは次の投稿にて。

 

(使用開発ツール:Xcode 6.0.1, SDK iOS 8.0)

2014年11月10日月曜日

Roltoへの印刷機能をswiftで書く(補足編)

本編となるRoltoControllerの説明を2回に分け、しかも利用する側のswiftコードも一緒に書いたので、必要と思われる説明はほぼ書き終わりました。でも、少しだけ補足したいと思います。

 

Roltoの画質設定パラメーターは3つあります。その設定機能は、アプリの環境設定に含めたと、途中で説明しました。具体的なswiftコードは単純なのですが、一番悩んだのが画面表示です。最終的な表示結果は、次のように考えて決めました。

まずは、パラメーターの並び順です。SDKの解説書には、ブロックサイズ、しきい値、誤差拡散の順で並べられています。この3つのうち、最後の誤差拡散だけは特別で、オンに設定すると他の2つが無効となります。つまり、3つの中では誤差拡散だけが特別で、他の2つをオンオフする機能も持ち合わせます。このような位置付けなので、一番上に誤差拡散を持ってきて、残りの2つが続くという並び順が、ユーザーには一番理解しやすい形となります。

画面表示で使用するUI部品は、誤差拡散がUISwitch、他の2つがUISliderにしました。これらの設定機能を、アプリの環境設定に加えました。アプリを起動して表示した状態が、以下の画像(画面の一部をトリミングしたもの)です。

誤差拡散のオンオフは、2種類の印刷方式の切り替えと表現すれば、ユーザーに伝わりやすいと考えました。UISwitchのオンオフの方向に合わせて、印刷方式名のラベルをつけてあります。

残りの2項目は、UISliderに最大値と最小値を指定して付けました。文字数の関係で「ブロックサイズ」を「Bサイズ」としました(一応、正しい名前はヘルプ機能で補足してあります)が、それ以外は名称をそのまま使っています。もっとわかりやすい言葉に変えたかったのですが、思い浮かびませんでした。ユーザーには、意味が分からなくても、これらの値を変えながら好みの設定を探してもらうしかないと思います。一番下のUILabelで、誤差拡散のときには2つの設定が関係ないことを説明しています。

表示してある初期値(自分で使って、まあまあ良かった感じの値)にさほど意味はありません。しかし以前に、似たようなアプリを作ったときに「最初の値は何だった」と尋ねられた経験があります。余計な手間を減らす意味で、画面に初期値の表示を加えました。

アプリの動きは次のようになります。アプリの初期設定にはデフォルト値があり、何もしなければ、それが使われます。環境設定画面で変更して保存すれば、それが新しい設定値に変わります。RoltoControllerでインスタンスを生成した直後に、設定値をインスタンスに設定して、それが印刷時に使われます。アプリの起動途中で、環境設定画面から設定を変更すると、その新しい値が次の印刷から使われます。

印刷で使う画面も、環境設定画面も、使い終わったら消してメモリーも解放するため、メモリー上に一緒に存在することはありません。環境設定で変更した後は、新たな画面でRoltoControllerインスタンスが新しく生成されるので、必ず最新状態の設定が使われるようになっています。

具体的なswiftコードですが、あまりにも普通なので省略します。特別な工夫は不要ですから、swiftとiOS APIの基礎知識があれば、簡単に作れるでしょう。

 

Roltoの接続方式についても、少しだけ。iPadアプリからRoltoを使う場合、Roltoとの接続方式として、Wi-Fi直接接続と無線LANルーター経由の2種類から選べます。SDKはどちらにも対応していて、作り方に違いはありません。両方とも試しましたが、直接接続は何度も問題が発生しました。iPadがスリープから復帰したとき、Wi-Fi接続が別な相手に切り替わったり、長い時間アクセスしなかったRoltoが接続できなかったりと、接続に関するトラブルが意外に多く発生しました。ところが無線LANルーター経由でRoltoを接続すると、接続に関するトラブルは皆無でした。まれに通信エラーが発生しますが、直接接続と同じ程度でした。

私の限られた経験からですが、直接接続よりも、無線LANルーター経由を絶対的にお薦めします。複数の機器から印刷できますし、使い勝手の面からも、接続の安定性の面からも、圧倒的に上です。

実際に使ってみると、カバーが開いていたり、印刷の途中で紙が切れたりしたら、その状況がメッセージとしてiPadに表示されました。正しく動いていて、信頼できるSDKだと感じました。依頼主も大喜びなので、Roltoから印刷できるように改良して良かったと思います。

 

最後に、ちょっと注意点を。初回に書きましたが、今回のSwiftコードはXcode 6.0.1で開発しました。その後継バージョンとなるXcode 6.1では、Optionalの扱いが少し変わったようで、「!」の付いたコードの一部がコンパイルエラーになったり、「!」の不足でもコンパイルエラーが発生するようです。エラーになった箇所は、「!」を消したり足したりして、コンパイルエラーを解消してください。

2014年11月9日日曜日

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

前回に引き続き、小型プリンターRoltoを使うRoltoControllerクラスをswiftで書きます。前回は、Roltoを検索する機能だけで終わったので、今回は残りの機能を一気に紹介しましょう。前回は(RoltoのSDKが提供している)RoltoPrintDiscoverクラスが中心でしたが、今回はRoltoPrintクラスが中心となります。

 

今回最初に作るのは、プリンターの状態を調べるメソッドです。

// プリンタの状態を調べる
func getPrinterStatusF() {
    strProcStatus = PRINTER_PROC_GO      // 処理開始に設定
    if (roltoP == nil) { // ロルトが登録されてなければ中止
        strMsgResult = "Roltoが接続されていません。"
        strProcStatus = PRINTER_PROC_NG  // 処理失敗に設定
        return
    }
    if flagPrinting {  // 印刷中なら中止
        strMsgResult = "印刷中のため使えません。"
        strProcStatus = PRINTER_PROC_NG // 処理失敗に設定
        return
    }
    roltoP.requestPrinterStatus( { (rResult, rStatus) in
        self.setMsgStatusF(rResult, rStatus)
    } )
}
// ロルト状況のメッセージをセット
func setMsgStatusF(rResult:Bool, _ rStatus:RoltoPrinterStatus) {
    if !rResult {
        strMsgResult = "Roltoの状態取得に失敗しました。"
        strProcStatus = PRINTER_PROC_NG  // 処理失敗に設定
        return
    }
    strProcStatus = PRINTER_PROC_NG      // 処理失敗に仮設定
    switch (rStatus) {
    case 0:          // RoltoPrinterStatusIdle
        strMsgResult = "アイドル状態で印刷待ちです。"
        strProcStatus = PRINTER_PROC_OK  // 処理成功に設定
    case 1:          // RoltoPrinterStatusPrinting
        strMsgResult = "印刷中です。"
    case 2:          // RoltoPrinterStatusWritingFirmware:
        strMsgResult = "ファームウェア更新中です。"
    case 3:          // RoltoPrinterStatusCoveropen
        strMsgResult = "カバーが開いています。"
    case 4:          // RoltoPrinterStatusPaperOut
        strMsgResult = "用紙がありません。"
    case 5:          //RoltoPrinterStatusOverheated
        strMsgResult = "オーバーヒート中です。"
    default:
        strMsgResult = "想定外の状態です。"
    }
}

登録が成功してなくても呼ばれる可能性があるため、RoltoPrintが登録されているか調べ、登録されてない場合はエラーメッセージをセットします。印刷中というのは、印刷メソッドのところで出てきますので、ここでは説明を省きます。

前回と同じように、問い合わせ終了後の実行ブロックは関数として外に出し、スッキリさせています。返ってきた結果から、それぞれに対応するメッセージをセットするだけです。Roltoの状態を得るのに成功しても、印刷できる状態でない場合は、すべて処理失敗として扱っています。つまり、Roltoで印刷できる場合にだけ「OK」を返すということです。

 

もう1つ、プリンターが接続中かどうか、調べるメソッドも用意します。

// 
func checkPrinterOnF() -> Bool {
    if (roltoP == nil) { return false }
    return true
}

印刷などでプリンターを利用する前に、チェックするためです。

 

続いて、呼び出す側のswiftコードです。

// プリンターの状態を調べる関数
func getRoltSatus() {
    if flagUsePrinter { return }    // 何かの処理中なら中止
    if !(printerCtrl.checkPrinterOnF()) {  // プリンターが登録されていない場合は、
        findPrinterF()                     // プリンターを検索する(前回に作成)
        return
    }
    // プリンターの状態を問い合わせる
    flagUsePrinter = true
    setMsgPrintF("プリンターの状態を問い合わせ中。")
    printerCtrl.getPrinterStatusF()
    // 指定した回数だけ、結果を問い合わせる
    intRecallCount = 6
    runIntervalF(1.0, self, "getProcResultF") // 前回作った繰り返し関数を利用
}

一番最初は、何かを実行していないか、プリンターが登録されているかをチェックし、条件を満たさなければ中止します。開始後には、実行の終了を待つために、前回用意したgetProcResultF関数を使っています。指定した秒数だけ、終わるまで問い合わせ続けます。

 

また、RoltoControllerクラスの話に戻りましょう。次に作るのは、印刷するメソッドですが、その前に画質パラメーターを設定する変数とメソッドを用意します。

// 画質に関係するパラメータ3つ
var paramErrorDiff : Bool = true
var parmBlockSize : NSNumber = 0.5
var parmBinOffset : NSNumber = 0.5

func setImageParams(rP1:Bool, _ rP2:NSNumber, _ rP3:NSNumber) {
    paramErrorDiff = rP1
    parmBlockSize = rP2
    parmBinOffset = rP3
}

見てのとおり、値のチェックを省略してます。気になる方は、チェックを入れたほうがいいでしょうね。

 

いよいよ、印刷するメソッドです。UIImage画像を受け取って、Roltoを呼び出します。

// 印刷中を表す変数
var flagPrinting : Bool = false  // 印刷中はture
// 画像を印刷するメソッド
func printImageF(rImage:UIImage) {
    strProcStatus = PRINTER_PROC_GO        // 処理開始に設定
    if (roltoP == nil) {  // ロルトが登録されてなければ中止
        strMsgResult = "Roltoが接続されていません。"
        strProcStatus = PRINTER_PROC_NG    // 処理失敗に設定
        return
    }
    if flagPrinting {  // 印刷中なら中止
        strMsgResult = "印刷中のため使えません。"
        strProcStatus = PRINTER_PROC_NG // 処理失敗に設定
        return
    }
    let iParams:Dictionary = [RoltoPrintParameterBlockSize:parmBlockSize, 
        RoltoPrintParameterBinarizationOffset:parmBinOffset, 
        RoltoPrintParameterErrorDiffusion:paramErrorDiff]
    flagPrinting = true    // 印刷中に設定
    roltoP.performPrintImage(rImage, params: iParams, completion: {(rResult) in
        self.setMsgResultF(rResult)
        self.flagPrinting = false    // 印刷中を解除
    })
}
// 印刷結果のメッセージをセットする関数
func setMsgResultF(rResult:RoltoPrintResult) {
    strProcStatus = PRINTER_PROC_NG    // 処理失敗に仮設定
    switch (rResult) {
    case 0:          // RoltoPrintResultSuccess:
        strMsgResult = "正常に印刷されました。"
        strProcStatus = PRINTER_PROC_OK // 処理成功に設定
    case 1:          // RoltoPrintResultUserCancel:
        strMsgResult = "印刷がキャンセルされました。"
    case 2:          // RoltoPrintResultCommunicationError:
        strMsgResult = "通信エラーが発生しました。"
    case 3:           //RoltoPrintResultPaperOutError:
        strMsgResult = "用紙がありません。"
    case 4:           //RoltoPrintResultOverheatError:
        strMsgResult = "オーバーヒートで使えません。"
    case 5:          // RoltoPrintResultCoverOpenError:
        strMsgResult = "カバーが開いてて使えません。"
    case 6:           //RoltoPrintResultPrinterBusyError:
        strMsgResult = "印刷中のため使えません。"
    case 7:           //RoltoPrintResultUnexpectedError:
        strMsgResult = "原因不明のエラーが発生しました。"
    default:
        strMsgResult = "想定外のエラーが発生しました。"
    }
}

他と同じように、印刷終了後の処理ブロックは外に出しました。印刷開始や終了を知らせるための値をセットするのは他と同様です。

印刷開始から終了までの間だけ、印刷中を示すフラグをtrueにしています。検索や印刷をそのまま実行しても、印刷中のメッセージ番号が返るので分かります。しかし、自分からの印刷依頼だけは自分で分かるので、プリンターへの余計なアクセスを減らすために、付けたほうが良いと判断しました。メーカーのサンプルがそうなっていたので、そのまま真似したものです。

 

これを呼び出す側のswiftコードです。

// 画像を受け取ってプリンターへ印刷する関数
func printImageRoltoF(rImage:UIImage) {
    if flagUsePrinter { return }    // 何かの処理中なら中止
    if !(printerCtrl.checkPrinterOnF()) {
        setMsgPrintF("プリンターが接続されていません。")
        runIntervalF(4.0, self, "clearMsgPrintF")
        return
    }
    // メッセージを表示して、繰り返しの準備
    flagUsePrinter = true
    setMsgPrintF("プリンターに印刷を依頼中。")
    // プリンターで印刷
    printerCtrl.printImageF(rImage)
    // 指定回数だけ繰り返しながら、印刷の状態を問い合わせる
    intRecallCount = 50
    runIntervalF(1.0, self, "getProcResultF") // 前回作った繰り返し関数を利用
}

最初には、プリンター関係の何かを実行中と、プリンターが接続中かを調べ、大丈夫なら印刷を開始します。他のメソッドと同じように、指定された秒数だけ繰り返して問い合わせ、印刷結果をメッセージ欄に表示します。

 

再び、RoltoControllerクラスの話に戻りましょう。残りのメソッドも作りました。印刷の途中キャンセルと、プリンターの登録解除です。

// 印刷のキャンセル
func cancelPrintF() {
    roltoP.cancelPrint()
}

// プリンターの登録を解除
func releasePrinterF() {
    roltoP = nil
}

2番目の登録解除ですが、RoltoのSDKにはプリンターを解除する機能がないため、メモリーを解放するぐらいの効果しかありませんね。

 

以上、RoltoControllerクラスの中身と、その利用例のswiftコードを紹介しました。利用する側の例では独自ライブラリの関数を多用してます。でも、RoltoControllerクラスのほうでは使ってないので、汎用的に使えるクラスだと思います。

タイマーを使って問い合わせる処理も、RoltoControllerクラスに入れようかと考えました。しかし、利用する側の処理なので控えました。このままの形のほうが機能を明確に分けられているし、それでも再利用性が高いと思ったからです。

どのアプリでも、今回のRoltoControllerクラスを追加することで、Roltoから印刷する機能が簡単に追加できるでしょう。また、別なプリンターをサポートするコードも、ここで紹介した考え方で作れば、同様に再利用しやすいクラスとして仕上げられます。さらには、プリンターが違っても、メソッドを可能な限り同じに作れば、アプリでのプリンター切り替えが簡単になります。

今回の考え方を参考にして、機能などをどこで区切ったらよいのか考えながら、印刷機能や他の機能を作ってみてください。

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度は実行されたという証拠になりますから。これの発展形としては、空白の数とか、全角と半角の空白文字でも小さな違いを作れます。

 

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

2014年11月6日木曜日

Roltoへの印刷機能をswiftで書く(全体設計編)

どんな機能でも、ある程度の設計方針を決めてから作り始めないと、効率が悪いのはもちろん、仕上がりまで悪くなりがちです。ただの印刷機能ですが、全体の構造を先に考えました。今回は、その辺の話でも。

印刷機能といっても、いろいろな要素が含まれます。プリンターのドライバーと関わる部分、印刷内容を作る部分、ユーザーインターフェースに関わる部分などです。これらをどのように組み合わせるのか、どのように分割するのかを考えるのが、全体設計です。構成要素への分割だけでなく、要素間のインターフェースも含みます。

 

まずは、プリンターに近い部分から。プリンターを用いた印刷のような、特定の機器に依存する機能は、機器へ依存する箇所をできるだけ少なく作ることが大切です。つまり、別なプリンターを追加したり切り替えたときに、印刷機能を利用する側の変更が、少なくて済むように作るということです。具体的には、プリンターとやり取りするインターフェースを、特定のプリンターに依存せず、どのプリンターでも共通の仕様にしておきます。

プリンタを利用するための一般的な機能として、次のように洗い出してみました。

プリンターのクラスで用意する機能
・プリンターの検索と登録
・プリンターの状態確認
・プリンターで印刷
・印刷のキャンセル
・プリンターの解放と登録解除

これらをメソッドとして用意すれば、プリンターを切り替えたときの、プリンターを利用する側の変更が最小限で済みます。メソッド名も、機種に依存しない一般的な用語で作るのは当然のことですね。

クラスの作り方としては、プリンター全体のスーパークラスを先に用意し、そのサブクラスとしてRoltoのクラスを作るのが順当な考え方でしょう。でも、Roltoが1台目ですし、後で大きく違うタイプのプリンターを追加することもあり得ます。スーパークラスは2機種目の追加のときに考えれば良いし、余計な作業はやりたくないので、今はスーパークラスを作らないと決めました。今回は、スーパークラスを後で追加しても大丈夫なようにメソッドを用意し、Roltoのクラスだけを作ります。

Roltoを別なプリンターに切り替える話をしましたが、逆のこともいえます。別なアプリでRoltoを使う場合、Roltoのクラスをそのままコピーすれば、それを呼び出すコードだけ書けば済みます。つまり、再利用しやすいコードになるわけです。

 

続いて、印刷内容を作る部分です。オフスクリーンで描画し、その内容をUIImageとして仕上げます。オフスクリーンの描画に関しては、作成から終了までの機能を、独立したクラスとして以前に作りました。どのアプリにでも使えるようになっています。今回も、それを使います。

印刷内容を作るのに使うデータは、そのデータを扱うモデルのクラスに入っています。オフスクリーン描画クラスを使うと、実際に書くコード量は短くなるでしょう。今回は印刷の種類が1つで、わざわざ別クラスに作る必要もないと考え、モデルのクラスに含めることとしました。今後、印刷の種類が増えたら、別クラスに分けるかもしれません。

 

最後に、ユーザーインターフェース(UI)部分です。プリンターのような機器を使う場合は、UIに2つの要素が含まれます。1つは、印刷するデータを選んだり、印刷の形式を選んだりするもので、プリンターに直接関係しない要素です。残りは、プリンターを検索したり、印刷を開始したりとか、プリンターを操作する要素で、プリンターに直接関係するものです。これらが明確に分けて画面に付けられれば良いのですが、実際には混在して作ることになります。

また、プリントする機能は、対象となる内容を表示した画面に付けるのが一般的で、印刷する内容の種類だけ追加することになります。大抵の操作手順は、関係する画面ごとに印刷ボタンを用意して、タップしたら選択画面に切り替わります。現れた画面では、データの選択や、印刷の開始などが行なえます。

難しいのは印刷の途中キャンセルで、キャンセルを可能にするとUIが使いづらくなりがちです。現れた選択画面を消さないままキャンセル可能にしておき、そこに印刷の成否などを表示し、確認できたらユーザー操作で戻るといった形でしょうか。逆にキャンセルが不要なら、印刷開始で元の画面に戻り、印刷の成否は、戻った画面にメッセージを表示するという形も可能です。余計な操作を不要に作れます。

以上のようなことを総合的に検討するのですが、今回は印刷が1種類で、特定の画面上でしか使わないため、そこに多くの機能をつけることとしました。もちろん依頼主と相談し、要望を重視しての結果です。

具体的には、その画面を開いたときにRoltoを自動的に検出して(検出の手間を不要にする配慮)、画面上には、Roltoの状態を表示するボタン(もし接続してなければ、検出から始める)、印刷ダイアログの呼び出しボタンを加えます。ダイアログ内では、印刷対象のデータを選択する機能と、印刷の開始とキャンセルのボタンも付けます。印刷時間が短そうなので、印刷の途中キャンセル機能は付けません。また、Roltoの画質変更機能は、アプリの環境設定画面に付けることとしました。

以上の機能は、それぞれ対象となるクラスに加えます。こちらも、印刷の種類が増えた時点で、新しいクラスを追加するか検討することとしました。

 

全体の構造が固まったので、あとは1つずつ作っていくだけです。まずは、Roltoのクラスからでしょう。

2014年11月4日火曜日

Roltoへの印刷機能をSwiftで書く(準備編)

以前開発したiPad業務アプリの依頼主から、新しい要望が来ました。運用中の業務アプリから、小型プリンターRolto PT-10への印刷機能を追加してほしいとのことでした。強い要望だったのと、プリンターをプレゼントされたので、結局作りました。その要点を何回かに分けて紹介します。

 

話を聞いたとき、そのプリンターの存在は知りませんでした。同じように知らない人がいると思いますから、まずプリンターを紹介します。キングジムさんが開発した、50mm幅の感熱ロール紙を使う小型プリンターです。他の機器とのインターフェースはWiFiのみで、iPhoneアプリが無料で公開されています。そのアプリを使って、メモや写真や、ウェブブラウザの表示内容などを印刷できます。

感熱ロール紙ですから、印刷できるのはモノクロ2値のドットだけです。そのままだと中間調を印刷できないため、写真の印刷では誤差拡散によるディザ画像となります。

プリンターと同時に、iOS用のSDKも公開されたようで、そのことを知った依頼主が、何とか作れないかと相談に来たわけです。SDKは誰でもダウンロードできるようになっていました(検索で簡単に探せるので、リンクは付けません)。

 

SDKを調べると、次のようなことが判明しました。ドライバーはC++で開発され、Objective-C用にラッピングされていること。サンプルのアプリケーションは、すべてObjective-Cで書かれていました。また、注意点もありました。組み込むためには、C++のコンパイラーを呼び出す形にすること、コンパイラーを「libc++」から「libstdc++」に変更すること。

機能追加の対象となるiPad業務アプリの最終版は、すべてSwiftで書いています。つまり、追加する印刷機能もSwiftで書くことになります。当然、Objective-C用の作られたドライバーを、Swiftから呼び出す形です。少し工夫が必要となる分、誰かの参考になると思い、ここで紹介することにしました。

 

まずは、Objective-C用ドライバーを呼び出すための準備から。検索すると、非常に丁寧に解説しているウェブページを発見しました。Xcode上の設定操作まで含めて、かなり詳しく書かれています。

  Objective-Cコードをswiftから呼び出す方法を解説
   How to call Objective C code from Swift

私は少し遠回りしましたが、このとおり操作すると、問題なく使えるようになります。

 

開発環境のXcodeは6.0.1、RoltoのSDKは最新の1.01です。SDKに付いてきた、ヘッダーファイルの「RoltoPrint.h」とモジュールの「libRolto-PrintSDK.a」も、プロジェクトに読み込みました。SDKに付属の資料を見ながら、swiftのコードも少し書き始めていました。

上記サイトの説明どおりに操作したところ、いきなりのコンパイルエラーです。SDKの資料を見ながら速攻で書いた部分も多数のエラーが出ました。さらに、SDKに付属のヘッダーファイル「RoltoPrint.h」にもエラーが出ています。サイトの説明どおりに操作したつもりが、どこかで間違ったのかと思い、何度も確認しましたが、どうやら間違ってはいないようです。行き詰まってしまいました。

少し休憩して「RoltoPrint.h」を眺めていたら、アレレと気付きました。UI部品を使っているのに、そのimport文が付いてないのです。さっそく「RoltoPrint.h」に「#import <UIKit/UIKit.h>」を追加して、エラーは消えました。まさか提供されている側にエラーの原因があるなんて、予想もしませんでした。そのため気付くのが遅れ、数十分ほど無駄にしました。いやあ、思い込みは怖いですね。ちなみにこのエラーは、「ご意見・お問い合わせ」受付からのメールにて、キングジムさんに報告しました。先ほどですが。ははは(汗)。

今回エラーが出たのは、「RoltoPrint.h」を「Lib」フォルダ以外にもコピーしていたのも原因なのですが、エラーが出るようなヘッダーファイルじゃマズイでしょうという話です。

 

「RoltoPrint.h」のエラーが消えると、「RoltoPrint.h」のクラスが認識され、他のコードのコンパイルエラーも大きく減りました。当たり前ですね。コンパイルエラーが消えたことで、正常に接続されたことが確認できました。ここまでで、ようやく開発準備が整いました。