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)

0 件のコメント:

コメントを投稿