Blog スタッフブログ

iOS システム開発

[iOS]UIBezierPathを利用した自由描画と拡大縮小

Swift

こんにちは、株式会社MIXシステム開発担当のBloomです。

早速本題のUIBezierPathを利用した自由描画と拡大縮小について、お仕事の中で得た知見を共有させていただきたいと思います。

UIBezierPathによる自由描画

では、早速自由描画を行うサンプルコードを掲載します。

import UIKit
class DrawView: UIView {
    var drawImageView: UIImageView!
    var lastPoint: CGPoint?
    var firstPoint: CGPoint?
    var drawGesture: UILongPressGestureRecognizer!
    var freeLineBezierPath: UIBezierPath?
    
    func setup() {
        drawImageView = UIImageView(frame: self.bounds)
        self.addSubview(drawImageView)
        
        drawGesture = UILongPressGestureRecognizer(target: self, action: #selector(drawGesture(_:)))
        drawGesture.minimumPressDuration = 0
        self.addGestureRecognizer(drawGesture)
    }
    @objc func drawGesture(_ sender: AnyObject) {
        guard let drawGesture = sender as? UILongPressGestureRecognizer else { return }
        
        //タッチ座標を取得
        let touchPoint = drawGesture.location(in: self.drawImageView)
        switch drawGesture.state {
        case .began:
            lastPoint = touchPoint
            freeLineBezierPath = UIBezierPath()
            freeLineBezierPath?.lineCapStyle = .round
            freeLineBezierPath?.lineWidth = 2.0
            freeLineBezierPath?.move(to: lastPoint!)
    
        case .changed:
            drawImageView.image = drawGestureAtChanged(lastPoint: lastPoint!, newPoint: touchPoint)
            lastPoint = touchPoint
        case .ended: fallthrough
        case .cancelled:
            drawFreeLineFinished(bezierPath: freeLineBezierPath!)
            lastPoint = nil
            firstPoint = nil
            break
        default:
            lastPoint = nil
            firstPoint = nil
        break
      }
    }

    func drawGestureAtChanged(lastPoint: CGPoint, newPoint: CGPoint) -> UIImage
    {
        let middlePoint = CGPoint(x: (newPoint.x + lastPoint.x) / 2.0, y: (newPoint.y + lastPoint.y) / 2.0)
      
        freeLineBezierPath?.addQuadCurve(to: middlePoint, controlPoint: lastPoint)
        UIGraphicsBeginImageContextWithOptions(drawImageView.frame.size, false, 0.0)
        let canvasRect = CGRect(x: 0, y: 0, width: self.frame.size.width, height: self.frame.size.height)
        UIColor.red.setStroke()
        
        freeLineBezierPath?.stroke()
        let imageAfterDraw = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return imageAfterDraw!
    }
    func drawFreeLineFinished(bezierPath: UIBezierPath)
    {}
}
class ViewController: UIViewController {
    @IBOutlet weak var draw: DrawView!
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        draw.setup()
        draw.layer.borderColor = UIColor.gray.cgColor
        draw.layer.borderWidth = 1.0
    }
}

実行結果

これでまず自由描画を行うことができました。このままこの描画結果を拡大・縮小してみましょう。UIImageへ変換してから拡大・縮小しても良いですが、そうすると線の太さまで変わってしまいます。今回はBezierPathのまま操作してみましょう。

import UIKit
class DrawResultView: UIView {
    var freeLineBezierPath: UIBezierPath?
    override func draw(_ rect: CGRect) {
        guard let freeLineBezierPath = freeLineBezierPath else { return }
        UIColor.red.setStroke()
        freeLineBezierPath.lineWidth = 2.0
        freeLineBezierPath.stroke()
    }
}
protocol DrawViewDelegate {
    func freeLineBezierPathCreated(path: UIBezierPath)
}
class DrawView: UIView {
    func drawFreeLineFinished(bezierPath: UIBezierPath)
    {
        delegate?.freeLineBezierPathCreated(path: bezierPath)
    }
}
import UIKit
class ViewController: UIViewController {
    @IBOutlet weak var result1: DrawResultView!
    @IBOutlet weak var result2: DrawResultView!

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        draw.setup()
        draw.delegate = self
        draw.layer.borderColor = UIColor.gray.cgColor
        draw.layer.borderWidth = 1.0
        result1.layer.borderColor = UIColor.gray.cgColor
        result1.layer.borderWidth = 1.0
        result2.layer.borderColor = UIColor.gray.cgColor
        result2.layer.borderWidth = 1.0
    }
}
extension ViewController: DrawViewDelegate {
    func freeLineBezierPathCreated(path: UIBezierPath) {
        // 左上詰め
        let clipRect = path.bounds
        let translation = CGAffineTransform(translationX: -clipRect.origin.x, y: -clipRect.origin.y)
        let translatedPath = path.copy() as! UIBezierPath
        translatedPath.apply(translation)
        
        let path1 = translatedPath.copy() as! UIBezierPath
        let scalehalf = CGAffineTransform(scaleX: 0.5, y: 0.5)
        path1.apply(scalehalf)
        result1.freeLineBezierPath = path1
        
        let path2 = translatedPath.copy() as! UIBezierPath
        let scale2x = CGAffineTransform(scaleX: 2.0, y: 2.0)
        path2.apply(scale2x)
        result2.freeLineBezierPath = path2
        
        result1.setNeedsDisplay()
        result2.setNeedsDisplay()
    }
}

実行結果

途中の処理でパスを左上詰めすることで画像として利用しやすくしています。あとはアフィン変換でパス位置を変換するだけで拡大・縮小を表現することができます。

これで線の幅を維持したまま描画結果の拡大縮小を行うことができました。良かったですね。