2014年12月30日火曜日

設計しながらコーディングする方法

このブログでは、いくつかの機能を実現する方法を取り上げました。その際には必ず、最初に大まかな設計をしています。その具体的な方法を紹介します。

 

この大まかな設計作業ですが、紙に書いているわけではありません。実は、Xcode上で行なっています。それも、ソースコードを編集するテキストエディタ上でです。「えっ」と思った方が、いるかもしれませんね。でも、いろいろと試した結果、この方法が一番効率的だと判断したわけです。あくまで、私にとって一番効率的ですが。

 

では、具体的な手順を見ていきましょう。

まず最初に行なうのは、入れ物の作成です。そう、Swiftのソースコードを入れるファイルです。Xcode上で新規のSwiftファイルを追加します。

作成時には、これから作成する機能に適したファイル名を付けます。このファイル名も、私なりにルール化していますから、そのルールに合わせた名前を決めます。環境設定の機能を受け持つライブラリのソースコードなら、「BC_Pref.swift」ですね。

そのソースコードを、最初はどこに作るのでしょうか。私の場合は、以前の投稿で紹介した、iOS実験専用アプリを使います。ライブラリ化するソースコードは必ず、このアプリ内で作ります。Xcodeのメニューから、新規Swiftソースコードを追加して、準備は完了です。

 

入れ物が用意できたので、大まかな設計を始めます。思いついた機能や工夫を、テキストとして打ち込みます。ソースコードとして処理されるので、当然、コメントとして入力します。形式としては、箇条書きのメモです。

言葉をひととおり入力したら、今度は全体の構成を考えます。クラスとして仕上げるなら、必要となるクラス変数やメソッドを洗い出して、全体の形を想像します。

全体の形がほぼ決まったら、その内容をコメントの形で入力します。大まかには、次のような感じになります。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?
// データ型の一致をどうやって調べる?
// ====================================== 必要な仕様
// 全体を1つのクラスとして作る
// データ型ごとにメソッドを用意する:Int, Float, String, Boolの4種類
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// ====================================== 構成要素
// クラス変数:
// メソッド:初期化
// メソッド:ファイルから読み込み、ファイルへ書き出し
// メソッド:値の取得
// メソッド:値の設定

ここで大事なのが、区切り線です。たくさんの文字を入力しても、どこが区切りなのか明確に示すために、区切り用のコメント行を挿入します。コメント行の最後に、それ以降で入力する内容の見出しを付けます。この見出しが、入力したテキストからはみ出ているように見えると、区切りごとの内容が見やすくなります。

区切りの先頭には、必ず「検討すべき点」を入れます。ここは、解決していない問題点や、疑問に思っていること、機能として含めようか迷っていることなど、決まっていない内容を書きます。今後判断しなければならない内容なら、何でも記述するのが基本です。

 

クラスとして実現する場合は、まず最初にクラスだけ作ります。その中に、構成要素として挙げたコメント行を入れます。この時点で、クラス内の構成要素の並び順も決まります。

このような作業を続けながらも、気付いた点をコメントとして追加していきます。コメントは少しずつ増えていき、それに比例して仕様も細かくなります。ここまでの状態では、次のような感じになっているでしょう。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?
// ====================================== 必要な仕様
// データ型ごとにメソッドを用意する:Int, Float, String, Boolの4種類
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
    // クラス変数:設定値セット、保存ファイル名、バージョン番号
    // メソッド:初期化
    // メソッド:ファイルから読み込み(保存ファイルの読み込みも含む)
    // メソッド:ファイルへ書き出し
    // メソッド:値の取得(4種類)
    // メソッド:値の設定(4種類)
}

クラス変数やメソッドのコメントをクラス内へ移動させたので、実際のコードを書き始められる状態となりました。ここからの進め方としては、コメントをさらに追加して仕様を細かく詰めるか、メソッドなどの具体的な入れ物(Swiftコード)を先に作るか、どちらでも好きな形で進められます。

これ以降の作業でも同じなのですが、「検討すべき点」や「必要な仕様」の中に含まれるコメント行は、不要になった時点でどんどんと削除していきます。この段階では、クラスを作ったので、「必要な仕様」に含まれる「全体を1つのクラスとして作る」の行を削除しました。

 

今回は、先にSwiftコードの入れ物を作ってみましょう。おそらくですが、その手順ほうが、動いたときの状態を想像しやすい人が多そうですから。この段階では、メソッド名は決めますが、引数などは最小限しか入れません。その結果、次のようなソースコードになります。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// ====================================== 必要な仕様
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
    // クラス変数:設定値セット、保存ファイル名、バージョン番号
    // メソッド:初期化
    init() {
    }
    // メソッド:ファイルから読み込み(保存ファイルの読み込みも含む)
    func readPrefFileF() {
    }
    // メソッド:ファイルへ書き出し
    func writePrefFileF() {
    }
    // メソッド:値の取得(4種類)
    func getPrefF() -> Int {
        return 1 // dummy
    }
    func getPrefF() -> Float {
        return 1.0 // dummy
    }
    func getPrefF() -> String {
        return "dummy"
    }
    func getPrefF() -> Bool {
        return false // dummy
    }
    // メソッド:値の設定(4種類)
    func setPrefF(rData:Int) {
    }
    func setPrefF(rData:Float) {
    }
    func setPrefF(rData:String) {
    }
    func setPrefF(rData:Bool) {
    }
}

このまま入力を進めると、追加したソースコードやコメント行が増えるほど、メソッドの区切りが見づらくなってしまいます。それを防ぐために、メソッドの種類ごとの境目に、区切り用のコメント行を挿入します。その結果、次のようになります。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// ====================================== 必要な仕様
// エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
// 設定値セットにバージョン番号を含める
// ====================================== 構成要素
class PrefController {
    // クラス変数:設定値セット、保存ファイル名、バージョン番号
    // ==================================== 初期化
    init() {
    }
    // ==================================== ファイルの読み書き
    // ファイルから読み込み(保存ファイルの読み込みも含む)
    func readPrefFileF() {
    }
    // ファイルへ書き出し
    func writePrefFileF() {
    }
    // ==================================== 値の取得(4種類)
    func getPrefF() -> Int {
        return 1 // dummy
    }
    func getPrefF() -> Float {
        return 1.0 // dummy
    }
    func getPrefF() -> String {
        return "dummy"
    }
    func getPrefF() -> Bool {
        return false // dummy
    }
    // ==================================== 値の設定(4種類)
    func setPrefF(rData:Int) {
    }
    func setPrefF(rData:Float) {
    }
    func setPrefF(rData:String) {
    }
    func setPrefF(rData:Bool) {
    }
}

見て分かるように、区切りコメント行の追加によって、前にあったコメント行の内容を吸収している箇所があります。記述する内容は、できるだけ区切り行に含めて、他のコメント行を減らすように心掛けます。

 

ここまでの段階でも、クラス内の大まかな構成が見えてきたと思います。ここからは、実際のコーディングを進める前に、細かな仕様を固める段階に入ります。大事なのは、クラス変数、メソッドの引数と戻り値、エラーチェックの内容、メソッド内に含める処理の4つです。これらを明らかにするように、細かな点を固めていきます。

まず最初に、クラス内の変数を決めます。この例では、キー付きの環境設定値セットを入れるために、データ型を決定しなければなりません。必要なら、簡単な実験を別にすることもあります。いろいろ調べた結果、今回はデータ型Dictionaryで大丈夫と結論付けました。

クラス内の変数が追加できたら、各メソッドに引数や戻り値を追加します。たまたま今回の例では、戻り値が追加済みなので、引数だけを追加することになります。さらに、エラーチェック内容と含める処理を、コメント行として追加します。

ここまでの状態で、次のようなソースコードになりました。

// BC_Pref.swift
import Foundation
// ====================================== 検討すべき点
// 環境設定値を入れるデータ型は?:Dictionaryが使えそう
// データ型の一致をどうやって調べる?:is演算子で大丈夫か?:if letも使えそう
// 上記のデータ型チェックは、全データ型の組み合わせで結果を確認すること
// ====================================== 必要な仕様
// ====================================== 構成要素
class PrefController {
    // クラス変数:設定値セット、保存ファイル名、バージョン番号
    var dictPref : Dictionary<String,AnyObject> = [String:AnyObject]()
    var prefFName : String = "dummy.plist"
    var strVersion : String = ""
    // ==================================== 初期化
    init(_ rDictPref:Dictionary<String,AnyObject>, _ rFName:String, _ rVer:String) {
    }
    // ==================================== ファイルの読み書き
    // ファイルから読み込み(保存ファイルの読み込みも含む)
    // ファイルが存在するか、存在したらバージョン番号を調べる、一致したら値を採用
    func readPrefFileF() {
    }
    // ファイルへ書き出し
    // バージョン番号を追加する
    func writePrefFileF() {
    }
    // ==================================== 値の取得(4種類)
    // 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
    func getPrefF(rKey:String) -> Int {
        return 1 // dummy
    }
    func getPrefF(rKey:String) -> Float {
        return 1.0 // dummy
    }
    func getPrefF(rKey:String) -> String {
        return "dummy"
    }
    func getPrefF(rKey:String) -> Bool {
        return false // dummy
    }
    // ==================================== 値の設定(4種類)
    // 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
    func setPrefF(rKey:String, _ rInt:Int) {
    }
    func setPrefF(rKey:String, _ rFloat:Float) {
    }
    func setPrefF(rKey:String, _ rString:String) {
    }
    func setPrefF(rKey:String, _ rBool:Bool) {
    }
}

ここまで出来上がると、具体的な動きが想像しやすくなります。頭の中でプログラムを動かし、呼び出し側とのやり取りを空想実行してみます。

どの順番でメソッドが呼び出され、それぞれのメソッド内で何が行なわれ、処理結果として何が返されるのか。また、クラス内の変数が、どのように変化していくのか。さらには、間違った使い方をされたとき、クラッシュなどの大きな問題が発生しないのか。などなど様々な点で、仕様に欠点がないかを探します。

もし問題を発見したら、引数を加えたり変更したり、新しいメソッドを追加したり、エラーチェックの内容を書き加えたりします。変更した場合は、再び空想実行してみて、変更の正しさを確認します。これで大丈夫と思えるまで、以上のような検討を続けるわけです。

この段階で細かな仕様を固める理由は、全体を見通しやすいからです。引数や戻り値を含んだメソッド名だけが並び、ほとんどが画面内に収まるか、少しスクロールするだけで全部が見れます。そんな状態なので、相互の関係とかも含めた、全体での整合性などが思考しやすくなっています。

 

いろいろと考えていると、細かな疑問点や不明点が見付かるものです。

たとえば、取得メソッドでエラーが発生した場合、正しい値は返せません。どんな値を返すべきなのか、この段階で決めるのがベストだと思います。nilを返すのか、データ型に該当する何かの値を返すのか。検討して決めた内容を、該当する箇所へコメント行として追加します。次の例のように。

// 取得メソッドにコメント行を追加(該当部分だけ抜粋)

    // ==================================== 値の取得(4種類)
    // 共通エラーチェック:設定値セットが空か、存在するキーか、データ型が正しいか
    // エラー発生時には、nilではなく、ありそうもない値を返す:-99999とか、"ERROR!!!"とか
    func getPrefF(rKey:String) -> Int {
        return 1 // dummy
    }
    func getPrefF(rKey:String) -> Float {
        return 1.0 // dummy
    }
    func getPrefF(rKey:String) -> String {
        return "dummy"
    }
    func getPrefF(rKey:String) -> Bool {
        return false // dummy
    }

具体的な戻り値も、思いついたのならコメント行に記述します。このような形で、細かな仕様を少しずつ明らかにしていきます。

すべての疑問点や不明点が解消され、何もなくなったら、設計段階が終了です。あとは、メソッドの中身をコーディングしていくだけです。

 

ここまでで気付いた疑問点や不明点を解消済みですから、コーディングを開始したら、一気に書いてしまえます。設計段階で深く考えているほど、新しい疑問点や不明点は出てきません。

逆に、設計段階の考えが不十分だと、疑問点や不明点が新たに出てきます。そのような場合には、まず最初に、ソースコードの先頭部分に付けた「検討すべき点」へ、出てきた疑問点や不明点を、コメント行として書き加えます。それから、解消する方法を考え始めます。

基本的には、開始したコーディングを一旦中断して、疑問点や不明点を解消するのが先です。解消方法によっては、メソッドなどの構成が影響を受けるので、無駄な作業を行わないためです。ただし、解消方法がすぐに出そうもないときは、気分転換も兼ねて、コーディングを続けるという選択肢もあります。後から余計な修正が入っても構わないと、割り切りながら。

コーディングしていると、共通の処理が見えてきます。その場合は共通処理を関数として追加し、無駄なコードを書かないように工夫します。また、定義値なども積極的に追加し、間接参照を利用したソースコードに仕上げます。

コーディングが終わった時点のソースコードを、ここに載せても構わないのですが、長いので省略します。そのソースコードは、実際の処理を書き加えたメソッド、メソッドが共通で使う関数、メソッドで使う定義値の変数などが、追記された状態となります。具体的なコードは、数回前の投稿に含まれますから、そちらを参照してください。

 

ひととおりの機能を作り終えたら、本格的な機能確認を行ないます。機能確認というのは、実装した機能が具体的にどう動くのか、確認するための試験実行です。SDKの多くは、細かな仕様を書いておらず、不明確なことが多くて困りものです(書くのが大変なので、気持ちは分からないではないですが)。実際の動きがどうなるか、実装したコードを実行して、確かめるしかありません。

今回の例では、データ型を調べるために、「as演算子」と「if let ... as?」の2つを用いました。それらが実際にはどう動くのか、実行して確かめる必要があります。このような作業を、私は開発ツールの機能確認と呼んでいます。

その結果ですが、機能拡張のライブラリ化の投稿でも書いたとおりです。2つの記述方法とも、データ型が一致しているか調べる動作ではなく、値が代入可能かを調べる動作と判明しました。

期待どおりの動作結果でなかった場合には、そのことをソースコードに残しておく必要があります。今回の例では、該当するソースコードの箇所に、以下のようなコメントを残しました。

// 実際の実行結果などはコメントで残す(コメント部分のみ抜粋)

    // データ型の一致をチェックする機能の、重要な注意点
    // is演算子、if let ... as?のどちらも、代入可能かで判定される実験結果になった
    // Int, Float, Boolの全部の組み合わせで、相互に代入可能。そのため、同じデータ型と判定される
    // 上記2種類の処理は、データ型が同じかではなく、代入可能で判定しているのだろうか?
    // この実行結果は、Xcode 6.1.1、SDK iOS8.1にて確認

このように実行環境まで含め、少し長めのコメントになります。時間が経つと忘れがちな内容なので、細かく書いておくほど、後から助かるでしょう。

 

機能確認が終わり、その動作で問題があれば修正しなければなりません。逆に、その動作でも構わなければ、本格的なテストに移ります。テストの話は、長くなるので省略します。テストが終了すれば、機能的には出来上がりです。

私の場合は、iOS実験専用アプリを使って開発することが多く、テストのソースコードも、そのアプリに含めて保存します。定期的にバックアップしていますから、テストも含めて安全に保存されています。

最後の作業は、ソースコードの整理です。「検討すべき点」などの開発用のコメント行を削除して、仕上がりにふさわしいソースコードとして整えます。余計なコメントは削除して、必要最小限のコメントだけを残します。

私の書き方の特徴でもあるのですが、「ファイルの読み書き」や「値の取得(4種類)」といった、クラス内の区切り用のコメント行は削除しません。それが付いていたほうが、ソースコードの区切りが明確になって、時間が経ってから見たときに、内容を把握しやすくなります。とりあえず残すという位置付けではなく、絶対に必要な区切り線として位置付けていて、全部のソースコードに付けています。ただし、このブログに投稿するときは、ほとんど削除していますが。

 

ここで紹介したのは、ソースコードが1つだけの例です。複数のソースコードを作る場合でも、基本的な考え方は同じで、それぞれに「検討すべき点」などを作ります。

また、全体に関しての「検討すべき点」は、どれか1つのソースコードを選んで、それだけに「検討すべき点(全体編)」を付けます。もちろん使い方は同じです。

 

以上が、私が普段使っている開発方法です。1つのソースコードに、実際の命令ソースコードだけでなく、設計途中のメモをコメントとして加え、それを更新しながら、コーディングもやや並行気味に行ないます。コメントしたメモを命令コードに書き換え、コメントが減りながら命令コードが増えるという形です。最後は、最低限のコメントと、完成した命令コードが残ります。

ソースコードをやや並行気味に書いていますが、一番大事なのは、きっちりと設計すること。命令コードが大枠しかない段階、つまり全体を見渡しやすい段階で、空想実行しながら機能を深く掘り下げて、設計の質を高めることです。この段階で、さまざまな空想実行ができると、良いソースコードが作れます。意地悪な順序で実行させたり、無茶な引数を与えたり、いろいろと空想実行します。ただし、何にでも対応すると、余計なコードが増えますから、対応はほどほどにすべきでしょう。

別な面でも、ソースコードには気を配ります。区切り用のコメントで全体を見やすく整え、開発途中で気付いた注意点などもコメントとして残します。数年後の完全に忘れた頃に見ても、困らないようなソースコードになるようにも配慮します。将来の自分への親切ですね。これは、意外に大事です。

設計や実装の作業は、個人的な好みの部分が大きいでしょう。ここで紹介した方法は、その理由も説明しました。もしヒントとして利用できそうな部分があれば、自分なりのアレンジを加えながら、オリジナルの開発手順を作ってみてください。開発効率の向上だけでなく、出来上がったソースコードの質が上がるかも。

2014年12月28日日曜日

環境設定をライブラリ化(使用例編)

環境設定機能をライブラリ化する話の続きです。出来上がったSwiftコード(PrefControllerクラス)の使い方を紹介します。難しい点はないので、ちょっとした注意点が中心です。

 

アプリで使い始めるには、最低限の準備が必要です。環境設定値を保存するファイル名、バージョン番号、個々の環境設定値に使うキー、それぞれのデフォルト値です。デフォルト値はDictionaryとして、その他はStringとして用意します。具体的なSwiftコードは、次のようなものです。見やすくするために、環境設定値が4つ(扱えるデータが4種類なので)と少なめです。

// 環境設定クラスを使う(準備)
...
// 環境設定機能の変数
var zPref : PrefController!
// 環境設定のファイル名とバージョン番号
let PREF_FNAME : String = "sample_pref.plist"
let PREF_VER_NUM : String = "010001"
// 環境設定キーの定義
let PREF_KEY_SNmae_S : String = "ShopName"
let PREF_KEY_SId_I : String = "ShopId"
let PREF_KEY_GRank_B : String = "GoldRank"
let PREF_KEY_TRate_F : String = "TargetRate"
// 環境設定のデフォルト値
let dictPref : Dictionary<String,AnyObject> = [
    PREF_KEY_SNmae_S : "○○屋",
    PREF_KEY_SId_I : 999,
    PREF_KEY_GRank_B : false,
    PREF_KEY_TRate_F : 0.6 ]
...
// アプリの初期処理の中でインスタンス生成
zPref = PrefController(dictPref, PREF_FNAME, PREF_VER_NUM)
...

それぞれの値を定義した後、アプリの初期処理の中で、PrefControllerクラスのインスタンスを生成します。これで準備が整いました。もし初期設定ファイルが保存してあり、バージョン番号が同じであれば、デフォルト値が更新されます。

zPrefというのは、私なりの使い方です。zDateなどもあり、共通ライブラリのインスタンスを生成する際、アプリのどこからでも使える変数として用意します。この変数を通して、クラスのメソッドを利用します。

この準備で重要なのは、環境設定キーの定義です。以降の処理でも、ここで定義したキーを使います。もし定義値を使わずに環境設定値にアクセスすると、文字列のリテラルを指定することとなって、間違ったリテラルを指定しても、エラーかどうか分かりません。実行時に間違いだと気付くことになります。そうならないために、ここで定義した値を使い、コーディング時点でエラーを発見するわけです。決まったリテラル文字列を使う際に、実行時ではなくコンパイル時にエラーを発見するための、よく使われる工夫ですね。

定義に使うキーの変数名にも注目です。ここでは、変数名の最後の1文字で、データ型を表しています。「S, I, B, F」は、それぞれ「String, Int, Bool, Float」を意味してます。データ型が間違っていないか、変数名で注意しながらコーディングできます。

変数名が長くなるとか、データ型を含む変数名を間違えるとか、心配する必要はありません。Xcodeのテキストエディタには、入力支援機能があり、定義した変数も出てきます。「PREF」とタイプしたあたりで、一連の定義値名が並んでいるはずです。それを選んで入力すれば、データ型付きの変数名も間違えずに入力可能です。

 

環境設定の準備ができたので、いよいよ使う段階です。環境設定値の関係する個々の処理で、取得メソッドを用います。次のSwiftコードのように。

// 環境設定クラスを使う(個々の処理で値を取得)
...
let iShopId : Int = zPref.getPrefF(PREF_KEY_SId_I)
...
if zPref.getPrefF(PREF_KEY_GRank_B) {
    ...
}
...
let iRate : Float = zPref.getPrefF(PREF_KEY_TRate_F)
let iAmountLevel : Float = StdAmount * iRate
...

取得メソッドを使うときは、データ型の指定に注意を払わなければなりません。同じメソッド名で、戻り値の異なる4つがあります。そのどれが選ばれるのか、特定できる形で使わないと、余計なエラーを生んでしまいます。

基本的には、型を指定した変数に入れるということです。型が指定されていることで、間違ったメソッドが呼ばれることはありません。

変数以外でも、データ型が特定できる場合があります。たとえば、if文です。条件式はBool型と決まっているため、そこに参照メソッドを入れると、戻り値がBool型に特定されます。また、関数やメソッドの引数でも、型が特定されているので、同じように正しいメソッドが選ばれます。

計算式の場合は、少し注意が必要です。計算に使われるリテラル値や変数の型によって、特定しづらい場合もあります。そのようなときは、取得メソッドの値をいったん変数に入れてから、計算で使うほうが安全でしょう。

 

環境設定の値は、アプリごとに専用画面を用意して、ユーザーが変更可能に作ります。その処理では、すべての環境設定値を取得し、画面に表示します。ユーザーが画面上で変更した後、新しい値を受け取って設定メソッドで更新し、ファイルに保存する流れとなります。たとえば、次のようなSwiftコードに。

// 環境設定クラスを使う(環境設定画面)
...
// 環境設定値の取得
let iShopName : String = zPref.getPrefF(PREF_KEY_SNmae_S)
let iShopId : Int = zPref.getPrefF(PREF_KEY_SId_I)
let iGoldRank : Bool = zPref.getPrefF(PREF_KEY_GRank_B)
let iRate : Float = zPref.getPrefF(PREF_KEY_TRate_F)
...
// 環境設定値のファイルへの保存
zPref.setPrefF(PREF_KEY_SNmae_S, iShopName)
zPref.setPrefF(PREF_KEY_SId_I, iShopId)
zPref.setPrefF(PREF_KEY_GRank_B, iGoldRank)
zPref.setPrefF(PREF_KEY_TRate_F, iRate)
zPref.writePrefFileF()
...

これは、あくまで一例です。すべての環境設定値を更新する必要はありません。更新が必要な環境設定値だけ更新するのが、一般的でしょう。

 

バージョン番号の使い方にも、少しだけ触れます。ここで用いるバージョン番号は、String型です。文字列ですから、何でも構わないわけです。でも、やっぱり上手に使う方法はあります。

1つは、意味を持たせることです。ここに示した例では、6桁の数字を用いました。「010001」です。これは、バージョン「1.0.1」を意味していて、それぞれの数値に2桁を与えて文字列化したものです。もちろん「01.00.01」でも「1.0.1」でも構いません。好きな形を使いましょう。

もう1つの工夫は、アプリのバージョンに合わせる方法です。アプリのバージョンアップで、環境設定値も必ずバージョンアップするとは限りません。そうではなく、バージョンアップしないほうが多いでしょう。アプリの最新のバージョン番号ではなく、環境設定値を最後にバージョンアップしたときのアプリのバージョン番号を、環境設定のバージョン番号として用いるという考え方です。どのバージョンから同じなのか、明確に分かる利点があります。

ちなみに私の場合ですが、アプリのバージョン番号とは関係なく、環境設定独自のバージョン番号を付けています。特に大きな理由はありませんが、関係を気にしないほうが自由に付けられてよいと思っただけです。

 

以上で、環境設定機能のライブラリ化の話は終わりです。

今までは、アプリごとに環境設定クラス(ファイル名:MC_Pref.swift)を用意していましたが、これからは共通ライブラリ(ファイル名:BC_Pref.swift)をアプリに追加して、キーやデフォルト値を定義すれば済みます。

環境設定変更用の画面作りは同じですが、手間の何割かは減ると思います。独自ライブラリの種類が増えるほど、アプリ開発の手間が減りますね。

 

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

2014年12月26日金曜日

環境設定をライブラリ化(本編)

環境設定機能をライブラリ化する話の続きです。大まかな仕様が決まったので、実際のソースコードを作る段階です。小さなクラスですから、手短に紹介します。

 

まず、ファイル名を決めます。どのアプリでも使える共通ライブラリで、Swiftのクラスとして作りますから、ファイル名の先頭は「BC」しかありません。環境設定を短い言葉で表すと「Pref」なので、ファイル名は「BC_Pref.swift」としました。このファイルを、いつものようにiOS実験専用アプリへ追加して作ります。

最初は、入れ物となるクラスですね。クラス名は、とりあえず「PrefController」としてみました。このブログでも紹介した、日付を扱うクラス「DateController」と同じ、自作の「Controller」シリーズの1つです。

// 環境設定機能のクラス
class PrefController {
    let PREF_KEY_VER : String = "Version"

    var dictPref : Dictionary<String,AnyObject> = [String:AnyObject]()
    var prefFName : String = "dummy.plist"
    var strVersion : String = ""

    // 初期化
    init(_ rDictPref:Dictionary<String,AnyObject>, _ rFName:String, _ rVer:String) {
        dictPref = rDictPref
        prefFName = rFName
        strVersion = rVer
        // 環境設定ファイルの存在を調べて、同じバージョンがあれば入れ替え
        readPrefFileF()
    }
}

キー付きの値を入れるので、入れ物としてSwiftのデータ型Dictionaryを使います。当然、キーのデータ型はStringです。値のほうが4種類あるので、データ型はAnyObjectとなり、Dictionaryに入れるデータの型は「String,AnyObject」としました。

初期化の処理では、デフォルトの環境設定値(データ型はDictionary)と、環境設定ファイル名、バージョンを示すStringの3つを受け取って、それぞれ変数へ設定します。その後、以前に保存された環境設定ファイルを読み込む関数(後述)を呼び出します。

なお、バージョン番号を示すString型の変数rVerの使い方は、このライブラリを使うときに説明します。

 

続いて、環境設定ファイルの読み込みメソッドと、書き出しメソッドです。具体的なSwiftコードは、次のとおりです。

// ファイル関係の処理
// ファイル名を含んだ、Docフォルダのパスを生成
private func getFilePathDocsF(rFName:String) -> String {
    let iPathDocs : AnyObject = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
    let iPathFile : String = iPathDocs[0].stringByAppendingPathComponent(rFName)
    return iPathFile
}
// 環境設定ファイルを読み込む
func readPrefFileF() {
    // 環境設定ファイルの存在を調べる
    let iPathFile : String = getFilePathDocsF(prefFName)
    let iFileMgr : NSFileManager = NSFileManager.defaultManager()
    if !(iFileMgr.fileExistsAtPath(iPathFile)) { return }
    // ファイルが存在したので、読み込んでバージョン番号チェックして、同じバージョンなら入れ替え
    var dictPref2 : Dictionary = NSDictionary(contentsOfFile: iPathFile)!
    if let iObject : AnyObject = dictPref2[PREF_KEY_VER] {
        if let iStrVersion : String = iObject as? String {
            if iStrVersion == strVersion {
                dictPref = dictPref2 as Dictionary
            }
        }
    } else {
        println("ERROR:PRF_RPF:内部エラー:ファイルにVer情報なし")
    }
}
// 環境設定ファイルを書き出す
func writePrefFileF() {
    // Dicに、バージョン番号を付加(あれば上書き)
    dictPref[STR_KEY_VER] = strVersion
    // ファイルへ書き出す
    let iPathFile : String = getFilePathDocsF(prefFName)
    let iResult : Bool = (dictPref as NSDictionary).writeToFile(iPathFile, atomically: true)
    if !iResult { println("ERROR:PRF_WPF:ファイル書き失敗") }
}

最初のパス生成関数は、両方のメソッドで使う共通処理です。重複して書かないようにと、1つにまとめました。

環境設定ファイルを読み込むreadPrefFileFメソッドでは、ファイルが存在するか最初に調べています。存在しなければ終了し、存在すれば読み込んでいます。

Dictionaryとして読み込んだ内容に、バージョン番号の値があるか調べています。このような場合、Swiftではif-let文が便利です。成功すれば変数に値が入り、失敗すればエラーメッセージを出します。正しく使っていれば、存在しないはずはないのですが、念のために入れておいただけです。

続いて、同じくif-let文で、バージョン番号のStringを変数に入れています。しつこいですが、これも念のためです。最後に、アプリ側のバージョン番号と比較して、同じ場合には、環境設定値の変数に入れます。つまり、最初に持っていたデフォルト値に上書きするというわけです。

このような作り方だと、デフォルト値に戻す機能は作れません。しかし、Documentsフォルダにある環境設定ファイルを削除してから、アプリを起動すれば、いつでもデフォルト状態に戻せます。この使い方が可能なので、ここでは上書きする方法を選びました。

次は、環境設定ファイルを書き出すwritePrefFileFメソッドです。最初に、バージョン番号を付け加えています。もし既にバージョン番号がある場合もありますが、わざわざチェックする処理が面倒なので、追加または上書きのどちらも構わないとしました。この方法が、もっとも単純な処理内容となりますので。

ファイルへの書き出しでは、DictionaryをNSDictionaryにキャストして、書き出すメソッドを呼び出しています。書き出しに失敗した場合は、エラーメッセージを出しています。失敗したとしても、メッセージを出すぐらいしかできませんね。

書き出しのメソッドでは、Dictionaryの変数の値を、そのまま書き出しています。後述する設定値の変更メソッドが、Dictionaryの変数の中身を更新し、その結果を書き出すという形にしたからです。

 

いよいよ、環境設定値を取得するメソッドです。データ型が4種類あるので、メドッソも4つ用意します。コンパイラーの型チェックを利用するためです。メソッド名は同じまま、戻り値のデータ型が異なるメソッドとなります。Swiftコードは、次のとおりです。

// 環境設定値の取得
func getPrefF(rKey:String) -> Int {
    let iStrId : String = "ERROR:PRF_GPI:"
    if checkGetPrefF(1, iStrId, rKey) {
        return dictPref[rKey] as Int
    }
    return -999999      // エラー時の値
}
func getPrefF(rKey:String) -> Float {
    let iStrId : String = "ERROR:PRF_GPF:"
    if checkGetPrefF(2, iStrId, rKey) {
        return dictPref[rKey] as Float
    }
    return -999999.9    // エラー時の値
}
func getPrefF(rKey:String) -> String {
    let iStrId : String = "ERROR:PRF_GPS:"
    if checkGetPrefF(3, iStrId, rKey) {
        return dictPref[rKey] as String
    }
    return "ERROR!!!"  // エラー時の値
}
func getPrefF(rKey:String) -> Bool {
    let iStrId : String = "ERROR:PRF_GPB:"
    if checkGetPrefF(4, iStrId, rKey) {
        return dictPref[rKey] as Bool
    }
    return false       // エラー時の値
}
// エラーメッセージ
let MSG_ERR_TYPE : String = "ではない"
let MSG_ERR_KEY : String = "未登録キー"
let MSG_ERR_EMPTY : String = "Dicが空"
// 使い方が間違ってないか、チェックする共通関数
// チェック内容:Dicの要素がゼロ?、キーが存在するか?、値の型が正しいか?
private func checkGetPrefF(rNum:Int, _ rStrId:String, _ rKey:String) -> Bool {
    if !(dictPref.isEmpty) {
        if let iObject : AnyObject = dictPref[rKey] {
            switch rNum {
            case 1:
                if let iInt : Int = iObject as? Int {
                    return true
                } else {
                    println(rStrId + "Int" + MSG_ERR_TYPE)
                }
            case 2:
                if let iFloat : Float = iObject as? Float {
                    return true
                } else {
                    println(rStrId + "Float" + MSG_ERR_TYPE)
                }
            case 3:
                if let iStr : String = iObject as? String {
                    return true
                } else {
                    println(rStrId + "String" + MSG_ERR_TYPE)
                }
            case 4:
                if let iBool : Bool = iObject as? Bool {
                    return true
                } else {
                    println(rStrId + "Bool" + MSG_ERR_TYPE)
                }
            default: break
            }
        } else {
            println(rStrId + MSG_ERR_KEY)
        }
    } else {
        println(rStrId + MSG_ERR_EMPTY)
    }
    return false
}

4つのメソッドは、戻り値の型が違うだけで、処理の流れなどは同じです。どの部分で切り分けて共通化するのか迷いましたが、上のような形にしました。呼び出す側が1〜4の番号を指定して、switch文でデータ型を振り分けています。いろいろ悩んだのですが、この形が一番シンプルではないかと思いました。

ソースコードのコメントで書いているように、チェック内容は次の3つです。Dictionaryが空かどうか、指定したキーが存在するかどうか、指定した値の型が前の値の型と一致しているかどうかです。このうち後者2つで、if-let文を利用しています。変数への代入が成功すれば、チェックが通ったという処理です。

一番悩んだのは、エラー発生時の戻り値です。nilを返すという方法もありますが、おそらく呼び出し側で異常終了することになるでしょう。それが嫌いなので、データ型の値の中で、ありそうもない値を設定してみました。Boolは無理なので、falseにしましたが。こうすると異常終了しない可能性が高まります。エラーメッセージが出るので、それで気付くはずですから。このような部分は、作る人の好きずきなので、自分の作りたい形で構わないと思います。

3種類のエラーチェックのうち、データ型のチェックだけは完全ではありません。その辺の話は、最後に書きます。

 

最後は、環境設定の値を設定するメソッドです。こちらも戻り値のデータ型ごとに用意するので、メソッド名が同じまま4つあります。全体的には取得メソッドと似ていて、次のようなSwiftコードにしました。

// 環境設定値の設定
func setPrefF(rKey:String, _ rInt:Int) {
    if checkSetPrefF(1, rKey, "ERROR:PRF_SPI:") { dictPref[rKey] = rInt }
}
func setPrefF(rKey:String, _ rFloat:Float) {
    if checkSetPrefF(2, rKey, "ERROR:PRF_SPF:") { dictPref[rKey] = rFloat }
}
func setPrefF(rKey:String, _ rStr:String) {
    if checkSetPrefF(3, rKey, "ERROR:PRF_SPS:") { dictPref[rKey] = rStr }
}
func setPrefF(rKey:String, _ rBool:Bool) {
    if checkSetPrefF(4, rKey, "ERROR:PRF_SPB:") { dictPref[rKey] = rBool }
}
// キーの存在を確認する共通関数
private func checkSetPrefF(rNum:Int, _ rKey:String, _ rMsgId:String) -> Bool {
    if let iObject : AnyObject = dictPref[rKey] {
        switch rNum {
        case 1:
            if iObject is Int {
                return true
            } else {
                println(rMsgId + "Int" + MSG_ERR_TYPE)
            }
        case 2:
            if iObject is Float {
                return true
            } else {
                println(rMsgId + "Float" + MSG_ERR_TYPE)
            }
        case 3:
            if iObject is String {
                return true
            } else {
                println(rMsgId + "String" + MSG_ERR_TYPE)
            }
        case 4:
            if iObject is Bool {
                return true
            } else {
                println(rMsgId + "Bool" + MSG_ERR_TYPE)
            }
        default: break
        }
        return false
    } else {
        println(rMsgId + MSG_ERR_KEY)
        return false
    }
}

更新する前に読み込むはずなので、Dictionaryが空かどうかのチェックは含めませんでした。指定したキーが存在するかどうかと、指定した値の型が前の値の型と一致しているかを調べています。どちらもOKのときだけ値を更新し、それ以外ではエラーメッセージを表示します。

取得および設定のメソッドで、メッセージの言葉は共通です。文字列を定義して、間接参照する形で作っています。また、データ型ごとにエラーメッセージIDが異なるため、どのメソッドでエラーが出たのかも分かるようになっています。

 

後回しにした、データ型のチェックの話です。取得メソッドでは、「iInt : Int = iObject as? Int」のように、AnyObjectをキャストして代入を試みています。この方法だと、Int、Float、Boolのすべての組み合わせで、それぞれが代入できてしまいます。厳密には、エラーチェックとして機能していません。相手がStringのときだけ、エラーになります。代入するという方式がダメなのかと思いましたが、実はそうではなかったのです。

もう片方の設定メソッドでは、「if iObject is Int」のように、is演算子を用いて型チェックをしています。iObjectのデータ型はAnyObjectです。いろいろなデータ型の組み合わせを試しに実行してみたら、面白い結果となりました。iObjectに入れたデータ型がInt、Float、Boolのどれかであれば、「is Int」も「is Float」も「is Bool」も、すべてtrueになるのです。if-letによる代入と、同じ判定結果となりました。

今のところAnyObject型しか試していませんが、AnyObject型の変数に入れると、Int、Float、Boolのすべてでtrueになるようです。データ型を調べるというよりは、代入可能かどうかを調べるという仕様なのかもしれませんね。

とりあえずは、この形で行くことにしました。代入可能ということは、間違った使い方の一部を発見できなくなりますが、代入できずに異常終了することは避けられますから。

 

実際に作ってみると、最初に予想したよりは長いSwiftコードになってしまいました。やはり、処理の流れが同じでも、データ型が違うものを共通化するのは、シンプルには作りづらいということでしょう。何でも入れられる変数として作れば簡単ですが、それだと型チェックのメリットが消えます。少しぐらい長くなっても、できるだけ良い形のライブラリとして仕上げたいですからね。

なお、ここに掲載したSwiftコードは、私の使っているものと少し異なります。私のものは、ファイルの読み書きの部分が、共通ライブラリを呼び出す形になっています。今回の掲載では、ライブラリの呼び出し部分を、通常のファイルの読み書きに書き換えました。それにより、別なコードに依存しないコードになっています。

以上で、Swiftコードの紹介は終わりです。長くなりましたので、使用例は次回の投稿で。

 

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

2014年12月24日水曜日

環境設定をライブラリ化(全体設計編)

Swiftでの開発にも慣れてきたので、今まで作ったアプリの機能のうち、ライブラリ化できそうなものを選んで、独自ライブラリの充実を進めています。最近も、新しい機能をライブラリ化しました。どのアプリにも必ず付ける、環境設定の機能です。どのように仕上がったのか、その過程も含めて紹介します。

 

いつものように全体設計から始めます。つまり、大まかな仕様を決めてから、作り始めるという私なりの手順です。まずは、過去に作った環境設定の話から。

今までの環境設定は、どのアプリでも、1つのクラスとして作りました。仕様はほぼ共通です。環境設定のデフォルト値を持ち、アプリの初期状態では、その値を使います。環境設定の画面も用意し、その画面で設定を変更すると、設定値をファイルに保存します。

アプリを起動したときは、環境設定ファイルの存在を調べ、それがあればファイル内の値を採用します。逆にファイルが存在しなければ、デフォルトの値を採用します。

ただし、環境設定の値の中には、バージョン番号も含まれます。アプリのバージョン番号ではなくて、環境設定のフォーマットのバージョン番号です。アプリのバージョンが上がっても、環境設定に含まれる項目が同じなら、環境設定のバージョン番号は同じままです。当然、環境設定のフォーマットが変わると、環境設定のバージョン番号が変わります。

アプリの起動時に、環境設定ファイルを読み込みますが、読み込み後には環境設定バージョンもチェックします。アプリ側でもデフォルトの環境設定値を持ち、それにもバージョン番号が含まれます。両者のバージョン番号を比べて、もし一致していなければ、環境設定ファイルは無視されて、デフォルトの環境設定値が使われます。

環境設定がバージョンアップしたとき、環境設定値の移行は行なっていません。最初の頃はやっていたのですが、使っているユーザー数が少ないので、手動で再設定してもらっても文句を言われないと判明しました。無駄な開発はしたくないので、今はやらなくなりました。

環境設定ファイルの保存場所は、アプリのDocumentsフォルダで決まりです。ここに入れておくとiTunesから見れて、手動でもバックアップが取れます。試用段階で使った環境設定ファイルを、そのまま本番用にコピーすることもでき、本番時に環境設定をやらなくて済むメリットもあります。このような方法が使えるのも、設定値の移行処理が不要と判断した理由です。

以上のような形が、私に必要な環境設定の形です。バージョン番号を持ち、ファイルに保存でき、バージョン番号の古い環境設定ファイルは無視するという機能が必要です。

 

続いて、環境設定に含める値の種類です。とりあえず、Int、Float、String、Boolの4種類があれば大丈夫です。複数の値をセットで扱うグループ分けも、使う際のグループ指定が面倒そうなので、なくて構いません。

それぞれの値に、キーは必要です。キーを文字列で指定して、値を設定したり、保存値を得たりする使い方が良いでしょう。キーごとに値の種類を決めて使うことになりますが、データ型をわざわざ指定するような形ではなく、入れた値で自動判定するような使い方がベストです。

ただし、使う際のチェックは多めに入れたいと思います。間違ったキーを指定したとき、保存されているのと違うタイプのデータ型を要求したとか、環境設定値が空なのに読みにいったとか、使い方の間違いを知らせてくれる機能は付けましょう。

Swiftの魅力の1つでもある、コンパイラーによる型チェックを有効に使うことも、非常に重要な点です。環境設定機能のように、異なるデータ型を扱える処理の場合は、何でも入れられるデータ型を1つ用意して、それを全種類のデータに使いたくなります。そうすると、コンパイラーによる型チェックが使えず、実行時にエラーが発生しがちです。そうならないように、データ型を指定したメソッドを用意して、意識的に使い分ける形にします。

 

以上のような環境設定機能を、Swiftのクラスとして作ります。必要なメソッド整理すると、次のようになります。

環境設定クラスの大まかな構成
・初期化メソッド:保存ファイル名やデフォルト値を渡して初期化
・ファイル保存メソッド:設定値をファイルに保存(バージョン番号の含めて)
・ファイル読込メソッド:ファイルから設定値を読み込む(バージョン番号の確認も)
・環境設定値の取得(データ型ごとに作る):指定されたキーの設定値を返す
・環境設定値の設定(データ型ごとに作る):指定されたキーの設定値を更新(追加は不可)

環境設定値の設定では、新しいキーを途中で追加することはありません。最初から種類が決まっていますから、全種類を含めたデフォルト値を最初に用意して、あとは値を更新するだけです。

これらの機能は、かなり簡単に作れるでしょう。一番大事なのは、エラーチェックです。今まで挙がったものを整理すると、次のようになります。

取得と設定でのチェック内容
・環境設定が空かどうか
・指定されたキーが存在するかどうか
・取得する既存データの型が、使われたメソッドの返り値の型と一致しているか
・更新対象となる既存データの型が、更新するデータの型と一致しているか

環境設定を使い始めるときは、使い勝手を重視して、型の指定を特にはしません。そのため、環境設定値を取得したり更新するときには、既存の環境設定値が持っているデータ型を調べて、違ってないかチェックする形にします。アプリから呼び出されて使われるライブラリですから、どんな操作でも勝手にされるわけではありません。あくまで、間違った使い方を発見するためのチェックですから、ほどほどのチェック内容にしておき、使い勝手を優先したほうが良いと判断しました。

一応、環境設定のデータ構成も。あえて書くと、次のような形になります。

環境設定のデータ構成(以下のペアを何組でも)
・キー:String
・値:Int,Float,String,Boolの4種類の中から1つだけ

使い勝手を優先したため、キーごとにデータ型を指定する項目は含まれません。これだけでも、実用的には十分だと思います。

 

ここまで整理できたので、あとは作るだけです。大まかな仕様を見る限り、コンパクトにまとまったクラスになると予想できます。次回の投稿で、具体的なSwiftコードを紹介します。

2014年12月21日日曜日

オフスクリーン描画のライブラリ化(拡張編)

このブログは、私の友人も見ています。先日紹介し終わった描画ライブラリを見て、さっそく意見をもらいました。使う方法が少し面倒でも構わないから、もっと機能を増やしたバージョンも作って紹介したらどうかと。

突っ込んで尋ねると、友人自身は既に自分で改良し、使い始めているとのこと。だったら、紹介文を書いてほしいと依頼しましたが、文章を書くのが苦手なのに加え、説明が下手なので勘弁してほしいとのことでした。

まあ、あまり手間をかけずに改良できそうなので、試しに作ってみました。改良前と異なる部分だけ、簡単に紹介します。

 

まずは、友人が示した要望を列挙します。言われたのは、次のようなことです。1つのインスタンスで何度も描画できること。つまり、描画の開始と終了を何度でも繰り返せること。何回か描画を繰り返す場合、登録した色を継続して使えること。逆に、登録した色をクリアーして、新しく描き始めることもできること。描画途中の状態で、何度でもUIImageを取り出せること。つまり、UIImageを取り出すときに描画を終了しないこと。

この要望を上手に整理すれば、少しの手直しだけで、要望を実現できそうです。一番大事なのは、色の登録と描画の開始終了を、別々に行なえることでしょう。

まずは、色の登録です。描画の開始終了と分離するわけですから、開始時点に色数を指定した方法が使えなくなります。また、途中でのクリアー処理も必要となります。メソッド数は増やしたくないので、クリアー処理を追加し、残りは既存の色数追加メソッドを用いることにしました。クリアーした時点では色数がゼロになり、必要な色数だけ追加メソッドで加えれば問題ありません。

続いて、描画の開始と終了です。何度も繰り返すわけですから、それぞれメソッドを用意します。現状では、インスタンス生成時に描画を開始しています。これをどうするかです。仕様をスッキリさせるために、インスタンス生成時には何もしないことにしました。生成後に、描画開始のメソッドを実行する必要があります。

その他の構成は、既存のメソッドのままで大丈夫そうです。当然、メソッド内部の処理は変更しますが、メソッドの構成は変えなくても問題ないでしょう。まとめると、次のようなメソッド構成になります。

描画ライブラリ(拡張版)のメソッド
・描画の開始(イメージの大きさを指定)
・描画の終了
・色(コンテキスト)登録のクリアー
・色(コンテキスト)の追加
・色(コンテキスト)の設定(線色)
・色(コンテキスト)の設定(塗りぶつし色)
・UIImageの取得
・各種描画(線、長方形、文字列など)

どのメソッドも1つずつ作りますが、描画メソッドだけは描画の種類ごとに作ります。

 

ライブラリの質を高めるためには、エラーチェックが欠かせません。細かな値のエラーチェックよりも、呼び出してはいけない条件で呼び出されたとき、エラーで処理するようなチェック仕様が大事です。

このライブラリには、2つの状態があります。描画可能な状態と、描画不可能な状態です。もちろん最初は描画不可能な状態で、描画開始メソッドにより描画可能状態となり、描画終了メソッドにより描画不可能状態に戻ります。開始と終了以外のメソッドを、2つの状態との関係で1つずつ見ていきましょう。

色登録のクリアーメソッドは、描画中に実行されては困ります。実行可能なのは、描画不可能状態のみとしましょう。これにより、描画が終了した後でクリアーするという、使い方の制限が生まれます。

色の追加メソッドは、描画中(描画可能状態)でも、描画不可能状態でも使えなければなりません。描画状態のチェックはしないこととします。追加する色数の値チェックだけは必要ですね。

2種類の色の設定メソッドはどちらも、描画状態に関係なく使えたほうが便利でしょう。描画状態のチェックはしません。ただし、指定した色番号が登録数の範囲内かだけは、チェックが必要です。

UIImageの取得メソッドは、当然ながら描画状態でないと使えません。チェックが必要です。

各種描画メソッドも、描画状態でないと使えません。これにも、チェックが必要です。また、色番号を指定するメソッドでは、登録数の範囲内かチェックする必要があります。

整理すると、以上のような形となりました。あとは、この仕様に合わせて、実際のSwiftコードを修正するだけです。

 

それぞれのメソッドの細かな説明は、以前に書いた投稿に含まれています。ここでは、修正後のソースコードを載せるだけで十分でしょう。次のようなSwiftコードになりました。

// オフスクリーン描画の機能拡張版
enum DrawType : Int {
    case None = 0
    case Stroke = 1
    case Fill = 2
    case Both = 3
}

class DrawImageOffScreen {
    // 描画可能フラグ
    var enabledDraw : Bool = false
    // 色のデフォルト値
    let COLOR_DFAULT_STROKE : [CGFloat] = [0.0, 0.0, 0.0, 1.0] // 黒で不透明
    let COLOR_DFAULT_FILL : [CGFloat] = [1.0, 1.0, 1.0, 1.0] // 白で不透明
    // CGContextに設定する値の入れ物
    var numContext : Int = 0
    var arySColor : [[CGFloat]] = [] // StrokeColor
    var aryFColor : [[CGFloat]] = [] // FillColor

   // =================================== 描画の開始と終了
     // 描画の開始
    func startDrawF(rWidth:CGFloat, _ rHight:CGFloat, _ rOpaque:Bool) {
        if enabledDraw { println("ERROR:DIOS_SP:Disabled"); return }
        // オフスクリーン描画の開始(rOpaqueがfalseで背景が透明に,scaleが0.0で自動)
        let iCGSize : CGSize = CGSizeMake(rWidth, rHight)
        UIGraphicsBeginImageContextWithOptions(iCGSize, rOpaque, 0.0)
        enabledDraw = true
    }
    // 描画の終了
    func stopDrawF() {
        if !enabledDraw { println("ERROR:DIOS_SP:Disabled"); return }
        // 描画の終了
        enabledDraw = false
        UIGraphicsEndImageContext()
        return
    }
    // =================================== コンテキスト管理
    // コンテキストの初期化
    func clearContextF() {
        if enabledDraw { println("ERROR:DIOS_AC:Disabled"); return }
        numContext = 0
        arySColor = []
        aryFColor = []
    }
    // 指定した個数のコンテキストを追加する
    func addContextF(rNumAdd:Int) {
        if (rNumAdd < 1) { println("ERROR:DIOS_AC:rNumAddが小さすぎ"); return }
        numContext += rNumAdd
        for _ in 1...rNumAdd {
            arySColor.append(COLOR_DFAULT_STROKE)
            aryFColor.append(COLOR_DFAULT_FILL)
        }
    }
    // コンテキストの線色を設定する
    func setContextStrokeColorF(rNum:Int, _ rRed:CGFloat, _ rGreen:CGFloat, _ rBlue:CGFloat, _ rAlpha:CGFloat) {
        if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_SC:rNumが範囲超え"); return }
        arySColor[rNum][0] = rRed
        arySColor[rNum][1] = rGreen
        arySColor[rNum][2] = rBlue
        arySColor[rNum][3] = rAlpha
    }
    // コンテキストの塗りつぶし色を設定する
    func setContextFillColorF(rNum:Int, _ rRed:CGFloat, _ rGreen:CGFloat, _ rBlue:CGFloat, _ rAlpha:CGFloat) {
        if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_FC:rNumが範囲超え"); return }
        aryFColor[rNum][0] = rRed
        aryFColor[rNum][1] = rGreen
        aryFColor[rNum][2] = rBlue
        aryFColor[rNum][3] = rAlpha
    }
    // =================================== 描画結果を返す
    // 描画内容をUIImageとして返す
    func getImageF() -> UIImage! {
        if !enabledDraw { println("ERROR:DIOS_GI:Disabled"); return nil }
        // 描き終わった内容をUIImageに保存する
        let iImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()
        return iImage
    }
    // =================================== いろいろな描画(以下省略)
}

図形や文字を描くメソッドは、前と同じままなので掲載を省略しました。前のままコピーして付け加えると、拡張版のクラスとして動作するはずです。

 

当然ですが、使い方も少し変わります。インスタンスを生成した後、何度も描画できます。それぞれの描画処理では、描画を開始し、コンテキストを追加して色を設定し、描画メソッドを繰り返して描き、描き終わったらUIImageを取得して、最後に描画を終了します。以下のSwiftコードは、以前に掲載したものを、拡張版に合わせて直した結果です。

// 架空の整理券を描く(飾りを省いたバージョンを、拡張版に合わせて修正)
var iDraw = DrawImageOffScreen()
// 描画の開始
iDraw.startDrawF(400, 150, false)
// 色ごとに変数を用意する
iDraw.addContextF(10)
let cMain : Int = 0
let cMainL : Int = 1
let cTextDark : Int = 2
let cTextLight : Int = 3
let cTextSp : Int = 4
let cLineStrong : Int = 5
let cLineWeak : Int = 6
let cDecoLine : Int = 7
let cDecoBox : Int = 8
let cDecoCircle : Int = 9
// 色に値を設定する
iDraw.setContextStrokeColorF(cMain, 0.1, 0.1, 1.0, 1.0)
iDraw.setContextFillColorF(cMain, 0.9, 0.9, 1.0, 1.0)
iDraw.setContextStrokeColorF(cTextDark, 0.1, 0.1, 0.1, 1.0)
iDraw.setContextStrokeColorF(cTextLight, 0.5, 0.5, 0.5, 1.0)
iDraw.setContextStrokeColorF(cTextSp, 1.0, 0.0, 0.0, 1.0)
iDraw.setContextStrokeColorF(cLineStrong, 0.1, 0.1, 1.0, 1.0)
iDraw.setContextStrokeColorF(cLineWeak, 0.7, 0.7, 1.0, 1.0)
iDraw.setContextStrokeColorF(cDecoLine, 0.1, 0.1, 1.0, 1.0)
iDraw.setContextFillColorF(cDecoBox, 0.1, 0.1, 1.0, 0.5)
iDraw.setContextFillColorF(cDecoCircle, 0.3, 1.0, 0.3, 0.5)
// ここから描画内容
let iNumTicket : String = "FN-579-413"
let iNumMember : String = "GCG-40167"
// 描画:整理券情報
iDraw.drawRectF(cMain, .Both, 1.0, 10.0, 10.0, 170.0, 60.0)
iDraw.drawTextF(cTextDark, "整理番号:", 16.0, 15.0, 13.0)
iDraw.drawTextF(cTextDark, iNumTicket, 30.0, 15.0, 33.0)
// 描画:タイトルと日付
iDraw.drawTextF(cTextDark, "入場整理券", 16.0, 200.0, 35.0)
iDraw.drawTextF(cTextDark, "  日付:2014年12月20日", 14.0, 200.0, 55.0)
// 中央の区切り線
iDraw.drawLineF(cLineStrong, 3.0, 10.0, 80.0, 380.0, 80.0)
// 描画:会員情報
iDraw.drawTextF(cTextDark, ("会員番号:" + iNumMember), 14.0, 15.0, 90.0)
iDraw.drawTextF(cTextSp, "開催場所:秘密(招待メール参照)", 14.0, 180.0, 90.0)
// 描画:発行人
iDraw.drawTextF(cTextLight, "発行人:ガイガー・カウント・グループ", 12.0, 180.0, 115.0)
iDraw.drawTextF(cTextLight, "watashida@gaigaigai.com", 12.0, 250.0, 130.0)
// UIImageの取得
iImageView20c.image = iDraw.getImageF()
// 描画の終了
iDraw.stopDrawF()

基本的には開始と終了が違っていて、他にも色(コンテキスト)の追加が必要となります。描画メソッドや色設定メソッドなどは、前と同じまま使えます。また、UIImageの取得も、途中で何回でも可能になっています。

何度も描くため、描画の開始メソッドで、イメージの大きさを指定しています。このため、違うサイズのイメージでも、連続して描けるようになりました。

 

使う上で注意しなければならないのは、エラーチェックに引っかからない点でしょう。たとえば、色(コンテキスト)をクリアーするメソッドは、描画中にはエラーになります。描画途中でクリアーされると困るので、そうしたわけです。色をクリアーしたいときは、描画を終了してからクリアーを実行し、新たな色を追加する手順となります。

どのエラーチェックにも意味があり、間違った使い方ができにくいように考えて付けてあります。エラーチェックに引っかからないような使い方が正しい使い方であり、その使い方で困らないはずです。

使い方の例は、これぐらいで大丈夫でしょう。それほど難しい機能は搭載していないので、普通に使えると思います。

 

今回の拡張バージョンですが、私に限っては、前のバージョンの方が使いやすいと思っています。こうした描画ライブラリを使うときは、描画部分を関数として1つにまとめるでしょう。すると、描画する度に、その関数を呼び出す使い方です。連続して何個も描くような使い方は、おそらく生じないと考えられます。関数内に収めるため、拡張版でも構わないといえば構わないのですが、拡張したメリットはほとんどなさそうです。

もちろん、拡張版のような形を望む人もいるでしょう。該当する人は、今回のソースコードを参考にして、自分なりのライブラリを作ってください。

 

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

2014年12月17日水曜日

Xcode 6.1.1を導入しました

できるだけ枯れた開発ツールを使うのが基本方針の私ですが、開発マシンのMacBook Airに、Xcode 6.1.1を導入しました。最大の理由は、Mac用のツールを作るのに、Swiftを使いたかったからです。MacアプリでSwiftが使えるのはXcode 6.1からなので、今までメインで使っていたXcode 6.0.1ではダメなのです。

 

以前の投稿で、1つ前のバージョンXcode 6.1を、試験マシンにインストールした話を書きました。いろいろと問題があって、速攻で失格となりましたが。それだけに、改良版の6.1.1には不安があります。試験マシンで大丈夫そうだったので、開発マシンも切り替える決断をしました。やっぱり、普段使っている開発マシンじゃないと、開発効率が悪すぎて困ります。

開発マシンに入れた理由が、もう1つあります。iPadアプリ開発の本番用を仕上げて一段落していて、切り替えて激しくテストするのには、一番良いタイミングだったことです。今は、空いている時間を見付けて、今まで作った機能のライブラリ化を進めています。こんなときこそ、テストとしては最適でしょう。

最終的にダメだったとしても、Xcode 6.0.1を再インストールして戻せますから、気持ち的にはラクです。戻す作業も、試験マシンで何回か試しているので経験済みですし。

 

過去に作った機能のライブラリ化では、このブログで紹介済みのiOS実験専用アプリを使っています。それをビルドしてみたら、コンパイルエラーが何個か出ました。オプショナルの扱いが少し変わっているようで、変数名の後ろに「!」を追加してエラーを消しました。

Xcode 6.1を試したときも同じようなエラーが出たので、コンパイラーが使うAPIの定義が、6.1から少しずつ改訂されているのでしょうね。間違いを訂正していることも考えられ、新しいAPI定義のほうが正しいでしょうから、できるだけ新しい開発ツールで開発するほうが良いでしょう。ツールのバグを抜きにすれば。

 

AppleサイトのAPI資料を何度も見ていますが、あるiOSのバージョンから使えなくなったメソッドなども含まれていて、どのバージョンから使えなくなったのか明示されています。今後も、APIの変更は続くでしょう。

同じSwiftコードでも、Xcodeのバージョンによって、エラーの出方が異なります。正確には、Xcodeのバージョンではなくて、コンパイル設定で指定されているSDKのバージョンですが。ちなみに、Xcode 6.1.1では、iOSアプリのデフォルト設定として、「Latest iOS (iOS 8.1)」となっていました。

このブログでも、いくつかのソースコードを公開しています。どれもXcode 6.0.1で開発しました。一部の投稿では、Xcodeのバージョンを明示していますが、何も記述していない投稿もあります。今後は、XcodeとSDKの両方のバージョン番号を明示する必要があるでしょうね。また、これまで書いたものにも、バージョン表記を付けておかなくては。

2014年12月15日月曜日

数秒後に自動で消えるメッセージ表示

たいていのアプリでは、実行結果やエラー内容などを伝えるために、短い文章でメッセージを表示します。とても単純な機能ですが、深く考えていくと、いろいろと工夫できる余地があります。メッセージ表示用の関数を独自ライブラリ用として作ったので、それとともに紹介します。

 

メッセージの表示で大切なのは「安心して使えるための心遣い」です。コンピュータ上で動作するアプリは、中でどのように処理されているのか、まったく見えません。タップや文字タイプの操作を受け付けていますが、それがどう処理されているのかは見えないのです。それだけに、ユーザーは多少の不安を感じながら操作していると思います。

その不安を少しでも解消するために、メッセージの上手な出し方が役立ちます。基本的な考え方は、何か操作したら、できるだけ即時に反応を返すことです。成功したのか失敗したのか、必ず返すようにします。するとユーザーは、自分の操作が受け付けられて処理されたと、安心するものなのです。

さらに上手な作り方は、単なる成功や失敗を伝えるのではなく、処理内容に関する情報を含めることです。「登録しました」よりも「1件を登録しました」とか「2件を登録しました」のほうが、より安心感をユーザーに与えられます。同時に、間違った操作を(まれにアプリのバグを)発見するきっかけにもなります。2件登録したと思ったのに、「1件を登録しました」と表示されたときとか。

さらなる改良も可能で、登録したデータの中身も表示するようにも作れます。ただし、詳しい情報を多く含めると、作るのが大変になっていきます。ほどほどで十分でしょう。私の場合は、件数を含めるぐらいで止めています。

 

メッセージの表示で気を付けたいのが、表示し終わったあとの処理です。そのまま表示しっぱなしでは、後で見たときに違和感を生じさせます。数秒間だけ表示した後、自動的に消すのが賢い作り方でしょう。

何秒間だけ表示するかは、表示する内容によって決まります。成功したのであれば、短い時間で構わないでしょう。逆にエラーを知らせるメッセージなら、成功よりは長めの表示時間が必要です。アプリ全体で標準化し、成功メッセージは何秒、注意メッセージは何秒、エラーメッセージは何秒と決めるとよいでしょう。ただし、使われる場面やメッセージの種類によっても変わりますから、アプリ内のメッセージを何種類かに分け、それぞれで成功やエラーの表示秒数を決めるのが良さそうです。場面ごとに使い心地が違うので、なかなか決めづらいとは思いますが。

 

表示時間以外にも、気を配りたい点があります。その1つが文字の色分けです。どんな色でも自由に使って構わないわけではありません。メッセージの内容に応じて、適した色を割り当てる必要があります。普段の生活で使われている色分けに合わせるのがベストです。

私の場合は、処理が成功すると緑色、ちょっとした注意を伝えるときは青色、エラーを示すときは赤色にしています。一般的な色のイメージに合わせた色分けだと思います。

このような色の割り当てでアプリを作ると、メッセージよりも色だけを見て、成功したか判断するようになります。ほとんどが緑色しか出ませんから、緑色で表示されれば安心して次の処理へ進めます。もし青色や赤色で表示されたら、いつもと違うと認識して、エラーメッセージを真剣に読むことになります。

 

今まで作ったアプリでは、次のような形でコーディングしていました。メッセージを表示するUILabelには、専用の関数を用意してアクセスします。お馴染みの間接参照で、メッセージを消す関数も用意します。表示した後で消す処理はNSTimerで実現し、数秒後に消す関数を呼び出す形でした。NSTimerで数秒後に呼び出す部分だけは、独自ライブラリの関数として作り、それを呼び出して使っていました。

メッセージを消す処理では、ちょっとした問題が発生します。指定した秒数後に単純に消してしまうと、次のメッセージが続いてい表示されるときに、それも消してしまうのです。具体的な動きで説明しましょう。最初のメッセージを表示して、5秒後に消す設定だとします。5秒経過する1秒前に別なメッセージが表示されると、その新しいメッセージを1秒後に消してしまうのです。

これを防ぐために、次のような形で作りました。表示するメッセージごとにidを付けて、メッセージを消す処理にもidを持たせます。消す処理の中では、消そうとしているメッセージのidが、自分が持っているのと同じidだったときだけ、メッセージを消すという形にします。すでに別なメッセージが表示されていれば、消さずに終了というわけです。

各メッセージのidは、乱数から生成させて重複を防いでいます。乱数を使えば、同じ処理から発生したメッセージでも異なるidになるため、間違って消すことはありません。メッセージ処理の内容は、大まかに以上のような感じです。

それを最近になって改良し、どのUILabelでも使えるメッセージ専用の関数に仕上げました。自分専用の独自ライブラリに入れて、次のアプリから使う予定です。メッセージ表示先のUILabelを指定できるため、どのメッセージ欄にも共通して使えます。

 

仕上がるまでの過程も紹介しましょう。最初は、どこからでも呼び出せるようにと、どのクラスにも所属しない関数(アプリ内のグローバル関数)として作ろうとしました。しかし、さっそく問題が発生します。NSTimerで関数を呼び出すときには、関数が所属するtargetを指定しなければなりません。クラスに所属しないグローバル関数だと、何に所属しているのか分かりません。いろいろ調べても判明しませんでした(知っている方がいたら教えてください)。

仕方がないので、ライブラリ用のクラスに所属する関数に切り替えました。メッセージ専用のクラスでも構わないのですが、メッセージ以外の様々な関数を入れた方が管理しやすそうです。クラスに所属しない関数は、ファイル名「BP_Base.swift」に入れています。これと似たような扱いで、これのクラス版としての意味を込め、ファイル名を「BC_Base.swift」としました。クラスに入れないと作れない関数の、入れ物クラスというわけです。

クラスに所属する関数として試すと、メッセージを消すための関数が起動しません。targetに指定したクラスには、そのような名前の関数がないと、実行時に怒られます。いくら確認しても、関数は確かにあります。コピーペーストしたので、関数のスペルも間違っていません。ネットで検索したところ、次のような原因が判明しました。targetとして指定するクラスは、NSObjectを継承していなければならないという条件があったのです。なるほど、NSTimerは、NSObjectに含まれる何かを使っているのですね。さっそく入れ物クラスを、NSObjectから継承するように指定しました。

以上のような流れで問題が解決し、無事にメッセージ関数が出来上がりました。

 

いよいよ関数の中身です。用意する関数は2つで、アプリから呼び出すsetMsgWTimerF関数と、指定された秒数後にメッセージを消すclearMsgWIdF関数です。具体的なSwiftコードは、次のとおりです。

// 仕方なくクラスに入れたライブラリ関数群(今のところメッセージ処理だけ)
// メッセージの種類の定義
enum MsgType : Int {
    case Normal = 0
    case OK = 1
    case Caution = 2
    case Error = 3
    case Debug = 101
    case InternalError = 102
}
// クラス本体
class BaseFunc : NSObject {
    func setMsgWTimerF(rLabel:UILabel, _ rStrMsg:String, _ rType:MsgType, _ rSec:NSTimeInterval) {
        if (rSec <= 0) { println("ERROR:BF_SMWT:rSecがゼロ以下"); return }
        switch rType {
        case .OK: rLabel.textColor = COLOR_OK
        case .Caution: rLabel.textColor = COLOR_CAUTION
        case .Error: rLabel.textColor = COLOR_ERROR
        case .Debug: rLabel.textColor = COLOR_DEBUG
        case .InternalError: rLabel.textColor = COLOR_INT_ERROR
        default: rLabel.textColor = COLOR_NORMAL
        }
        rLabel.text = rStrMsg
        // 乱数から得たidをセットして、タイマー開始
        let iId : Int = getRandomF(100000)
        rLabel.tag = iId
        let iParam : NSDictionary = ["iId": iId, "iLabel": rLabel]
        let iTimer : NSTimer = NSTimer.scheduledTimerWithTimeInterval(rSec, target:self, selector:Selector("clearMsgWIdF:"), userInfo:iParam, repeats:false)
        NSRunLoop.currentRunLoop().addTimer(iTimer, forMode:NSRunLoopCommonModes)
    }
    func clearMsgWIdF(rTimer: NSTimer) {
        let iUserInfo : NSDictionary = rTimer.userInfo as NSDictionary
        let iId : Int = iUserInfo["iId"] as Int
        let iLabel : UILabel = iUserInfo["iLabel"] as UILabel
        // 同じidだったときだけ、メッセージを消す
        if (iId == iLabel.tag) {
            iLabel.text = "     "    // 半角の空白文字を5個
            iLabel.textColor = COLOR_NORMAL
        }
    }
}

難しい部分はほとんどないので、要点だけ解説します。クラス内なのでメソッドと呼ぶべきなのですが、関数という扱いで使っていますから、これ以降も関数として説明します。

まず、メッセージを表示するsetMsgWTimerF関数から。引数のエラーチェックは、最低限だけです。何秒後に実行するかの秒数が、ゼロ以下だとエラーにしています。

COLOR_OKなどはUIColor型の色定義で、間接参照として用意しています。役割ごとに色を定義し、いろいろな箇所から参照することで、色を変える場合に一カ所の変更で済むようにしてあります。ここでは、デバッグ用と内部エラー用の色も別に割り当てて、使い分けています。

関数内で使っているgetRandomF関数は、乱数からidを得る関数です(ベタで書いても1行で済むのですが、乱数用の関数として用意することで、間接参照を実現しています)。最大値を指定して呼び出します。乱数なので、万が一ぐらいの確率で一致することも考えられます。しかし、一致したときの影響は、表示が少し変になる程度なので、実用上は問題ありません。得られたidは、UILabelのtagプロパティに入れておきます。別なメッセージで上書きされたら、自分のidとは異なるtag値に変わっているはずです。

こうして書きながら、あらためて考えると、乱数を使う必要はなかったように思います(汗)。もっと単純な方法でも可能でしょう。こういうことが、たまにありますよね。もう直すのが面倒なので、とりあえずは今のまま使います。後で直すかもしれませんが。

数秒後に実行されるclearMsgWIdF関数へ渡すデータは、NSDictionaryとして作り、NSTimerのuserInfoに設定します。単純な配列でも渡せるようですが、渡すデータの型が異なるので、入れ物としてNSDictionaryを用いました。

続いて、メッセージを消すclearMsgWIdF関数を。NSTimerのuserInfoから、引数を受け取ります。NSDictionaryにキャストしてから、それぞの値もキャストして取り出します。受け取ったUILabelの現在のtagプロパティが、同じく受け取ったidと比べて、同じ場合にだけメッセージをクリアーします。

こんなに簡単な作りですが、しっかりと動いてくれます。短時間に何個のメッセージを表示しても、間違ってクリアーされる心配はありません。

 

上記の関数を使う側のSwiftコードも紹介します。指定した秒数後にメッセージが消せ、役割に適した色でメッセージを表示できる処理が、たった1行で記述できます。しかも、どのラベルに表示するかも指定可能です。次のようなSwiftコードで。

// 自動消去メッセージの使用例
...
let zBase : BaseFunc = BaseFunc()  // アプリの初期処理でインスタンス生成
...
class AnyViewCtrl : UIViewController {
    ...
    let lblMsg : UILabel = createLabelF(...)  // UILabelの生成
    ...
    // 処理成功メッセージを表示
    zBase.setMsgWTimerF(lblMsg, "登録されました。", .OK, 4.0)
    ...
    // エラーメッセージを表示
    zBase.setMsgWTimerF(lblMsg, "名前を入力してください。", .Error, 9.0)
    ...
    // 内部エラーのメッセージを表示
    zBase.setMsgWTimerF(lblMsg, "内部エラー:VAZ31:登録失敗", .InternalError, 36000.0)
    ...
}

見てのとおりですが、簡単に説明します。

まず、アプリの初期処理で、今回のクラスのインスタンスを生成します。現状ではメッセージ用の関数しかありませんが、そのうち別な共通処理を追加するつもりでいます。アプリに不可欠な機能をまとめたクラスなので、アプリの初期処理で生成するというわけです。アプリ内の共通処理という位置付けになり、インスタンスを入れた変数zBaseを経由して、用意した関数にアクセスします。インスタンスを生成していますが、経由するための土台の役割しか持っていません。その意味で「Base」という名前をつけました。前に「z」が付くのは私のルールで、アプリ共通の機能を使うときの接頭語のような役割です。

この例では、できるだけ見やすくする意味で、メッセージの内容を凝っていません。実際のアプリでは、登録件数を付けて表示するなど、メッセージの文字列を生成する処理が、関数を呼び出す前に付きます。

この例から分かるように、1行だけの呼び出しで、消去までの秒数や、メッセージの色、使用するUILabelが指定できています。

ちなみに、メッセージを消さない機能を付けていませんが、この内部エラーの例のように極端な秒数を指定すれば、消さないのと同じ効果が得られます。この例では36000秒(=10時間)を指定しています。これでも不安なら、もっと大きな値も指定できます。

当然ですが、長く表示するメッセージでも、何かのメッセージで上書きされると消えてしまいます。36000秒後に実行されるクリアー処理も、メッセージのidが異なるため、何もせずに終了するでしょう。

 

上記の使用例では、メッセージを消すまでの秒数を、直接の値で指定しています。数多くのメッセージを出すアプリでは、ある程度まで秒数を標準化したほうが良いと思います。次のようなSwiftコードで定義し、参照するとよいでしょう。

// メッセージを消すまでの秒数を定義(こちらはアプリ内で定義)
let TIME_I_MSG_NORMAL : NSTimeInterval = 4.0
let TIME_I_MSG_OK_S : NSTimeInterval = 4.0
let TIME_I_MSG_OK_L : NSTimeInterval = 6.0
let TIME_I_MSG_CAU_S : NSTimeInterval = 6.0
let TIME_I_MSG_CAU_L : NSTimeInterval = 10.0
let TIME_I_MSG_ERR_S : NSTimeInterval = 6.0
let TIME_I_MSG_ERR_M : NSTimeInterval = 9.0
let TIME_I_MSG_ERR_L : NSTimeInterval = 12.0
let TIME_I_MSG_LL : NSTimeInterval = 36000.0   // 10時間
let TIME_I_MSG_LLL : NSTimeInterval = 86400.0  // 24時間

この定義ですが、一応は目的別になっています。ただし、目的別に1つずつではありません。目的別に分けつつ、それぞれで短めと長めを用意しました。さらにエラーでは、3種類の長さを用意しています。このようなセットだと、目的別の分け方を取りつつ、柔軟に対応できると考えました。

定義の中で、最後の2つは特殊用途です。重大なエラーが出たとき、メッセージが消えないようにする目的で使うものです。どれぐらいの時間が適切なのか不明なため、とりあえず2種類を付けました。本当に適した数値は、使ってみてというか、アプリが本番で使われたときに判明すると思います。使われるためには、本番での重大なエラーの発生が必要で、その機会が来るかどうかは不明ですが。

上記の定義を使った場合、前述のSwiftコードは次のように変わります。

// 上記の秒数定義を使った例
    ...
    // 処理成功メッセージを表示
    zBase.setMsgWTimerF(lblMsg, "登録されました。", .OK, TIME_I_MSG_OK_S)
    ...
    // エラーメッセージを表示
    zBase.setMsgWTimerF(lblMsg, "名前を入力してください。", .Error, TIME_I_MSG_ERR_M)
    ...
    // 内部エラーのメッセージを表示
    zBase.setMsgWTimerF(lblMsg, "内部エラー:VAZ31:登録失敗", .InternalError, TIME_I_MSG_LL)
    ...

このように秒数定義を用意して参照すれば、同じ役割の秒数を変更するとき、一括でできて便利です。

 

今回のメッセージ用関数の改良で、メッセージ処理がライブラリ化できました。今後のアプリでは、この関数を多いに利用するでしょう。それによって、この関数ではカバーできないメッセージ処理が見えてきて、新たな関数の追加も視野に入ると思います。

使用頻度の高い処理をライブラリ化すると、その次にライブラリ化すべき対象が見えてくるという、好循環が期待できそうです。

 

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

2014年12月13日土曜日

雑誌などのソースコードは再構成して使う

新しい言語や使ったことのない機能などを学ぶ場合、雑誌やウェブにあるサンプルのソースコードを参考にする人は多いと思います。当然ですが、私もその一人です。数多く目にするソースコードですが、「アプリに利用する場合」は、そのまま使ったり、ほぼ真似て作るのはお薦めできません。それには大きな理由があるので説明します。

 

そもそも、雑誌やウェブ、またはメーカーが提供するソースコードの目的は何でしょう。たいていの場合は、作りたい機能を実現することです。つまり、機能の実現を重視したソースコードとして書かれています。

そうした目的のために一番適したコーディング方法は、具体的な命令文をベタで書くことです。説明のためのコメントは加えますが、それ以外では命令文だけをベタに記述します。どのような命令を組み合わせ、どの順序で並べれば確実に動くのか、もっとも理解しやすい方法だからです。

命令文をベタに羅列する書き方は、命令文以外の要素が含まれません。そのため、余計な説明が不要になります。再利用のために何々を工夫したとかいう話は、目的の機能を実現するためには関係ないのです。つまり、機能の実現方法を説明する目的には、命令文をベタに書く方法が最適なわけです。

 

では逆に、アプリを開発するときのソースコードは、どんな点を考えて書かれるのでしょうか。もちろん、人によって考えは違うでしょう。でも、アプリ内の構造を上手に作るとしたら、意見の違いは少ないのではないでしょうか。いろいろな点が考えられますが、実現するのが難しくて、もっとも重視したい点となると、変更への柔軟性です。将来の拡張を十分に考慮し、変更が発生したときの手間が最小限で済むように、いろいろと工夫して書くはずです。

次に挙げる点としては、コードの再利用があります。一度作った機能は、別なアプリで使うときに、手直しせずに使えたら非常に助かります。新しく書くソースコードの量が減るからです。無駄な開発作業を減らす、もっとも有効な手段でしょう。

つまり、ソースコードを作る目的というか、どんな点を重視してソースコードを書くのかが、雑誌のサンプルとアプリ開発では根本的に違っているということです。この点に気付かない人が、意外に多いかもしれません。

 

少し余談になりますが、柔軟性や再利用以外の考慮点として、実行速度が挙げられます。最近は、それほど気にしなくて良くなりましたが、完全になくなったわけではありません。ただし、以前とは状況が違ってます。

実行速度が問題になりそうなアプリを作るときでも、何から何まで速度重視で作るケースは減りました。ツールが発達したことで、ネックになっている処理部分が見付けやすくなったことも大きいです。速度重視の開発でも、手順としては次のように行なうケースが増えていと思います。最初は、スッキリした構造で作り、一応の機能を完成させます。もし遅くて困る機能が発見されたら、解析ツールを使って、遅い原因となっている処理を特定し、その部分に改良を加えます。こうした手順だと、全体として良い構造を確保しながら、速度問題も解決できる可能性が高まります。

もし最初から処理速度が問題になりそうなアプリでも、開発を始める前に大まかな解決方法を考え、その方法に沿いながら、アプリの構造を良くする方法まで検討します。良い構造を抜きにして開発するのは、もう受け付けられない状況になっていますね。

 

話を戻して、変更への柔軟性や再利用を重視した作り方を取り上げましょう。具体的には、どのようなソースコードになるのでしょうか。

まずは、よく使う機能のライブラリ化です。同じ命令コードを繰り返し書かなくて済むように、機能ごとにまとめて、異なるアプリでも共通して使えるように作ります。クラスとして作る場合も、関数群として作る場合もあるでしょう。どちらにするかは、実現する機能の特徴に加え、プログラマーの好みにも関係するでしょうね。いや、本当に好みなんですよ。

さらに考えるのは、機能の切り分けと、切り分けた部分のインターフェースです。たとえば、アプリで作成した内容を、プリンターで印刷したいとします。大きく分けて、印刷内容を生成する処理と、その内容をプリンターに渡して印刷する処理が考えられます。後者の処理は、使用するプリンターに依存し、プリンターごとに作る必要があります。

そうなると一番良い切り分け方は、次のようになります。印刷内容を生成する処理を独立させ、プリンターへ渡す部分のインターフェースを標準化することです。プリンターに渡して印刷する処理は、プリンターごとに作りますが、標準化されたインターフェースに準拠して作るわけです。

こうすると、次のようなメリットが生まれます。印刷内容を生成する処理では、プリンターを切り替えても、修正する箇所が最小限で済みます。同時に、プリンターを扱う処理も独立でき、別なアプリへ持っていきやすくなります。インターフェースを標準化していますから、持っていった別なアプリでも、複数のプリンターを切り替えたとき、呼び出す側の変更が最小限で済みます。

このように見ていくと、機能の切り分けが重要であると理解できます。それぞれの機能が交換しやすく、また他へ持っていきやすくなるように、上手に切り分けることです。さらに、切り分けた部分のインターフェースを標準化して、切り分け後の同種機能を交換可能に作る点も、非常に重要です。上手な切り分けと、切り分けたインターフェースの標準化が、ソフトウェア設計で非常に重要なことなのです。

現在はオブジェクト指向プログラミングが広く普及していますが、これらの切り分けは、オブジェクト指向には依存しません。古くから使われている手続き型のプログラミング言語でも、簡単に実現可能です。その意味で、プログラミング言語の種類に依存せず、もっとも基本的な設計方法と言えるでしょう。

ライブラリ化も含めて、機能を区切る際に一番重要なのが「どこで区切るか」です。上手に区切らないと、メリットを大きくできません。考え方としては、似た機能を交換できるか、そのまま別なアプリへ持っていけるか、という点を重視して決めます。場合によっては、区切りを2段階にして、交換できる可能性を増やすこともあります。何度も試して経験する中で、少しずつ上手になると思います。

 

続いて、広く役立つのが間接参照です。色などの値を定義して、それを参照する使い方が一般的でしょう。上手なプログラミング手法として、C言語などでも古くから使われています。

それ以上に役立つのが、関数などの処理を通した間接参照です。関数を間に入れることで、色々な機能を盛り込めます。たとえば、現在の時刻を得る関数を用意して、アプリに含まれる全部の時刻参照で、その関数を使っているとします。すると、その関数の処理を変更し、通常よりも時間が速く進む機能を加えたり、指定した時間分だけ時刻を早めたり遅らしたりも可能になります。さらには、特定の日付時刻で実行させるといったトリックも、簡単に実現できます。

アプリ上では、メッセージを表示するためのUI部品を付けるでしょう。その場合、UI部品を直接参照するのではなく、UI部品を参照するための関数を用意するのが、間接参照による作り方です。メッセージを表示する個々のソースコードでは、UI部品での代わりに関数を呼び出す形になります。すると、メッセージ表示に特別な機能を追加したいとき(表示時刻を追加するとか)、関数だけを修正して対応できます。

値定義でも関数でも、事前に間接参照を使っておけば、1カ所を変更するだけで、参照している全部の値や機能を変えられます。つまり、変更への柔軟性が高いプログラムになるわけです。

 

以上のような工夫を数多く用いると、どのようなソースコードになるでしょうか。よく使う機能はライブラリ化して、用意した関数やメソッドを呼び出す形になります。間接参照を多用すると、値定義や関数を呼び出す形が増えます。

結果としてソースコードは、関数やメソッドが中心となり、ベタで書いた命令文の率が極端に減ります。雑誌などの見かけるような、命令文をベタで書いたソースコートとは、まったく別物に見えるはずです。アプリ内部の構造を上手に作れる人ほど、その傾向が強いでしょう。

 

ここまでの話を理解すると、ソースコードの作り方が大きく変わると思います。雑誌などのソースコードを参照しても、そのままコピーして使わずに、個々の命令文をプログラム部品として用いながら、アプリ全体の構造に当てはめていくでしょう。まるでソースコードを再構成するような感じで。

よく使う機能をライブラリ化したり、同類の機能と置き換えやすいように処理内容を区分けしたり、そのインターフェースを標準化したり、いろいろな箇所で間接参照を組み込んだり、様々な工夫が思いつくはずです。こうした考え方で、アプリの内部を設計すれば、優れた構造で作れると思います。

 

プログラムの再利用の話になると、オブジェクト指向の話が必ず登場します。でも、オブジェクト指向のプログラムング言語を使い、それを生かしたブログラミング手法(デザインパターンなど)で作ったとしても、再利用しやすいソースコードになるとは限りません。

たとえば、小型プリンターRoltoのサンプルコードは、Objective-C言語を用い、オブジェクト指向の考え方で書かれています。しかし、プリンター処理がアプリ処理と一体化しているため、プリンター処理だけ独立して、他のアプリへコピーしては使えません。また、他のプリンターに切り替えたとき、アプリの処理もかなり変更しなければなりません。最近使ったという理由でRoltoを例に挙げましたが、他のプリンターでもほとんど同じです。

これらのソースコードは、前述したように、ドライバーの使い方を説明するのが目的です。そのため、再利用しやすいコードを目指してはいません。結果として、オブジェクト指向に則って作りながらも、再利用しづらいソースコードの例となってしまいました。

つまり、オブジェクト指向も道具の1つでしかなく、より上位のレベルでのソフトウェア設計において、再利用を考慮することが大事だということです。ここで書いたようなアプリ設計の考え方を採用することで、オブジェクト指向の利点が、さらに有効に活かせるようになります。

 

ここまでの話を、最後に整理しましょう。大事な順に並べると、次のようになります。

変更への柔軟性や、再利用を重視したソースコードを作る視点
1. 機能の切り分け(インターフェースの標準化も含む)
2. 間接参照を多用する
3. オブジェクト指向の利点を利用する(デザインパターンも含めて)

この中では、オブジェクト指向だけが特殊です。他の上位2つは、オブジェクト指向のブログラミング言語でなくても利用できる、ソフトウェア設計術に位置づけられます。

アプリを開発する際には、ここで取り上げたような視点で、アプリの内部構造を考えてみてはいかがでしょう。そうすれば、雑誌などに載っているサンプルのソースコードをそのまま使わず、細かな命令文を部品として再構成するようになり、変更への柔軟性が高くて再利用しやすいソースコードが得られるはずです。

2014年12月10日水曜日

オフスクリーン描画をライブラリ化(おまけ実験編)

オフスクリーン描画機能をライブラリ化する話の続きです。ひと通りの説明は終わったのですが、おまけを追加します。今回作ったクラスを使うと、iOSの2次元描画機能(特にCGContextを使った処理)の特性が良く理解できる実験が可能なのです。その結果は、このクラスを使う際に注意点にもなりますから、絶対に知っておくべき内容でしょう。

 

この話題の最初に取り上げたように、CGContextを使った描画では、描く相手が常に1つだけのようです。今回作成した描画ライブラリを使って、簡単な実験をしてみましょう。

描画ライブラリはクラスとして作ったので、インスタンスを生成し、そのメソッドとして実行します。ここでの実験では、インスタンスを2つ以上作って、色々なメソッドを交互に実行してみました。実験1でのSwiftコードは、次のようなものです。

// 実験1
// インスタンス1
var iDraw1 = DrawImageOffScreen(200, 200, false, 1)            // Draw1開始
let c1 : Int = 0
iDraw1.setContextStrokeColorF(c1, 1.0, 0.0, 0.0, 1.0)
iDraw1.drawLineF(c1, 1.0, 10.0, 10.0, 60.0, 10.0)              // L1
iDraw1.drawRectF(c1, .Stroke, 1.0, 20.0, 20.0, 40.0, 40.0)     // R1
iDraw1.drawCircleF(c1, .Stroke, 1.0, 120.0, 20.0, 40.0, 40.0)  // C1
iImageView1.image = iDraw1.getFinalImageF()                    // Draw1終了:L1,R1,C1を描画
// インスタンス2
var iDraw2 = DrawImageOffScreen(200, 200, false, 1)            // Draw2開始
let c2 : Int = 0
iDraw2.setContextStrokeColorF(c2, 0.0, 1.0, 0.0, 1.0)
iDraw2.drawLineF(c2, 2.0, 10.0, 110.0, 60.0, 110.0)            // L2
iDraw2.drawRectF(c2, .Stroke, 2.0, 20.0, 120.0, 40.0, 40.0)    // R2
iDraw2.drawCircleF(c2, .Stroke, 2.0, 120.0, 120.0, 40.0, 40.0) // C2
iImageView2.image = iDraw2.getFinalImageF()                    // Draw2終了:L2,R2,C2を描画

当然ですが、どちらも正常に動作します。1時点に1つのインスタンスしか動作していないので、正常に動いて当たり前ですね。

 

続いての実験2では、2つのインスタンスの処理を、少しだけ重複させてみましょう。実験に使うSwiftコードは次のとおりです。

// 実験2
// インスタンス1
var iDraw1 = DrawImageOffScreen(200, 200, false, 1)            // Draw1開始
let c1 : Int = 0
iDraw1.setContextStrokeColorF(c1, 1.0, 0.0, 0.0, 1.0)
iDraw1.drawLineF(c1, 1.0, 10.0, 10.0, 60.0, 10.0)              // L1
iDraw1.drawRectF(c1, .Stroke, 1.0, 20.0, 20.0, 40.0, 40.0)     // R1
// インスタンス2
var iDraw2 = DrawImageOffScreen(200, 200, false, 1)            // Draw2開始
let c2 : Int = 0
iDraw2.setContextStrokeColorF(c2, 0.0, 1.0, 0.0, 1.0)
iDraw2.drawLineF(c2, 2.0, 10.0, 110.0, 60.0, 110.0)            // L2
// インスタンス1
iDraw1.drawCircleF(c1, .Stroke, 1.0, 120.0, 20.0, 40.0, 40.0)  // C1
iImageView1.image = iDraw1.getFinalImageF()                    // Draw1終了:L2,C1を描画
// インスタンス2
iDraw2.drawRectF(c2, .Stroke, 2.0, 20.0, 120.0, 40.0, 40.0)    // R2
iDraw2.drawCircleF(c2, .Stroke, 2.0, 120.0, 120.0, 40.0, 40.0) // C2
iImageView2.image = iDraw2.getFinalImageF()                    // Draw2終了:L1,R1,R2,C2を描画

描かれた結果は、コメントとして追加してあります。Draw1が終了した時点で得られるUIImageには、L2とC1が描かれています。もう片方のDraw2には、残りのL1,R1,R2,C2が描かれています。どちらのインスタンスで描いたのか関係なくなっています。この結果から、描く場所が1つしかないのではと考えるようになりました。

推測できたことが、もう1つあります。描画する場所は1つしかなく、それが入れ子できる形で動いているのではないかという点です。Drew2が開始してDraw1が終了するまでが、内側の入れ子状態で、描画で得られた結果も、そのとおりになっています。また、入れ子の外側の描画結果も、入れ子として考えると辻褄が合います。

 

同じものに描いているかどうか、さらに確認するために、Draw1とDraw2の終了を入れ替えた実験3を試します。次のようなSwiftコードを使って。

// 実験3
// インスタンス1
var iDraw1 = DrawImageOffScreen(200, 200, false, 1)            // Draw1開始
let c1 : Int = 0
iDraw1.setContextStrokeColorF(c1, 1.0, 0.0, 0.0, 1.0)
iDraw1.drawLineF(c1, 1.0, 10.0, 10.0, 60.0, 10.0)              // L1
iDraw1.drawRectF(c1, .Stroke, 1.0, 20.0, 20.0, 40.0, 40.0)     // R1
// インスタンス2
var iDraw2 = DrawImageOffScreen(200, 200, false, 1)            // Draw2開始
let c2 : Int = 0
iDraw2.setContextStrokeColorF(c2, 0.0, 1.0, 0.0, 1.0)
iDraw2.drawLineF(c2, 2.0, 10.0, 110.0, 60.0, 110.0)            // L2
// インスタンス1
iDraw1.drawCircleF(c1, .Stroke, 1.0, 120.0, 20.0, 40.0, 40.0)  // C1
// インスタンス2の終了
iImageView2.image = iDraw2.getFinalImageF()                    // Draw2終了:L2,C1を描画
// インスタンス2
iDraw2.drawRectF(c2, .Stroke, 2.0, 20.0, 120.0, 40.0, 40.0)    // R2
iDraw2.drawCircleF(c2, .Stroke, 2.0, 120.0, 120.0, 40.0, 40.0) // C2
// インスタンス1の終了
iImageView1.image = iDraw1.getFinalImageF()                    // Draw1終了:L1,R1,R2,C2を描画

このソースコードを見て、「あれれ、これが正常に動いたらダメじゃないの」と思った方は、賢いです。Draw2終了後のDraw2描画は、エラーになるため描かれません。それぞれの描画メソッドにおいて、描画可能状態をチェックする処理を迂回する、修正版ソースコードにて実行しました。

Draw1とDraw2の終了を入れ替えたので、描かれた結果も完全に入れ替わっています。逆に入れ子として見た場合は、前の実験2と完全に同じ結果になっています。これから分かることは、どのインスタンスかに関係なく、ただ単に描いたり取得したりするタイミングだけによって、得られる結果が決まるということです。どのインスタンスで描いたかには、まったく関係ありません。

ここまでの実験により、次のことが分かりました。インスタンスを何個用意しても、ただ1つの描画領域に描いていて、得られる結果はタイミングだけで決まること。複数のインスタンスで描く場合は、タイミングが重ならないように注意すること。この2つです。

 

分かった2つのこと以外にも、気付いた点があります。入れ子状態で描く場合は、正常に動くかもしれないことです。それを確認するために、入れ子での動作を実験4で試します。具体的なSwiftコードは、次のとおりです。

// 実験4
// インスタンス1
var iDraw1 = DrawImageOffScreen(200, 200, false, 1)            // Draw1開始
let c1 : Int = 0
iDraw1.setContextStrokeColorF(c1, 1.0, 0.0, 0.0, 1.0)
iDraw1.drawLineF(c1, 1.0, 10.0, 10.0, 60.0, 10.0)              // L1
iDraw1.drawRectF(c1, .Stroke, 1.0, 20.0, 20.0, 40.0, 40.0)     // R1
// インスタンス2
var iDraw2 = DrawImageOffScreen(200, 200, false, 1)            // Draw2開始
let c2 : Int = 0
iDraw2.setContextStrokeColorF(c2, 0.0, 1.0, 0.0, 1.0)
iDraw2.drawLineF(c2, 2.0, 10.0, 110.0, 60.0, 110.0)            // L2
iDraw2.drawRectF(c2, .Stroke, 2.0, 20.0, 120.0, 40.0, 40.0)    // R2
iDraw2.drawCircleF(c2, .Stroke, 2.0, 120.0, 120.0, 40.0, 40.0) // C2
iImageView2.image = iDraw2.getFinalImageF()                    // Draw2終了:L2,R2,C2を描画
// インスタンス1
iDraw1.drawCircleF(c1, .Stroke, 1.0, 120.0, 20.0, 40.0, 40.0)  // C1
iImageView1.image = iDraw1.getFinalImageF()                    // Draw1終了:L1,R1,C1を描画

まったく正常に動作しました。予想どおりの描画結果です。入れ子状態で使う場合はアリなのでしょうか。

 

入れ子が3段階になっても大丈夫か確認するために、さらなる実験5も試しました。次のようなSwiftコードで。

// 実験5
// インスタンス1
var iDraw1 = DrawImageOffScreen(200, 200, false, 1)            // Draw1開始
let c1 : Int = 0
iDraw1.setContextStrokeColorF(c1, 1.0, 0.0, 0.0, 1.0)
iDraw1.drawLineF(c1, 1.0, 10.0, 10.0, 60.0, 10.0)              // L1
iDraw1.drawRectF(c1, .Stroke, 1.0, 20.0, 20.0, 40.0, 40.0)     // R1
// インスタンス2
var iDraw2 = DrawImageOffScreen(200, 200, false, 1)            // Draw2開始
let c2 : Int = 0
iDraw2.setContextStrokeColorF(c2, 0.0, 1.0, 0.0, 1.0)
iDraw2.drawLineF(c2, 2.0, 10.0, 110.0, 60.0, 110.0)            // L2
// インスタンス3
var iDraw3 = DrawImageOffScreen(200, 200, false, 1)            // Draw3開始
let c3 : Int = 0
iDraw3.setContextStrokeColorF(c3, 0.0, 0.0, 1.0, 1.0)
iDraw3.drawLineF(c3, 3.0, 10.0, 10.0, 60.0, 10.0)              // L3
iDraw3.drawRectF(c3, .Stroke, 3.0, 20.0, 20.0, 40.0, 40.0)     // R3
iDraw3.drawCircleF(c3, .Stroke, 3.0, 120.0, 20.0, 40.0, 40.0)  // C3
iImageView3.image = iDraw3.getFinalImageF()                    // L2,R2,C2を描画
// インスタンス2
iDraw2.drawRectF(c2, .Stroke, 2.0, 20.0, 120.0, 40.0, 40.0)    // R2
iDraw2.drawCircleF(c2, .Stroke, 2.0, 120.0, 120.0, 40.0, 40.0) // C2
iImageView2.image = iDraw2.getFinalImageF()                    // L2,R2,C2を描画
// インスタンス1
iDraw1.drawCircleF(c1, .Stroke, 1.0, 120.0, 20.0, 40.0, 40.0)  // C1
iImageView1.image = iDraw1.getFinalImageF()                    // L1,R1,C1を描画

こちらも、正常な描画結果が得られました。3段階の入れ子で使っても、描画結果に問題はないようです。

ただし、こうした使い方が保証されているのでしょうか。そうでないとしたら、たまたま動いていて、将来のiOSのバージョンアップで動かなくなる可能性が心配です。やはり、入れ子での使用は避け、1つずつ順番に描き終える使い方が確実でしょう。

以上のような実験は、描画機能をライブラリ化したことで簡単に実行できます。もし全部をベタで書くのなら、ヤル気にならなかったでしょう。短く書いて簡単に使えることは、意外に大事なようですね。

 

ここでの実験で、CGContextを使った描画の特性が見えてきたと思います。その結果、描画機能を使う際の注意点も理解できました。この話題の最初で書いたように、あくまで描画用の「部品」を作る機能です。必要な部品を次々と順番に描画していけますが、1つが終わってから次を作り始めるという手順が、正しい使い方なのでしょう。

オフスクリーン描画のような機能をライブラリ化する場合、機能として工夫する余地は、あまりないように思います。その代わりに使い勝手を良くするとか、描いている内容を理解しやすくするとか、別な面での工夫が求められます。今回のライブラリ化では、1行で書ける使い勝手と、描画内容の理解しやすさを重視しました。

オフスクリーン描画機能をライブラリ化する話は、今回で終了です。いろいろ説明した中から、役に立つ考え方が見付かったら、自分なりのライブラリ化に利用してみてください。

 

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

2014年12月9日火曜日

オフスクリーン描画をライブラリ化(使用例編)

オフスクリーン描画機能をライブラリ化する話の続きです。ライブラリ用のクラスを作り終わったので、今度は使ってみる段階です。どのように使うと良いのかも含めて、使用例を紹介します。

 

まず最初に、一番大事な点を。ライブラリ化した描画機能を使うのですが、何を描いているのか、どのように描いているのか、ソースコードを見て理解しやすいことが大切です。そのためには、ライブラリを使う側でも工夫が必要です。

その工夫とは、まず登録する色に名前をつけることです。一番良いのは、色の役割で名前を付けることですが、それが難しい場合には、色そのものの名前(BlueやRedなど)でも構いません。描画メソッドを呼ぶ際に、色の名前をつけることで、どんな色で描かれるのか把握しやすくなります。

さらには、描画する内容を整理して見せることです。描く全体像をグループ分けして、グループごとにコメントを付けて区切るだけでも、それぞれが何を描いているか理解しやすくなります。一般的には、描画する領域を数個に分け、それぞれの間にコメント行を挿入して理解しやすく仕上げます。

もう1つ、描くものが大きくて要素の数が多い場合には、描画領域を区分けして、区分け領域の相対位置で座標を指定するようにします。複数のUIViewを使って、表示画面を区分けするのと同じような考えです。具体的には、領域ごとの座標点を持つ変数(それぞれにXとYで2つずつ)を用意し、メソッド中の座標指定では、用意した変数と数値との加算式を用います。なお、これが必要になるケースは少ないと思いますから、この具体例は今回省きます。

主な工夫は、以上です。たったこれだけの工夫でも、描いている内容が大まかに見通せるようになります。

 

最初に用意するのは、色の変数です。使う色の数だけ、変数を用意します。具体的なSwiftコードを見たほうが理解しやすいでしょう。次のように作ります。

// 描画を開始(インスタンス生成)
var iDraw = DrawImageOffScreen(400, 150, false, 10)
// 色ごとに変数を用意する
let cMain : Int = 0       // 組織のメインカラー
let cMainL : Int = 1      // 組織のメインカラー(薄)
let cTextDark : Int = 2
let cTextLight : Int = 3
let cTextSp : Int = 4     // 注目させる色
let cLineStrong : Int = 5
let cLineWeak : Int = 6
let cDecoLine : Int = 7
let cDecoBox : Int = 8
let cDecoCircle : Int = 9

色の変数を宣言する前に、クラスのインスタンスを生成しています。その理由は、描く処理をここから開始するという宣言の意味を込めているからです。インスタンス生成後に変数が来ると、これらの変数が、このインスタンスで使われると理解しやすくなりますので。

変数の名前は、すべて小文字のcで始まっています。colorを意味するcで、colorだと長いため、1文字のcを採用しました。

同様に、cに続く名前も、あまり長く付けないように心がけます。DecorationもDecoと省略して、最低限の長さにしています。できるだけ短い名前にすると、名前を使う側のソースコードも短くなり、少しですが読みやすさが増します。もし名前の意味を細く説明したい場合は、コメントとして追加すれば良いでしょう。

この例では、描く要素の役割ごとに名前をつけています。たとえ同じ色でも、役割ごとに名前を用意し、別々に使うことが重要です。後で色を変更したときに、役割の違う色まで一緒に変わるという問題が発生しません。このブログの間接参照のときに説明した、変更への柔軟性を高める作り方です。

 

続いて、色の値を設定します。もう名前が付いているので、名前順に一気に並べて、設定メソッドを呼び出すだけです。具体的なSwiftコードは次のようになります。

// 色に値を設定する
iDraw.setContextStrokeColorF(cMain, 0.1, 0.1, 1.0, 1.0)     // 純粋ではない濃い青
iDraw.setContextFillColorF(cMain, 0.9, 0.9, 1.0, 1.0)       // 薄い青
iDraw.setContextStrokeColorF(cTextDark, 0.1, 0.1, 0.1, 1.0)
iDraw.setContextStrokeColorF(cTextLight, 0.5, 0.5, 0.5, 1.0)
iDraw.setContextStrokeColorF(cTextSp, 1.0, 0.0, 0.0, 1.0)   // 注目色:赤
iDraw.setContextStrokeColorF(cLineStrong, 0.1, 0.1, 1.0, 1.0)
iDraw.setContextStrokeColorF(cLineWeak, 0.7, 0.7, 1.0, 1.0)
iDraw.setContextStrokeColorF(cDecoLine, 0.1, 0.1, 1.0, 1.0)
iDraw.setContextFillColorF(cDecoBox, 0.1, 0.1, 1.0, 0.5)
iDraw.setContextFillColorF(cDecoCircle, 0.3, 1.0, 0.3, 0.5)

どの色も、デフォルトの値を持ってますから、変更が必要な値のみを設定するだけです。また、使わない値を設定する必要もありません。線色(StrokeColor)だけ、塗りつぶし色(FillColor)だけというのもアリです。もちろん、使わないけど念のために設定しておくのもアリです。

ここでも、何か細くしたいことがあれば、コメントとして追加します。光の3原色(RGB)の仕組みが分かっていれば、3つの数値を見ただけで大まかな色は理解できますが、そうでない人のためにコメントで補足するのは、良い心遣いといえますね。

知人が話してくれたのですが、使わない色にも必ず設定するというのもアリだそうです。使わない色の全部に、そのアプリでは絶対に使わない目立つ色(たとえば紫とか)を設定しておきます。もしそれが表示さたら、指定の間違いを発見できるという使い方です。このようなエラー発見方法もありますという紹介でした。面倒なので、私は使っていませんが。もし採用する場合は、コメントで「使ってない色」と付けておくべきでしょう。

 

色の登録が終わったので、いよいよ描く段階です。描く領域で区分けして、間にコメント行を入れながら、通常は上から順番に描きます。最後に、描いた結果を変数に入れるか、UIImageViewのimageプロパティに設定します。具体的なSwiftコードですが、以下のコードは架空の整理券を描いています。

// ここから描画命令
// 整理券情報
iDraw.drawRectF(cMain, .Both, 1.0, 10.0, 10.0, 170.0, 60.0)
iDraw.drawTextF(cTextDark, "整理番号:", 16.0, 15.0, 13.0)
iDraw.drawTextF(cTextDark, iNumTicket, 30.0, 15.0, 33.0)
for i in 0...24 {
    var iX : CGFloat = 190.0 + (8.0 * CGFloat(i))
    iDraw.drawLineF(cDecoLine, 1.0, iX, 30, (iX + 10.0), 10.0)      // 飾り
}
// タイトルと日付
iDraw.drawTextF(cTextDark, "入場整理券", 16.0, 200.0, 35.0)
iDraw.drawTextF(cTextDark, "  日付:2014年12月20日", 14.0, 200.0, 55.0)
// 中央の区切り線
iDraw.drawLineF(cLineStrong, 3.0, 10.0, 80.0, 380.0, 80.0)
for i in 0...23 {
    var iX : CGFloat = 20.0 + (15.0 * CGFloat(i))
    iDraw.drawCircleF(cDecoCircle, .Fill, 1.0, iX, 75, 10.0, 10.0)  // 飾り
}
// 会員情報
iDraw.drawTextF(cTextDark, ("会員番号:" + iNumMember), 14.0, 15.0, 90.0)
iDraw.drawTextF(cTextSp, "開催場所:秘密(招待メール参照)", 14.0, 180.0, 90.0)
// 発行人
iDraw.drawTextF(cTextLight, "発行人:ガイガー・カウント・グループ", 12.0, 180.0, 115.0)
iDraw.drawTextF(cTextLight, "watashida@gaigaigai.com", 12.0, 250.0, 130.0)
for i in 0...17 {
    var iX : CGFloat = 10.0 + (8.0 * CGFloat(i))
    iDraw.drawLineF(cDecoLine, 1.0, iX, 140, (iX + 10.0), 120.0)   // 飾り
}
for i in 0...7 {
    var iX : CGFloat = 165.0 + (10.0 * CGFloat(i))
    iDraw.drawRectF(cDecoBox, .Fill, 1.0, iX, 134, 8.0, 8.0)       // 飾り
}
// UIImageをImageViewに保存
iImageView.image = iDraw.getFinalImageF()

このブログ上の表示では、区切りのコメント行も同じ色なので見づらいですが、Xcodeのエディタ上ではコメント行が薄い色になり、区切りが明確に見えます。

同じ領域の描画要素を並べるので、位置関係が把握しやすくなっています。座標の数値を変更して、重ならないように移動したり、逆に重ねたりも容易でしょう。

色に名前をつけたので、どんな役割の色が割り当てられているのかも、簡単に読み取れます。引数には変数も含まれていますが、描かれている内容は大まかに想像できます。あとは実際に表示や印刷してみて、座標の値を細かく調整するだけです。

 

上記の例では、飾りを描画するfor文が何個もあるので、全体として見づらくなっています。試しに、for文だけ削除してみましょう。

// ここから描画命令
// 整理券情報
iDraw.drawRectF(cMain, .Both, 1.0, 10.0, 10.0, 170.0, 60.0)
iDraw.drawTextF(cTextDark, "整理番号:", 16.0, 15.0, 13.0)
iDraw.drawTextF(cTextDark, iNumTicket, 30.0, 15.0, 33.0)
// タイトルと日付
iDraw.drawTextF(cTextDark, "入場整理券", 16.0, 200.0, 35.0)
iDraw.drawTextF(cTextDark, "  日付:2014年12月20日", 14.0, 200.0, 55.0)
// 中央の区切り線
iDraw.drawLineF(cLineStrong, 3.0, 10.0, 80.0, 380.0, 80.0)
// 会員情報
iDraw.drawTextF(cTextDark, ("会員番号:" + iNumMember), 14.0, 15.0, 90.0)
iDraw.drawTextF(cTextSp, "開催場所:秘密(招待メール参照)", 14.0, 180.0, 90.0)
// 発行人
iDraw.drawTextF(cTextLight, "発行人:ガイガー・カウント・グループ", 12.0, 180.0, 115.0)
iDraw.drawTextF(cTextLight, "watashida@gaigaigai.com", 12.0, 250.0, 130.0)
// UIImageをImageViewに保存
iImageView.image = iDraw.getFinalImageF()

文字列の描画が中心ですが、かなり見やすくなり、処理内容が読み取りやすくなったと思います。この状態が、今回のクラスを使う際に狙っていた形です。

今回の描画ライブラリでは、1つの要素を描くのに、必ず短い1行で書き終えることを重視しています。それぞれの行では、メソッド名で描く要素の種類を、色の名前で色の役割を、それ以降の引数で文字列や座標や大きさを読み取れます。複数行のメソッドが並んでも、楽に読み取れると思います。

 

描画ライブラリを使う側の作り方は、以上です。ここまで、swiftソースコードを分割して取り上げましたが、3つを単純に並べたものが、実際のソースコードになります。その並べた様子も、同じく整理された状態に見えます。

ここまで紹介した使い方の構成を簡単に整理すると、次のようになります。

ライブラリを使う側の全体構成
・色の変数名を用意
・色の値を設定して登録
・色を使って描く(描く内容を領域で分割:間にコメント行)

この構成に合わせて作れば、読みやすくて修正しやすいソースコードになると思います。

 

ここで挙げた例は、座標や文字列を直接入れたものが中心でした。しかし、私が作成した某業務アプリでは、印刷レイアウトをiPad上で編集する機能を付けました。印刷のレイアウトなどは、細かな変更依頼が何度も発生すると考え、レイアウトの編集機能もiPad上で作ってしまいました。その際には、すべての引数の値を変数に入れ、描画メソッドを呼び出す形となります。

レイアウト機能と連動させても、このライブラリは問題なく動きます。でも、ほぼ自由にレイアウトできるため、ソースコードから印刷内容を読み取ることが、まったく関係なくなります。使う側のソースコードの読みやすさは、描画内容をベタで書いたときの読みやすさを重視すべきなのでしょう。ここで取り上げた例のように。

 

今回は、作成した描画ライブラリの使用例を紹介しました。これで一通りの解説は終わったのですが、まだ宿題が残っています。CGContextを使った描画での特徴を理解できるような、ライブラリの使用例です。次回は、それを取り上げる予定です。

 

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

2014年12月8日月曜日

オフスクリーン描画をライブラリ化(本編2)

(注:数回後の投稿で行なった実験が終わったので、ソースコードの一部を修正しました。修正後の現状が正式版です)

オフスクリーン描画機能をライブラリ化する話の続きです。残りは描画用メソッドだけになったので、それをクラスに追加します。描くのは代表的な要素(図形や文字など)だけですが、それでも十分に役立ちます。

 

メソッドの具体的なコードを書く前に、描画メソッドの処理で考慮すべき点を考えます。

今回作る描画メソッドの多くでは、描画に用いる値をCGContextに設定します。必要な設定を終えてから描画しますが、その後始末はどうしましょうか。数回前の投稿で書いたとおり、CGContextは1つしか持てません。何かの値を変更してしまうと、それが残ったままです。次に別な描画で使う際に、悪影響が出ないとも限りません。

いろいろな描画メソッドを、他からの影響がないか注意しながら作るというのは、メソッド数が増えるほど大変です。もっとも良い解決方法は、他の描画メソッドの影響をまったく受けないように、各メソッドの作り方をルール化することです。

というわけで、描画メソッドの作り方をルール化しました。いろいろな設定はCGContextに対して行なうので、設定前のCGContextを保存しておき、設定変更して描画が終わった時点で、CGContextを元に戻すというルールです。具体的なswiftコードとしては、最初にCGContextSaveGStateを実行してCGContextを保存し、最後にCGContextRestoreGStateを実行してCGContextを元に戻します。これで、描画メソッド内でどんな設定をしても、他の描画メソッドに悪影響を与えません。

CGContextに何かを設定する全メソッドで、このルールを守ります。そうすれば、CGContextをどんなに変更しても気にしなくて済みます。他への影響を考慮しなくてよいというのが、もっとも作りやすい条件ですからね。

 

似たようなメソッドを複数持つ場合には、決めなければならないことが、もう1つあります。引数の並び順です。

並び順は好きに決めて構わないのですが、一般的な法則というか、守ったほうが良いルールはあります。必ず含まれる項目を先に、多く含まれる項目を次に、残りは最後にというルールです。含まれる率が多い項目ほど、前に並べるという考え方です。このルールを守ると、複数あるメソッド引数の並び順が揃って見えるようになります。

ただし、1つだけ注意点があります。関連する項目は近く似合ったほうが使いやすいので、項目をグループ分けしてから、含まれる多さをグループごとで評価するという考え方です。でも、グループ内で含まれる多さが違うと、結果として引数がバラバラに見えるようにもなりかねません。何事もバランスですね。実際に並べてみて判断するしかないようです。

今回のメソッドの引数では、次のような並び順に決めました。「登録色番号、主要属性、座標、大きさ、その他」という順番です。主要属性というのは、図形なら線の太さ、テキストなら文字列や文字サイズ、画像なら画像データなど、それぞれで重要な属性のことです。この並び順に沿って、各メソッドの引数を並べます。ただし、その他の中で何かと関係が深い項目があれば、関係の深い項目の近くに入れるでしょう。

 

図形を描く場合には、決めなければならないことが、もう1つあります。座標の指定方法です。たとえば、長方形を描くときを考えましょう。始点と終点を指定するのか、始点とサイズを指定するのか、どちらかを選ばなければなりません。

すべて統一する考え方もありますが、私の場合は、描くものごとに使い分けたほうが良いと考えました。直線を描く場合は、始点と終点を指定し、長方形や円などの場合は、始点とサイズを指定します。これが一番使いやすいのではと思いました。

 

考慮点が明らかになったので、ようやく直線を描くメソッドを作ります。直線を描くのに必要な情報は、線の太さに加えて、開始点と収容点の座標です。合計で5つの数値を用います。さらに、登録した色の番号も加え、引数は全部で6つとなります。

引数の並び順も最初に決めたルールどおり作ったのが、次のSwiftコードです。

// 直線を描くメソッド
func drawLineF(rNum:Int, _ rLWidth:CGFloat, _ rX1:CGFloat, _ rY1:CGFloat, _ rX2:CGFloat, _ rY2:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DL:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_DL:rNumが範囲超え"); return }
    let iContext : CGContext = UIGraphicsGetCurrentContext()
    // 現在のコンテキストを保存
    CGContextSaveGState(iContext)
    // 指定された登録値を、コンテキストに設定
    CGContextSetRGBStrokeColor(iContext, arySColor[rNum][0], arySColor[rNum][1], arySColor[rNum][2], arySColor[rNum][3])
    CGContextSetRGBFillColor(iContext, aryFColor[rNum][0], aryFColor[rNum][1], aryFColor[rNum][2], aryFColor[rNum][3])
    // 引数として受け取った値を、コンテキストに設定
    CGContextSetLineWidth(iContext, rLWidth)
    CGContextMoveToPoint(iContext, rX1, rY1)
    CGContextAddLineToPoint(iContext, rX2, rY2)
    // 線を描画
    CGContextStrokePath(iContext)
    // コンテキストを復帰
    CGContextRestoreGState(iContext)
}

メソッドの最初では、描画可能状態かどうかと、コンテキスト(現状では色のみ)の登録番号が配列の範囲内かを調べています。範囲外なら、エラーメッセージを出して何も描きません。エラーメッセージの形式は、前回と同じです。

配列の範囲内だと確認できたら、現在のコンテキストを保存するのが最初の処理です。メソッドの最後に、この保存したコンテキストを復帰させます。

続いて、CGContextに登録済みの内容(現状では色のみ)を設定します。さらに、引数として受け取った値を、同じくCGContextに設定して、描画する前の準備は完了。あとは、線を描くだけです。最低限の機能しか持たせていないので、メソッドは短くまとまっています。

値チェックの対象として、座標の値も含めることができます。たとえば、最初に設定したスクリーン範囲から、描いたものが外に出ているかをチェックするとかです。しかし、スクリーンの外にはみ出しても、UI部品の設定によっては、意識的に外側へ描かせることも可能です。厳密なチェックを入れると、より自由な使い方を制限しかねません。という理由から、座標の値はチェックしないことに決めました。

 

続いて、長方形を描くメソッドですが、その前に1つ準備が必要です。円や長方形のように、内側に空間のある図形は、枠線に加えて塗りつぶしも描けます。枠線だけなのか、塗りつぶしだけなのか、両方とも描くのか、3種類の中から選ぶ機能が必要です。

図形の形に関係なく、引数として共通で使える型を用意するのが一番です。そのための型を定義しますが、他のクラスでも共通して使えるように、クラスの外に出します。Swiftコードは、次のようにしました。

// 長方形や円を描く際の描画オプション
enum DrawType : Int {
    case None = 0
    case Stroke = 1
    case Fill = 2
    case Both = 3
}

何も描画しない「None」を加えたので、3つではなく、4つになっています。何も描画しない機能ですが、普通は使いません。でも、どう描くのかプログラム内で判断して、描画を使い分ける場合に役立ちます。何も描かない際に、条件分岐でメソッドを呼ばなくする代わりに、「None」を渡せば済み、すっきりしたソースコードで作れます。念のために入れておいた、という位置付けですね。

swiftでは、定義された型については省略形が使えます。「DrawType」型の変数に対して、「.Stroke」や「.Fill」の形で代入や比較が可能です。

 

準備ができたので、ようやく長方形を描くメソッドに取りかかれます。引数としては、定義した色の番号、線の太さ、座標、大きさ、前述のDrawTypeが含まれます。DrawTypeの順番は、最初に決めたルールどおりだと一番最後なのですが、色と関係が深いので、色番号の次に入れることにしました。

こうして出来上がったのが、次のSwiftコードです。全体の構成は、前述の線を描くメソッドと同じになっています。

// 長方形を描くメソッド
func drawRectF(rNum:Int, _ rDType:DrawType, _ rLWidth:CGFloat, _ rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DR:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_DR:rNumが範囲超え"); return }
    let iContext : CGContext = UIGraphicsGetCurrentContext()
    // 現在のコンテキストを保存
    CGContextSaveGState(iContext)
    // 指定された登録値を、コンテキストに設定
    CGContextSetRGBStrokeColor(iContext, arySColor[rNum][0], arySColor[rNum][1], arySColor[rNum][2], arySColor[rNum][3])
    CGContextSetRGBFillColor(iContext, aryFColor[rNum][0], aryFColor[rNum][1], aryFColor[rNum][2], aryFColor[rNum][3])
    // 引数として受け取った値を、コンテキストや変数に設定
    CGContextSetLineWidth(iContext, rLWidth)
    let iRect : CGRect = CGRectMake(rX, rY, rWidth, rHeight)
    // 長方形を描画 
    if (rDType == .Fill || rDType == .Both) {
        CGContextFillRect(iContext, iRect)     // 長方形を塗りつぶす
    }
    if (rDType == .Stroke || rDType == .Both) {
        CGContextStrokeRect(iContext, iRect)   // 長方形の枠線を描く
    }
    // コンテキストを復帰
    CGContextRestoreGState(iContext)
}

DrawTypeの値によって、塗りつぶすだけなのか、線だけなのか、両方とも描くのかが決まります。また、両方描いた場合の結果を考慮して、塗りつぶした後に枠線を描くような順序にしてあります。逆順にすると、枠線が細くなってしまいますから。

こちらも最低限の機能しか持たせていないので、コメントを除くと短いコードになっています。

 

円を描くメソッドは、長方形を描くメソッドとほとんど同じです。グラフィックAPIの描く命令を置き換えれば、簡単に作れます。Swiftコードは、次のとおりです。

// 円を描くメソッド
func drawCircleF(rNum:Int, _ rDType:DrawType, _ rLWidth:CGFloat, _ rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DC:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_DC:rNumが範囲超え"); return }
    let iContext : CGContext = UIGraphicsGetCurrentContext()
    // 現在のコンテキストを保存
    CGContextSaveGState(iContext)
    // 指定された登録値を、コンテキストに設定
    CGContextSetRGBStrokeColor(iContext, arySColor[rNum][0], arySColor[rNum][1], arySColor[rNum][2], arySColor[rNum][3])
    CGContextSetRGBFillColor(iContext, aryFColor[rNum][0], aryFColor[rNum][1], aryFColor[rNum][2], aryFColor[rNum][3])
    // 引数として受け取った値を、コンテキストや変数に設定
    CGContextSetLineWidth(iContext, rLWidth)
    let iRect : CGRect = CGRectMake(rX, rY, rWidth, rHeight)
    // 円を描画
    if (rDType == .Fill || rDType == .Both) {
        CGContextFillEllipseInRect(iContext, iRect)     // 円内を塗りつぶす
    }
    if (rDType == .Stroke || rDType == .Both) {
        CGContextStrokeEllipseInRect(iContext, iRect)   // 円の枠線を描く
    }
    // コンテキストを復帰
    CGContextRestoreGState(iContext)
}

当然ですが、エラーメッセージの略語も、円を描くメソッド用に変えています。

 

今度は、文字列を描くメソッドです。引数としては、文字列、文字サイズ、座標が含まれます。また、文字の色は、図形用として登録したものをそのまま使えるよう、登録した色番号を指定可能にしました。

文字列を描くためのAPIは、CGContextを使った形ではなく、文字列を扱うNSStringクラスのdrawInRectメソッドを使うようです。以前は別な方法もあったのですが、非推奨になったとか。実際に実験したら上手く動いたので、CGContextと関係ない点が少し納得いかないのですが、使うことにしました。Swiftコードは、次のとおりです。

// 文字列を描く
func drawTextF(rNum:Int, _ rText:NSString, _ rFontSize:CGFloat, _ rX:CGFloat, _ rY:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DT:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_DT:rNumが範囲超え"); return }
    // 指定された登録値の色を、UIColorに設定
    let iUIColor : UIColor = UIColor(red:arySColor[rNum][0], green:arySColor[rNum][1], blue:arySColor[rNum][2], alpha: arySColor[rNum][3])
    // 描画の準備:テキスト属性
    let iFont : UIFont = UIFont.systemFontOfSize(rFontSize)
    let iStyle : NSMutableParagraphStyle = NSMutableParagraphStyle()
    iStyle.alignment = .Left
    iStyle.lineBreakMode = .ByWordWrapping
    let iAttr : NSDictionary = [
        NSFontAttributeName: iFont,
        NSParagraphStyleAttributeName: iStyle,
        NSForegroundColorAttributeName: iUIColor,
        NSBackgroundColorAttributeName: UIColor.clearColor()]
    // 描画サイズを求める
    let iCGSize : CGSize = rText.sizeWithAttributes(iAttr)
    let iCGRect : CGRect = CGRectMake(rX, rY, iCGSize.width, iCGSize.height)
    // テキストの描画
    rText.drawInRect(iCGRect, withAttributes:iAttr)
}

見てのとおり、CGContextを使わないので、CGContextの現状を一時保存して復帰させる処理は含まれていません。

文字列の色はUIColorとして用意し、文字属性に入れます。文字の周辺を塗りつぶさないように、バックグラウンドは透明(clearColor)にしました。日本語フォントが選べるほどないため、とりあえずシステムフォント固定にです。将来は、フォント名を指定する形に変更すると思います。また、文字スタイルは使うことがないと判断して、固定にしました。こちらも、もし必要になったときには変更するつもりです。

 

最後に作るのは、UIImageを描くメソッドです。UIImageを受け取って、指定された座標に指定サイズで配置するだけです。クラス名とかぶるようなメソッド名ですが、仕方ないでしょう。登録したコンテキストの値(現状は色のみ)を使っていないので、先頭でのチェックは不要です。Swiftコードは非常に簡単で、次のとおりです。

// UIImageを描くメソッド
func drawImageF(rImage:UIImage, _ rX:CGFloat, _ rY:CGFloat, _ rWidth:CGFloat, _ rHeight:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_DI:Disabled"); return }
    let iRect = CGRectMake(rX, rY, rWidth, rHeight)
    rImage.drawInRect(iRect)
}

このメソッドですが、意外にも、非常に重要なのです。このクラスで描いた結果もUIImageで保存します。ですから、このクラスを使って作った部品を、このメソッドを使って、別なイメージに配置したりできます。たとえば、直線を組み合わせ、三角形を描いてUIImageとして作ったとします。このメソッドを使えば、その三角形を、別なイメージに何個も描けます。同じ部品を複数使う場合に役立つでしょう。

また、凝ったデザインで描きたい場合も重宝します。まず、一般的なグラフィック用アプリで複雑な絵(ビットマップ画像)を描き、PNG形式で保存します。それを読み込んでUIImageに設定すれば、このメソッドでそのまま描画できます。ここで用意したメソッドを組み合わせて、絵を作る必要はありません。複雑な形になるほど、プログラムで描くよりも、絵として描いて貼り付けたほうが簡単ですし、仕上がりも良くなります。

PNG形式の絵は、できるだけ大きめに描いて保存し(ただしファイル容量だけは要注意)、必ず縮小して使うようにします。そうすれば、画面表示でも印刷でも、デバイスが持つ最大の解像度が生かせます。

 

以上、長くなってしまいましたが、主な描画メソッドを紹介しました。かなり単純なものしかありませんが、これで意外にも間に合っています。PNG形式の絵を追加すれば、大抵の用途で何とかなりますから。

もっと違う図形、たとえば三角形や五角形を描くメソッドも考えましたが、使用頻度が少なそうなので止めました。これらは、直線を描くメソッドの組み合わせで描け、一旦部品化して何個も配置できますから。もし今後、何か必要になったら、その段階で作るつもりでいます。誰にでも言えることですが、自分専用のライブラリですから、必要な時点で作るという方針で構わないと思います。

 

ここでの説明では、APIの詳しい解説を省きましたので、Appleのサイト資料か、他の親切なブログで調べてください。作る際の考慮点に重点を置いて説明したので、別の面で参考になるのではないかと思います。

いよいよ次回は、ここまで作ったライブラリの上手な使い方を取り上げる予定です。読みやすいソースコードのためには、使い方も意外に重要だったりします。なぜ色を登録する形にしたのか、その理由も明らかにできるでしょう。

 

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

2014年12月6日土曜日

オフスクリーン描画をライブラリ化(本編1)

(注:数回後の投稿で行なった実験が終わったので、ソースコードの一部を修正しました。修正後の現状が正式版です)

オフスクリーン描画機能をライブラリ化する話の続きです。いよいよSwiftでソースコードを作る段階です。大まかな構造は決まっていますから、swiftだと簡単に作れます。ライブラリ用のクラスとして実現するので、クラス内に配列を用意して、CGContextで使う色設定を保存する必要があります。

 

まずは入れ物となるクラスを作ります。クラス名は少し長めですが、DrawImageOffScreenとしました。オフスクリーン以外の描画ライブラリも作ると予想し、DrawImageシリーズとして統一しようと思っての命名です。

クラス内には配列を用意して、CGContextの色設定を保存する必要があります。線色(StrokeColor)と塗りつぶし色(FillColor)の両方を。また、それぞれの色の初期値も先頭で定義しておくべきでしょう。Swiftコードは、次のようになります。

// オフスクリーン描画機能をライブラリ化したクラス
class DrawImageOffScreen {
    // 色のデフォルト値
    let COLOR_DFAULT_STROKE : [CGFloat] = [0.0, 0.0, 0.0, 1.0]  // 黒で不透明
    let COLOR_DFAULT_FILL : [CGFloat] = [1.0, 1.0, 1.0, 1.0]    // 白で不透明
    // CGContextに設定する値の入れ物
    var numContext : Int = 0
    var arySColor : [[CGFloat]] = []   // StrokeColor
    var aryFColor : [[CGFloat]] = []   // FillColor
    // 描画可能フラグ
    var enabledDraw : Bool = false
}

色の初期値としては、線色が黒の不透明、塗りつぶし色は白の不透明にしました。中を塗りつぶさない場合の指定は、図形を描く命令で可能なので、中を塗りつぶす場合のデフォルト値として白を選びました。

配列の要素数を示す変数名は、numContextとしました。今は色情報だけですが、他の情報も加えたとき、変数名を変更しなくて済むようにとの配慮です。

さらに、描画可能状態かを示すフラグenabledDrawも加えます。これがtrueのときだけ、設定や描画を可能とします。

このクラスの中に、いろいろなメソッドを追加することになります。

 

最初は、クラスの初期化です。できるだけ少ない行数でクラスを使いたいので、開始に必要な値を初期化に含めています。イメージの大きさ(幅と高さ)、背景の透明不透明、使用する色数だけです。

入れるかどうか迷ったのが、UIGraphicsBeginImageContextWithOptionsの引数scaleです。自動判定を意味する0.0のまま使い続けそうなので、今回は外しました。必要になった時点で入れればよいとの判断です。

// サイズを指定して、空のイメージを生成する
init(_ rWidth:CGFloat, _ rHight:CGFloat, _ rOpaque:Bool, _ rNumContext:Int) {
    if (rNumContext < 1) { println("ERROR:DIOS_IN:rNumContextが小さすぎ"); return }
    // コンテキストのデフォルト値を設定
    numContext = rNumContext
    for i in 0..<rNumContext {
        arySColor.append(COLOR_DFAULT_STROKE)
        aryFColor.append(COLOR_DFAULT_FILL)
    }
    // オフスクリーン描画の開始(rOpaqueがfalseで背景が透明に,scaleが0.0で自動)
    let iCGSize : CGSize = CGSizeMake(rWidth, rHight)
    UIGraphicsBeginImageContextWithOptions(iCGSize, rOpaque, 0.0)
    enabledDraw = true   // 描画可能に設定
}

ここでも「色のデフォルト値を設定」ではなく、「コンテキストのデフォルト値を設定」としました。色以外の情報を追加したとき、コメントを修正しなくて済むようにとの配慮です。

一番最初に、最低限のエラーチェックを入れいています。rNumContextに1より小さい値を指定した場合は、エラーメッセージを出し、何もしないで終わります。

エラーメッセージのフォーマットは以前からほぼ固定です。必ず「ERROR:」で始まり、エラーを出したクラス名の省略形と「_」が続き、その中の場所を示す省略形と「:」が加わります。最後にエラー内容を示す文字列を付けるという、ありがちな形式です。今回の場合は、「DrawImageOffScreen」の省略形が「DIOS」で、「init」の省略形が「IN」ですね。

 

続いて、初期化とペアになる終了処理を書きます。描いた画像をUIImegeとして返し、描画処理を終了するメソッドです。処理内容は非常に簡単で、次のようなSwiftコードになります。

// 出来上がった合成イメージを返す
func getFinalImageF() -> UIImage {
    if !enabledDraw { println("ERROR:DIOS_GI:Disabled"); return nil }
    // 描き終わった内容をUIImageに保存する
    let iImage : UIImage = UIGraphicsGetImageFromCurrentImageContext()
    // 描画の終了
    enabledDraw = false   // 描画不可能に設定
    UIGraphicsEndImageContext()
    return iImage
}

処理内容に関しては、とくに説明する必要はないでしょう。最初に描画可能かどうか調べて、可能なら処理を進めます。最後に、描画不可能に設定してから、コンテキストを終了してます。

迷ったのはメソッド名ですね。このメソッドには、2つの役割が含まれます。出来上がったUIImageをゲットする役割と、描画機能を終了する役割です。ゲットのほうを重視しながら、終わりを意味する「Final」を入れてみました。イマイチですね。

ここでは2つの役割を1つのメソッドに含めましたが、別々のメソッドとして作るという選択肢もあります。別々に作った場合の短所は、描画処理全体の最後に、余計なメソッドを呼ぶ必要があることです。逆に長所は、途中まで描いた段階でもUIImageを取得できることです。途中の段階と最終段階の両方をUIImageで得たい処理のとき、無駄な手間が不要となります。実際には小さい違いなので、好きな方を選んで構わないと思います。

 

次は、CGContextに設定するための色を指定するメソッドです。色には、線色と塗りつぶし色があり、それぞれ透明度まで含めると4つずつの値が含まれます。それを一気に指定すると、多すぎる引数となります。また、線を描く場合などは、線色しか使いません。やはり、別々のメソッドとして作るほうが無難でしょう。

そうして作ったSwiftコードは、次のとおりです。

// コンテキストの線色を設定する
func setContextStrokeColorF(rNum:Int, _ rRed:CGFloat, _ rGreen:CGFloat, _ rBlue:CGFloat, _ rAlpha:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_SC:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_SC:rNumが範囲超え"); return }
    arySColor[rNum][0] = rRed
    arySColor[rNum][1] = rGreen
    arySColor[rNum][2] = rBlue
    arySColor[rNum][3] = rAlpha
}
// コンテキストの塗りつぶし色を設定する
func setContextFillColorF(rNum:Int, _ rRed:CGFloat, _ rGreen:CGFloat, _ rBlue:CGFloat, _ rAlpha:CGFloat) {
    if !enabledDraw { println("ERROR:DIOS_FC:Disabled"); return }
    if (rNum >= numContext || rNum < 0) { println("ERROR:DIOS_FC:rNumが範囲超え"); return }
    aryFColor[rNum][0] = rRed
    aryFColor[rNum][1] = rGreen
    aryFColor[rNum][2] = rBlue
    aryFColor[rNum][3] = rAlpha
}

どちらのメソッドでも、描画可能かどうかと、配列の範囲を超えてないかだけはチェックしています。エラーメッセージはほとんど同じですが、それぞれのメソッドを示す省略形だけが違っています。

4つの値のチェックも入れるべきか迷いましたが、入れないことにしました。表示された色を確認しながら開発するはずなので、間違えたら気付くだろうと思ったからです。また、値が範囲を超えても、異常終了したりせず、超えた分を無視して処理されるからです。たとえば、1.0を超えた値を指定すると、1.0として処理されます。

 

このままだと配列の要素数が、最初に指定したまま固定になっています。使う側の処理内容によっては、どれだけ使うのか途中で判断し、追加しながら使いたい場合もあるでしょう。

そこで、配列を追加するメソッドだけを加えました。追加する個数を指定する形にしてます。次のように、Swiftコードは非常に簡単です。

// 指定された個数だけコンテキストを追加する
func addContextF(rNumAdd:Int) {
    if !enabledDraw { println("ERROR:DIOS_AC:Disabled"); return }
    numContext += rNumAdd
    for _ in 1...rNumAdd {
        arySColor.append(COLOR_DFAULT_STROKE)
        aryFColor.append(COLOR_DFAULT_FILL)
    }
}

このメソッドでコンテキストを追加した後、追加した数の分だけ、前述の色設定メソッドを使って、線色と塗りつぶし色を登録することになります。正しく使っているか確認する意味で、描画可能かどうかもチェックしています。

配列から削除する処理は面倒なので作りませんでした。追加があるだけでも便利に使えますし、イメージを作り終わったらメモリーを開放するでしょうから、削除が無くても困らないと考えました。

 

ここまでで、描画を担当するメソッド以外は作り終えました。このまま続けると、さらに長くなりそうなので、ここで一旦切ります。残りのメソッドは、次回ということで。

 

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

2014年12月5日金曜日

オフスクリーン描画をライブラリ化(全体設計編)

オフスクリーン描画機能をライブラリ化する話の続きです。今回は、全体設計の話を書きます。作り始める前に、どのような形で作るのか決めておかないと、無駄な作業が増えますからね。

 

私がオフスクリーン描画機能を使うのは、画面で表示する描画部品に加え、印刷内容を描くときです。いくつもの要素を描くのですが、同じ色で描く要素が多くあります。より具体的には、数十個の要素を、数色の色で描くという感じです。

実現したい動作としては、使う色をあらかじめ登録しておき、登録した色を指定しながら要素を描くという形です。複数の色を登録する機能、その色を使って図形や文字を描く機能が必須です。また、描くサイズを指定して描画を開始する機能、描き終わった内容をUIImageに出力する機能、描画を終了する機能も必要となります。

今回の描画機能は、クラスとして実現します。手短くまとめて、次のような全体像としました。

オフスクリーン描画機能のクラス
・描画の開始メソッド(描画サイズなどを指定)
・描画の終了メソッド(UIImageに出力して描画を終了)
・色の登録メソッド(線色と塗りつぶし色を指定:複数の登録が可能に)
・各種図形や文字の描画メソッド(登録した色を選択:大きさや線幅なども指定)

まとめると以上ですが、具体的に作り始めると、各種図形の部分が何種類にもなります。使うものから作り始めて、順次増やしていくのが現実的でしょう。

 

図形を描くメソッドと、色の登録メソッドを分けたのには、別な理由もあります。図形を描画するメソッドには、位置や大きさなど何個もの引数が必要です。これに色を加えると、引数が非常に多いメソッドになります。メソッドを使う側のソースコードには、引数が多いメソッドが並んで、美しくないですし、処理内容を読み取りづらくもなります。

色の指定を別なメソッドとして用意し、描画メソッドでは登録色を選ぶだけだと、描画メソッドの引数が減らせます。また、同じ色を何度も指定する無駄も解消できます。色の登録を別メソッドに分割することで、使う側のソースコードを読みやすくするという効果もあるのです。

 

全体像が決まったので、実現方法の検討に移ります。決めた形で実現できなければ、全体像を作り直さなければならないため、実現方法の検討は大事です。

iOSの描画機能では、色の指定も含めてCGContextを使います。まずCGContextを用意して、色や線の太さなどを設定します。設定したCGContextを指定しながら、目的の図形を描く命令を実行するという手順です。

最初は、次のように考えました。CGContextを複数保持して、それぞれのCGContextに色を設定します。図形を描く際には、目的の色に設定されたCGContextを指定して、描画命令を実行するという形です。

この考え方で実現できるかどうか、実験専用アプリで事前試験をしてみました。すると、大変なことが判明しました。必要な数だけCGContextを保持(変数で参照)したあと、あるCGContextを変更しても、全部のCGContextが変わってしまいます。当たり前ですね。同じCGContextを参照しているからです。

これを防ごうと、CGContextの深いコピーを作ろうとしました。でも、CGContextにはcopyメソッドが実装されておらず、コンパイルエラーになってしまいます。さらに、CGContextを新規で作る機能も用意されていないので、CGContextを複数保持して切り替えるという当初の案は暗礁に乗り上げてしまいました。

 

こんなことぐらいでは、あきらめきれません。こうなったら作戦変更です。CGContext自体を保持するのではなく、CGContextに設定する値を複数保持して、描画する直前にCGContextへ設定する方法を試します。

CGContextに設定できる属性は多数あります。全部を保持すると大変なので、今回は色だけに限ることにしました。線色と塗りつぶし色の2つだけです。この2つを登録できるだけでも、私に取っては十分に便利な描画機能になりますから。

この実現方法なら、あらためて実験する必要はありません。今まで使ったことのある機能の組み合わせで、簡単に実現できてしまいます。作成の障害はなくなりました。あとは実際に作るだけです。さらに細かな仕様は、これまでと同じように、実際に作るとき考えましょう。

 

作り始める前に考えたり実験したのは、以上のようなレベルまでです。ここまで整理できていると、あとは淡々と作り進むだけですね。実際、一気に作り終わりました。次回から、具体的なソースコードを紹介します。

2014年12月4日木曜日

オフスクリーン描画をライブラリ化(前書き編)

当ブログでは、よく使う機能を独自のライブラリ化している話が何度も出てきます。独自なのは本当ですが、たいしたことはやっていません。小型プリンターRoltoの場合は深く考えて設計したので、特別に取り上げました。でも、他の機能に関しては、ほとんど普通の作り方だと思います。

ただし、少しずつですが改良はしています。最初のうちは、iOSのAPIに詳しくないので、自分にとっての使いやすさを前面に出して設計していませんでした。でも、APIの特徴を理解し始めるとともに、ライブラリにも改良を加えたくなります。

最近、オフスクリーン描画のライブラリを改良しました。自分なりに考えて設計したので、もしかしたら誰かの参考になるかもしれません。また、iOSの描画機能を理解する手助けにもなると思います。これから数回に分けて、要点を紹介します。

 

中身に入る前に、2次元描画APIに関する私の理解レベルについて触れます。ハッキリ言ってしまうと、iOSの2次元描画機能は分かりづらいです。まず最初に、Quartz 2DとかCore Graphicsとか複数の名前があって、その範囲を正確には理解できていません。もしかしたら、今使っているAPIよりも良いAPIがあり、本来はそちらで作るべきかもしれないとの不安も少し持っています。もし間違って理解している箇所を見付けたら、正しい理解や使い方を、ぜひ教えてください。

一応、いろいろと調べてみた結果として、次のような内容が得られました。名前がUIGraphicsで始まるAPIを使って描画の開始や終了、出力先などを管理します。具体的な描画は、名前がCGContextで始まるAPIを使い、細かな設定が可能であることも知りました。これら2つの組み合わせが、オフスクリーン描画機能を作るのに必要なAPIの全体像となります。

 

もう1つ、大事な点があります。描画内容を作っている処理中は、まったく描画していないことです。単に描画する内容を構築しているだけで、それが終わったときに得られるのが、描画内容を記したコマンド集のようなものです。最終的にUIImageの形で作ったとしても、やはり描画コマンド集です。もしビットマップ画像を貼り付けた場合は、その画像へのリンクが含まれた描画コマンド集となります。

この描画コマンド集は、解像度に依存しません。ですから、すべての座標に浮動小数点のCGFloatを用いています。実際に描画されるのは、画面に表示されたり、プリンタに印刷されるときです。画面に表示する際には、ハードウェアの解像度に応じて、描画するビットマップ画像を生成するのでしょう。ディスプレイがレチナの場合は、縦横とも2倍の画素数(全体では4倍の画素数)を持ったビットマップ画像を生成するはずです。

このような仕組みなので、オフスクリーンで描画していると思っている処理は、まったく描画していなくて、描画するコマンド集を作っているだけです。ですから非常に高速で処理され、そこで長い時間が取られることはありません。

実際に処理時間がかかるのは、画面に描画するときです。当然GPUを使うので、描画内容が単純であれば一瞬で終わります。描画内容が複雑になったとき(つまり、描画コマンドの数が非常に多いとき)、遅いと感じるほどの時間を使うのでしょう。それを少しでも速くするには、重なる部分で透明度を使わないとか、描画計算時間が短くなるように工夫する必要があります。その工夫を、描画内容を作る際に行なうことは、言うまでもありません。

 

実際に作ってみて、分かってきたこともあります。先に結論を行ってしまうと、制限が意外に多いということです。何でも自由に制御できる形にはなっていません。制限を理解して、上手に使う必要があります。

たとえば、CGContextです。線の色とか太さとか、描画内容を指定するときにCGContextを使います。このCGContextは、自由に保存して、自在に切り替えるようにはなっていません。保存して切り替えようと思いましたが、保存するための機能がなく、深いコピーもできませんでした。swiftでの深いコピーにはcopyメソッドを使いますが、そんなメドッソはないとコンパイラーに怒られました。

swift上ではCGContextという型の名前ですが、Objective-C上ではCGContextRefという違う名前で、参照を意味するRefが付いています。通常のオブジェクトもインスタンスへの参照ですが、わざわざRefとは付けません。おそらく、何か特殊な参照なのでしょう。

この辺の制限の話は、ライブラリを使う段階で詳しく取り上げたいと思います。もちろん、私が知っている範囲でですが。

 

制限が多い理由も、作っているうちに理解してきました。もっとも大事なのが、UIGraphicsやCGContextで作る描画コマンド集は、実際に描画するための部品を作るのが役割だという点です。最終的な表示内容ではなく、あくまで描画に使う「部品」です。その部品を組み合わせて、最終的な表示内容を画面上に構築します。部品ごとに配置を変え、実際に表示する際に描画するわけです。

アニメーションで使う場合が、一番理解しやすいでしょう。動かす最小単位を部品として作り、部品ごとに異なる動きを与えます。同じ動きをするものは1つの部品内に集めて、一緒に作ります。作った中身は、前述のように描画コマンド集です。

このような役割なので、並行して複数を作る必要はありません。あくまで描画する前の準備段階であり、描画で使う部品を作るわけですから、1つ1つの部品を順番に作っていくわけです。そのため、常に1つの部品だけを描画していて、「どの部品に対して描画するのか指定する機能」は持っていません。必要な描画が終わったら部品を出力し、別な部品の描画を開始します。あくまで「1つ1つの部品を順番に作る」なのです。

 

以上の内容ですが、最初はまったく理解できていませんでした。最初に作りたいと思った形(CGContextを自在に切り替えて描画する形)ではライブラリが実現できず、少しずつ別な方法へと変更していった中で、だんだんと分かってきたことです。

作成したライブラリを使い、複数のインスタンスを生成して動かした結果を見て、制限があることと、その制限の理由が理解できました。この辺の詳しい話は、ライブラリを使って動かした結果を知ることが、もっとも理解しやすい方法だと思います。そのため、一番最後に取り上げるつもりです。

ここまでの話から分かるように、オフスクリーン描画機能を作り進む中で、大事な点に気付きました。描画しておらず、描画で使う部品を作っていることです。これって「オフスクリーン描画機能」と呼んで構わないの?という素朴な疑問が生じました。当然ですね。でも、機能としては「オフスクリーン描画機能」が一番しっくり来るというか、中身の機能をイメージしやすいので、その名前で作り進めることにしました。将来は変更するかもしれませんが。

話が長くなりましたので、一旦ここで切ります。ライブラリを作る話は、次回からですね。まずは、全体設計の話になると思います。実際のソースコードが登場するのは、その後でしょう。

2014年12月3日水曜日

Xcodeバージョンの切り替え時期で悩む

Xcode 6.1.1が正式に、App Storeに登録されましたね。少し前から、Appleの開発者向けサイトには何日も前からGMが登場していたので、正式版が出るのは時間の問題でした。

新しいバージョンの開発ツールが登場すると、開発しているマシンに入れる時期を毎回のように悩みます。皆さんは、どのように考えて判断しているのでしょうか。ちょっと知りたい気持ちです。私の場合の悩める思いを書いてみます。

 

Xcodeの現状は、開発環境として特別な状況です。swiftが登場したばかりですから、swift自体の安定性が良くなっているのか気になります。また、コンパイラーが使うswift用のAPI定義も、一部に間違っているものが見付かって、少しずつ修正されているでしょう。できるだけ新しいものを使いたい、との思いが強まります。

でも、新しいものだけに、大きく切り替わった時期というのは、大きなバグが出やすい時期でもあります。バグに遭遇して苦労したくない、つまり余計なことで開発を邪魔されたくない気持ちも大きいです。新しいものを使いたいけど、新しいものは危ないという、実に困った状況なのです。

 

私が現在使っているのは、Xcode 6.0.1です。まあまあ安定していて、大きなトラブルにも遭遇せず、開発を進められいています。開発が最近完了し、本番用としてビルドしたのも、現状のXcode 6.0.1でした。実機テストでも特に問題が発生しなかったので、安心してビルドすることができました。

並行して、Xcode 6.1も試しましたね。開発マシンとは別の実験用マシンにインストールして、開発マシンと同じプロジェクトをビルドしてみました。中身がかなり変わっているのでしょう。いろいろと問題が発生しました。

まず、ソースコードを編集中に、文法チェックしている処理が、何度も何度も異常終了しました。その度にメッセージが出て、文法チェックが止まります。こんな状態では、使っていても気分が悪いです。こんなに悪い状態なのに、よく公開したなと思いました。

さらに、6.0.1でコンパイルエラーが出なかったソースコードが、コンパイルエラーになります。Optionalの扱いが変わったというか、おそらくswift向けAPI定義が少し修正されたのでしょう。詳しく覚えていませんが、「!」を削除しなさいとか、「!」を追加しなさいとか、エラーと修正指示が何個も出ました。それは指示どおりに修正したら、問題なく動きました。

一番困ったのは、実機での原因不明な異常終了です。シミュレーター上では正常に動いているのですが、ビルド後に実機へ転送して動かすと、途中で異常終了します。その箇所のソースコードを調べても、問題がありそうには思えません。そもそも、Xcode 6.0.1では正常に動いているソースコードです。

動かない箇所をコメント化して、再び実機で動かすと、また別な箇所で異常終了します。3つめの異常終了が出た時点で、使うのをあきらめました。App Storeのコメントでも、さんざんの言われようでした。私も同感です。

 

私の場合、新しい開発ツールには真っ先に触りたいと思わないので、最新ツールには興味がありません。ただし、前述のようにXcodeの現状が特別な状態です。swift向けAPI定義の修正がされているバージョンだけに、使いたい気持ちは強くあります。だからこそ、悩ましいわけです。

通常の開発ツール選びでは、最新版を使いません。バージョンが大きく切り替わったとき、1つ前のバージョンの安定版を意識的に選びます。もしXcodeが相当前からswiftを採用していたら、おそらく5.xの最終版を使っていたと思います。でも、iOSの新バージョンに含まれる、ぜひとも使いたい機能が登場していた場合は、話が別です。仕方なく、最新版を使うでしょう。ただし、その場合でも、新しい機能を使うアプリ開発でだけ最新のXcodeを使い、他のアプリ開発では1つ前のバージョンのXcodeを使うでしょう。

 

で、Xcode 6.1.1です。通常のバージョンアップであれば、かなり安定しているのだろうと予測します。けれども、前の6.1が相当にひどかっただけに、試しに使うのも躊躇します。実機でだけトラブるのは、もう勘弁してほしいです。

とりあえずの判断としては、世間の評判を見定めて、評判が良いなら実験マシンにインストールしようと思います。既存の完成アプリをビルドしてみて、実機での動きが問題ないようなら、開発マシンにインストールする予定です。幸い、現在開発中のアプリはなく、切り替え時期としては最適ですから。

2014年12月1日月曜日

Titanium mobileからXcode+swiftへと移行

今ではXcode+swiftで普通に開発していますが、その前はTitanium mobileで開発していました。移行しようと判断した理由などを、簡単に書きます。

 

iPad用のアプリは、数年前に開発を始めました。まず最初に、開発ツール選びで迷いました。細かい部分まで作れて、そこそこ作りやすいツールを探したところ、当時はTitanium mobileがベストだと感じたからです。確か、Titanium Studioが出始めた頃だったと思います。Titanium mobileのバージョンが1.6とか1.7ぐらいでしたかね。

プログラミング言語は、お馴染みのJavaScriptです。クラスは使えないものの、オブジェクト指向っぽい書き方ができます。データ型の扱いが緩い感じで、個人的にはあまり好きになれない言語でした。ただ、関数中心という点だけは好きでしたね。

ツールの使い方や、Titanium mobileのAPIは、日本語の情報が出始めていたので、大まかな理解は素早くできました。あとは、TitaniumのウェブサイトにAPIの解説が載ってますから、それを参照しつつ、開発を進めました。

ただし、Titaniumのウェブサイトは最小限の情報しか載っていません。また、その場で見れるサンプルも非常に少ないです(この点は、Appleだとさらに悪いです)。細かな機能を使おうとすると、ウェブ上の資料だけでは目的の機能かどうか判断できませんでした。結局、自分でコーディングして動かしながら、期待したとおりの動作かどうか調べ、目的の機能を探していった感じです。余計な手間を多くかけたなと、今でも思い出します。

 

Titanium mobileを使った開発で、一番困った点はバグです。バージョン1.8ぐらいまでの時期は、描画関係に大きなバグがあり、それを回避しようと小技を多用しました。普通に描画すると大きく乱れて描かれるのが、透明度を設定すると乱れないことを発見して、それを使って回避するという技も見付けました。

画像を描画するアプリの開発では、描画関係のバグは致命的です。画面上での様々な演出を考えたのですが、バグを回避できなければ採用できません。回避方法を試行錯誤したおかげで、7割程度の描画演出が、何とか作れました。本当に苦労したのを覚えています。

Titanium mobileのバグで苦労している間、Xcode+Objective-Cでの開発も並行して勉強し始めました。Objective-Cでの書き方が、どうしても好きになれず、暇なときに勉強という感じでした。そのうちに、Titanium mobileを使ったアプリが完成し、Objective-Cの勉強も中断しました。

その後、iPadのアプリ開発はありませんでした。過去に作ったアプリの小さな改良はありましたが、Titanium mobileをそのまま使って、機能を少し追加しただけです。

私は今まで、色々な言語を経験してきました。もっともマシンに近い言語だと、大型汎用機やパソコンのマクロアセンブラです。他にも、フォートランやCなど古い言語から、Javaなどのオブジェクト指向言語も使いました。そうそう、若い頃はLISPも使ったことがあります。どれでも作ろうと思えば作れますが、今はモダンな言語を選んで使う時代でしょう。Objective-Cは、もう形式が古い言語だと思いますね。

 

こんな感じで時間が流れている間に、Appleからswiftが発表されました。いちおう言語仕様を見てみたら、かなり気になる存在に思えます。これは使ってみるしかないと判断しました。

本格的にマスターするには、ある程度の大きさのアプリを作るのが一番です。過去にTitanium mobileで作った中から業務アプリを選び、すべてswiftで書き直してみました。いろいろな要素が含まれるアプリだったので、これを作り終わったら、かなりのノウハウが蓄積できると考えたからです。

新言語といっても特殊な点は少ないので、マスターするのに苦労しません。もっとも大変なのは、数多くあるAPIの理解です。同じ機能を実現するにしても、Titanium mobileで使っていたAPIと、iOSのAPIでは名前すら違うため、一から勉強し直しとなります。どんなAPIがあるのか調べながら、1つ1つ機能を作っていきました。

ネットで検索すると、Objective-Cで作った例がいくつも見付かります。後で知ったのですが、Objective-Cとswiftでは、APIの定義値の名前などが微妙に違うのです。この微妙に違うという点が曲者で、swiftだと短めに区切ってある名前がいくつもありました。Objective-Cで使う名前が長いと批判されたのを、それを受けての変更だと思います。結果として、コンパイルエラーを消すためにAPI資料やサンプルを探しまくりました。

探しまくったのには、理由があります。Swift専用の短い名前は、Appleのサイト内のAPI資料にも載ってないことも結構あり(これは本当にひどすぎます)、誰かが使っているサンプルを頑張って見付けたりして、本当に余計な手間がかかりました。使っている人は、どこから探し出したのでしょうか。Appleのサンプルにでも載っているのでしょうか。

実際、今でもswift側だけ載っていない定義値の名前があります。もしかして、古いバージョンを見ているのでしょうか。クラス名やメソッド名をGoogleで検索して、トップ近くに出てくるページなので、古いバージョンとは考えられないのですが。

まあ、私の場合は、ソースコードを再利用しやすくライブラリ化するので、一度知ってしまえばOKです。作成した共通ライブラリを呼び出すだけなので、細かいコードを暗記してなくても大丈夫なのです。こうして共通ライブラリを1つずつ作りながら、業務アプリを完成させました。結果として、業務アプリが出来上がっただけではなく、いくつかの共通ライブラリも同時に得られました。

作成した共通ライブラリは、すべての機能を最初から作るのではなく、必要な機能だけを作ります。最初のうちは機能が少ないのですが、利用するアプリの数が増えることで、少しずつ機能が加わります。これからも、どんどんと成長していくでしょう。

 

Titanium mobileは、iOS用だけでなく、Android用のアプリを開発できる点も大きな特徴です。でも、私の周囲には昔からのMacユーザーがばかりで、Androidを持ってすらいない人ばかり。Android用のアプリを作ってくれと言われたことが、一度もありません。そのため、Titanium mobileを使わなくなっても、ほとんど困らないのです。

といった私なりの特殊状況のため、安心してXcode+swiftへと移行できたのでした。使ってみると、やはり純正ツールは一番良いなと感じました。アニメーションなどの機能が簡単に使えるし、珍しい機能を使っても、余計なバグに遭遇する可能性は相当減りました。当たり前ですが、開発ツールは純正が一番ですね。最新バージョンへのタイムラグもないですし。

試しに移植した業務アプリも無事に完成し、たまたま機能追加の依頼があったので、swift版に切り替えることにしました。本番での切り替えも順調に進み、今後はswift版だけをメンテします。

 

新言語のswiftは、使い始めから気に入りました。データの型を厳密にチェックするので、自分の間違いをコンパイル時に見付けやすくなります。厳しい型チェックを嫌う人もいますが、私は大好きです。

この型チェックを積極的に使うように、コーディングしています。具体的には、varやletで変数を宣言するとき、型やクラス名をあえて付けて、自分の理解が正しいかどうか、コンパイラーにチェックしてもらってます。値を入れられる側の変数で型宣言すると、宣言した型に必ずなりますから、以降のソースコードでエラーが出たときも、原因を追及しやすくなります。ソースコードの文字数は増えますが、今後も型宣言をどんどん入れて書こうと思います。

swiftの総評ですが、きっちりと書きたい派の私にとって、非常に良いプログラミング言語だと感じました。もちろん、小さな欠点はありますが、許容範囲に入ってます。いろいろ言われている細かな欠点は、さらに使いやすくバージョンアップしてほしいですね。全体としては、非常に魅力的なプログラミング言語だと思います。この言語が広く普及し、Apple以外の環境でも使えるようになってもらいたいです。