CALayerでお絵かきpart2

CALyerでお絵かきで来てくれる方が結構居ますがそんなに詳しい事書いていない上にretinaに対応していないというお粗末な解説なのでもうちょっと再利用出来る解説に…できるのか?

描画した内容をCALayerにする大まかな仕様、
  • UIImageViewを用意し背景となるUIImageとCALayerで描いた内容を流し込むUIImageと背景のCALayerに設置するCALayerが必要となります。
  • Retinaに対応する場合全てをRetina仕様にする必要があります。
  • 画像のScale変更させるなら描き出した線のScaleも合わせる ※1
  • 画像のサイズ変更出来る仕様の場合必ずUIImageViewのtransformでscaleを変更する ※2

※1 . 描画サイズはframe.sizeに対応するがCALayerはboundsに対応するため、描いた内容とCALayerに流し込む内容では大きさが異なります。
※2 . 画像をリサイズするとboundsの値が変更され色々と処理が増えるため

描画までのおおまかな流れです、
  • まずはキャンバスとなるUIImageViewとUIImageを用意しますが、ここで必ず確認しないといけないのがUIImageのscaleです。もしRetinaなどに対応する場合必ず対応したものに書きなおして下さい。
パスで画像を取り込む場合scaleで[[UIScreen mainScreen] scale]などして値を取得
- (nullable UIImage *)imageWithData:(NSData *)data scale:(CGFloat)scale
  • 次にCALayerに描画するので既に書き込まれたもしくはこれから書き込むCALayerをUIImageViewから取得してきます。ここで取ってくる値(objectAtIndex:)を変える仕様にするとレイアー構造に出来、数字が大きいほど前面に来ます。
CALayer *DummyLayer = [Canvas.layer.sublayers objectAtIndex:0];
DummyLayer.contentsScale = [[UIScreen mainScreen] scale];
  • 毎回元画像を呼び出すのはコストが高いためCALayerを流し込む予備の画像をCALayerと元に作成します。新規作成してしまうと元の画像が残らないので必ずCALayerを画像にします。
UIImage *DummyImage = [[UIImage alloc]initWithCGImage:(CGImageRef)DummyLayer.contents scale:[[UIScreen mainScreen] scale] orientation:UIImageOrientationUp];

ここまででtouchesBeganまでの流れです。

描画の大まかな流れです。
  • touchesBeganでUIImageViewからCALayerを取得しそれを基にUIImageを作成します。その際は必ずscaleを設定しないとRetina仕様にはなりません。
  • touchesMovedで、
UIGraphicsBeginImageContextWithOptions(Canvas.frame.size, NO,[[UIScreen mainScreen] scale]);
[DummyImage drawAtPoint:CGPointZero];
[[UIColor blackColor]  setStroke];
[path stroke];
//描画した画像をCALayerにセットします。
DummyLayer.contents = (id)[UIGraphicsGetImageFromCurrentImageContext() CGImage];
//取得してきた階層のレイヤーを新しいレイヤーに入れ替えます。
[Canvas.layer replaceSublayer:[Canvas.layer.sublayers objectAtIndex:0] with:DummyLayer];
  • touchesEnded && touchesCancelled はtouchesMovedと大差ありません。
UIGraphicsBeginImageContextWithOptions(Canvas.frame.size, NO,[[UIScreen mainScreen] scale]);
[DummyImage drawAtPoint:CGPointZero];
//画像のscaleが変更できる仕様でかつ線情報を保存溶かしたい場合だとここで一工夫 今から描画される線はframe基準ですがlayerに格納される線はbounds基準なので元に戻す必要があります。
UIBezierPath *bez = [UIBezierPath bezierPath];
[bez appendPath:self.bezierPath];

CGAffineTransform trans = CGAffineTransformMakeScale(1 / zoom,1 / zoom);
//bezierPathをbounds基準の大きさに変更(元データとなります)
 [self.bezierPath applyTransform:trans];
[setColor setStroke];
//変更前の線はそのまま描画
 [bez stroke];
                    
DummyLayer.contents = (id)[UIGraphicsGetImageFromCurrentImageContext() CGImage];
 [Canvas.layer replaceSublayer:[canvas.layer.sublayers objectAtIndex:0] with:DummyLayer];

大体の補足説明は以上ですが、流石にこのままだとtransformで変更した時に可哀想なことになるのでもう一説明です。
描く描画領域はframeでCALyerはboundsで決まると説明しましたよね?
なのでscaleを変えるともちろんCALayerの内容が見当違いの所に表示されます…
なので元データと記述したパスデータを使って、再描画してあげる必要があります。
なので私はUIBezierPathとパスの色、太さ等の値を持ったNSDictonaryデータが入った配列データを作成し、
再描画の時に利用しています。

for (NSDictionary *path in data) {
    @autoreleasepool{
        UIGraphicsBeginImageContextWithOptions(Canvas.frame.size,NO,[[UIScreen mainScreen] scale]);
        //描画領域に、前回までに描画した画像を、描画します。
        UIImage *old = [[UIImage alloc]initWithCGImage:(CGImageRef)newLayer.contents];
        [oldImage drawAtPoint:CGPointZero];
        UIBezierPath *bez = [UIBezierPath bezierPath];
        [bez appendPath:path[@"line"]];
        //[path[@"color"] setStroke];
        //現在のキャンバスサイズに大きさを合わせてあげます。
        CGAffineTransform trans = CGAffineTransformMakeScale(zoom,zoom);
        [bez applyTransform:trans];
        [bez stroke];
        newLayer.contents = (id)[UIGraphicsGetImageFromCurrentImageContext() CGImage];
         // 描画を終了します。
         UIGraphicsEndImageContext();
    }
}
[Canvas.layer replaceSublayer:[Canvas.layer.sublayers objectAtIndex:0] with:newLayer];