70389


プログラミング初心者です。  
使用言語：Swift

こちらで決めた画像をズームしながら手書きで描けるようにしたいのですが、
画像に描くことができない状況にいます。何のコードが足りないかご教示いただきたく質問致します。

コメント欄にて指摘されたので補足すると、画像を拡大縮小しながら画像にお絵かきをするということです。

* * *

**描く画面のレイアウト:**

[![画像は描く画面のレイアウト](https://i.stack.imgur.com/TBXua.png)](https://i.stack.imgur.com/TBXua.png)

**現状のソースコード:**

[code]

    import UIKit
    import PencilKit
    
    class DrawTViewController: UIViewController, UIScrollViewDelegate, UIDocumentInteractionControllerDelegate, UIImagePickerControllerDelegate {
        
        @IBOutlet weak private var scrollView: UIScrollView!
               
        private let minZoomScale: CGFloat = 1.0
        private let maxZoomScale: CGFloat = 8.0
                
        var lastPoint: CGPoint?                    //直前のタッチ座標の保存用
        var lineWidth: CGFloat?                    //描画用の線の太さの保存用
    //  var bezierPath = UIBezierPath()            //お絵描きに使用
        var bezierPath: UIBezierPath?              //お絵描きに使用
        var drawColor = UIColor()                  //描画色の保存用
        var currentDrawNumber = 0                  //現在の表示しているは何回めのタッチか
        var saveImageArray = [UIImage]()           //Undo/Redo用にUIImageを保存
                   
        let defaultLineWidth: CGFloat = 4.0        //デフォルトの線の太さ
                
        var interactionController : UIDocumentInteractionController?
                   
        var imageView: UIImageView = {
            let image = UIImage(named: "picture")!
            let imageView = UIImageView(image: image)  // アスペクト比固定でフィットさせる
            imageView.contentMode = .scaleAspectFit
            return imageView
        }()
                
          override func viewDidLoad() {
            super.viewDidLoad()
                    
            setupScrollView()
            self.view.addSubview(scrollView)
           
           let panGesture = UIPanGestureRecognizer(target: self, action: #selector(drawGesture))
    panGesture.delegate = self      // need UIGestureRecognizerDelegate
    panGesture.minimumNumberOfTouches = 1
    panGesture.maximumNumberOfTouches = 1
    scrollView.addGestureRecognizer(panGesture)
    
            }
                
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
                    
            updateImageView()
            updateContentInset()        
        }
                
        private func setupScrollView() {
            scrollView.delegate = self
            scrollView.backgroundColor = .white
            scrollView.minimumZoomScale = minZoomScale
            scrollView.maximumZoomScale = maxZoomScale
            scrollView.showsVerticalScrollIndicator = false
            scrollView.showsHorizontalScrollIndicator = false
            scrollView.addSubview(imageView)
                    
            // ダブルタップのジェスチャを追加
            let gesture = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:)))
            gesture.numberOfTapsRequired = 2
            scrollView.addGestureRecognizer(gesture)
        }
                
        // 画像のアスペクト比を維持したまま ScrollView にぴったり収まるように ImageView のサイズを調整する
        private func updateImageView() {
            guard let size = imageView.image?.size else { return }
            let wRate = scrollView.bounds.width / size.width
            let hRate = scrollView.bounds.height / size.height
            let rate = min(wRate, hRate, 1)
            imageView.frame.size = CGSize(width: size.width * rate, height: size.height * rate)
            // contentSize を画像サイズと同じにする
            scrollView.contentSize = imageView.frame.size
        }
    
        // ImageView が常に画面中央に配置されるように contentInset を設定する
        private func updateContentInset() {
            scrollView.contentInset = UIEdgeInsets(
                top: max((scrollView.frame.height - imageView.frame.height) / 2, 0),
                left: max((scrollView.frame.width - imageView.frame.width) / 2, 0),
                bottom: 0,
                right: 0
            )
        }
    
        // ズーム対象のビューを返す
        func viewForZooming(in scrollView: UIScrollView) -> UIView? {
            return imageView
        }
    
        // ズームのタイミングで contentInset を更新する
        func scrollViewDidZoom(_ scrollView: UIScrollView) {
            updateContentInset()
        }
                
        // ScrollView がダブルタップされた時
        @objc private func scrollViewDoubleTapped(_ gesture: UITapGestureRecognizer) {
            guard let scrollView = gesture.view as? UIScrollView else { return }
            if scrollView.zoomScale == minZoomScale {
                // タップされた場所を中心に最大に拡大する
                let location = gesture.location(in: scrollView)
                let rect = zoomRect(for: scrollView, scale: maxZoomScale, center: location)
                scrollView.zoom(to: rect, animated: true)
            } else {
                // 最小に戻す
                scrollView.setZoomScale(minZoomScale, animated: true)
            }
        }
                
        private func zoomRect(for scrollView: UIScrollView, scale: CGFloat, center: CGPoint) -> CGRect {
            let size = CGSize(
                width: scrollView.frame.width / scale,
                height: scrollView.frame.height / scale
            )
            let rect = CGRect(
                origin: CGPoint(
                    x: center.x - size.width / 2.0,
                    y: center.y - size.height / 2.0
                ),
                size: size
            )
            return rect
        }
    
        private func prepareDrawing() {
            //実際のお絵描きで言う描く手段(色えんぴつ？クレヨン？絵の具？など)の準備
            let myDraw = UIPanGestureRecognizer(target: self, action: #selector(DrawTViewController.drawGesture))
            myDraw.maximumNumberOfTouches = 1
            self.scrollView.addGestureRecognizer(myDraw)
                       
            drawColor = UIColor.black                    //draw色を黒色に決定する
            lineWidth = CGFloat(3)                       //線の太さを決定する
                       
            //実際のお絵描きで言うキャンバスの準備 (=何も描かれていないUIImageの作成)
            prepareCanvas()
                       
            saveImageArray.append(self.imageView.image!) //配列にcanvasView.imageを保存
        }
    
    /**
        キャンバスの準備 (何も描かれていないUIImageの作成)
    */
        func prepareCanvas() {
            let canvasSize = CGSize(width: view.frame.width * 1, height: view.frame.width * 1)       //キャンバスのサイズの決定
            let canvasRect = CGRect(x: 0, y: 0, width: canvasSize.width, height: canvasSize.height)  //キャンバスのRectの決定
            UIGraphicsBeginImageContextWithOptions(canvasSize, false, 0.0)                           //コンテキスト作成(キャンバスのUIImageを作成する為)
            let firstCanvasImage = UIImage(contentsOfFile: "Blackboard 英語")                         //キャンバス用UIImage(まだ空っぽ)
                   
            firstCanvasImage?.draw(in: canvasRect)                                                   //firstCanvasImageの内容を描く(真っ白)
            //何も描かれてないUIImageを取得
            imageView.contentMode = .scaleAspectFit                                                  //contentModeの設定
            imageView.image = firstCanvasImage                                                       //画面の表示を更新
            UIGraphicsEndImageContext()                                                              //コンテキストを閉じる
        }
                  
    /**
        draw動作
    */
        @objc func drawGesture(sender: AnyObject) {
            guard let drawGesture = sender as? UIPanGestureRecognizer else {
                print("drawGesture Error happened.")
                return
            }
                       
            guard let canvas = self.imageView.image else {
                fatalError("self.pictureView.image not found")
            }
    
    //      lineWidth = defaultLineWidth                            //描画用の線の太さを決定する
    //      drawColor = UIColor.blackColor()                        //draw色を決定する
            let touchPoint = drawGesture.location(in: imageView)    //タッチ座標を取得
                       
            switch drawGesture.state {
                case .began:
                    lastPoint = touchPoint                          //タッチ座標をlastTouchPointとして保存する
                    //touchPointの座標はscrollView基準なのでキャンバスの大きさに合わせた座標に変換しなければいけない
                    //LastPointをキャンバスサイズ基準にConvert
                    let lastPointForCanvasSize = convertPointForCanvasSize(originalPoint: lastPoint!, canvasSize: canvas.size)
                    bezierPath = UIBezierPath()
                    guard let bzrPth = bezierPath else {
                        fatalError("bezierPath Error")
                    }
                    bzrPth.lineCapStyle = .round                    //描画線の設定 端を丸くする
    //              bzrPth.lineWidth = defaultLineWidth             //描画線の太さ  ここに問題あり
                    bzrPth.lineWidth = lineWidth!                   //描画線の太さ
                    bzrPth.move(to: lastPointForCanvasSize)
                           
                case .changed:
                    let newPoint = touchPoint                       //タッチポイントを最新として保存
                    guard let bzrPth = bezierPath else {
                        fatalError("bezierPath Error")
                    }
                    //Draw実行しDraw後のimage取得
                    let imageAfterDraw = drawGestureAtChanged(canvas: canvas, lastPoint: lastPoint!, newPoint: newPoint, bezierPath: bzrPth)
                    self.imageView.image = imageAfterDraw           //Draw画像をCanvasに上書き
                    lastPoint = newPoint                            //Point保存
                           
                case .ended:
                    //currentDrawNumberとsaveImageArray配列数が矛盾無きまでremoveLastする
                    while currentDrawNumber != saveImageArray.count - 1 {
                        saveImageArray.removeLast()
                    }
                    currentDrawNumber += 1
                    saveImageArray.append(self.imageView.image!)    //配列にcanvasView.imageを保存
                    if currentDrawNumber != saveImageArray.count - 1 {
                        fatalError("index Error")
                    }
                    print("Finish dragging")
                           
                default:
                    ()
            }     
        }
                   
    /**
        UIGestureRecognizerのStatusが.Changedの時に実行するDraw動作
                    
        - parameter canvas : キャンバス
        - parameter lastPoint : 最新のタッチから直前に保存した座標
        - parameter newPoint : 最新のタッチの座標座標
        - parameter bezierPath : 線の設定などが保管されたインスタンス
        - returns : 描画後の画像
    */
        func drawGestureAtChanged(canvas: UIImage, lastPoint: CGPoint, newPoint: CGPoint, bezierPath: UIBezierPath) -> UIImage {
            //最新のtouchPointとlastPointからmiddlePointを算出
            let middlePoint = CGPoint(x: (lastPoint.x + newPoint.x) / 2, y: (lastPoint.y + newPoint.y) / 2)
            //各ポイントの座標はscrollView基準なのでキャンバスの大きさに合わせた座標に変換しなければいけない
            //各ポイントをキャンバスサイズ基準にConvert
            let middlePointForCanvas =  convertPointForCanvasSize(originalPoint: middlePoint, canvasSize: canvas.size)
            let lastPointForCanvas = convertPointForCanvasSize(originalPoint: lastPoint, canvasSize: canvas.size)
            bezierPath.addQuadCurve(to: middlePointForCanvas, controlPoint: lastPointForCanvas)        //曲線を描く
            UIGraphicsBeginImageContextWithOptions(canvas.size, false, 0.0)                            //コンテキストを作成
            let canvasRect = CGRect(x: 0, y: 0, width: canvas.size.width, height: canvas.size.height)  //コンテキストのRect
            self.imageView.image?.draw(in: canvasRect)                                                 //既存のCanvasを準備
            drawColor.setStroke()                                                                      //drawをセット
            bezierPath.stroke()                                                                        //draw実行
            let imageAfterDraw = UIGraphicsGetImageFromCurrentImageContext()                           //Draw後の画像
            UIGraphicsEndImageContext()                                                               //コンテキストを閉じる
                       
            return imageAfterDraw!
        }
    
    /**
        (おまじない)座標をキャンバスのサイズに準じたものに変換する
                    
        - parameter originalPoint : 座標
        - parameter canvasSize : キャンバスのサイズ
        - returns : キャンバス基準に変換した座標
    */
        func convertPointForCanvasSize(originalPoint: CGPoint, canvasSize: CGSize) -> CGPoint {
            let viewSize = scrollView.frame.size
            var ajustContextSize = canvasSize
            var diffSize: CGSize = CGSize(width: 0, height: 0)
            let viewRatio = viewSize.width / viewSize.height
            let contextRatio = canvasSize.width / canvasSize.height
            let isWidthLong = viewRatio < contextRatio ? true : false
                       
            if isWidthLong {
                ajustContextSize.height = ajustContextSize.width * viewSize.height / viewSize.width
                diffSize.height = (ajustContextSize.height - canvasSize.height) / 2
            } else {   
                ajustContextSize.width = ajustContextSize.height * viewSize.width / viewSize.height
                diffSize.width = (ajustContextSize.width - canvasSize.width) / 2         
            }
                       
            let convertPoint = CGPoint(x: originalPoint.x * ajustContextSize.width / viewSize.width - diffSize.width,
                y: originalPoint.y * ajustContextSize.height / viewSize.height - diffSize.height)
                       
            return convertPoint
        }
    
        @IBAction func Black(_ sender: Any) {
            drawColor = UIColor.black    //黒色に変更する
        }
                
        @IBAction func Red(_ sender: Any) {
            drawColor = UIColor.red      //赤色に変更する
        }
                    
        @IBAction func Blue(_ sender: Any) {
            drawColor = UIColor.blue     //青色に変更する
        }
    }
    
[/code]


CC BY-SA 4.0
2020-08-15T02:05:10.497
69565
2020-09-20T16:02:28.257
2020-09-20T13:55:44.107
41502
41502
question
swift uikit
拡大縮小しながら画像をお絵描きする方法


> 何のコードが足りないかご教示いただきたく質問致します。

9月6日時点で提示されているコードで不足している内容は以下の２点と思います。  
drawGesture() メソッドがジェスチャーの action に指定されていないため機能することがない  
scrollView が DrawTViewController の View に張り付けられていない  
9月6日時点のコードの viewDidLoad() 内に以下を追加してみてください。

[code]

    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(drawGesture))
    panGesture.delegate = self      // need UIGestureRecognizerDelegate
    panGesture.minimumNumberOfTouches = 1
    panGesture.maximumNumberOfTouches = 1
    scrollView.addGestureRecognizer(panGesture)
    self.view.addSubview(scrollView) 
    
[/code]

これで指による手書きとズームは機能するのではないかと思います。  
ApplePencil で機能しない場合は PKCanvasView を使用する必要があるかもしれません。

## 9月21日追記

> 最初はPencilKitを使って描けるようにしていきたいと考えております。

PencilKit を使用した場合のサンプルコードです。  
ApplePencil は持っていないため指による手書きで確認しています。  
表示する画像名は foo.jpg にしています。  
新しいプロジェクトを作成し ViewController.swift を置き換えれば確認できると思います。  
簡単なコードの説明です。  
PKCanvasView の場合は UIScrollView と違って viewForZooming() が呼び出されません。  
（Apple のクラスライブラリは、リスコフの置換原則に従わない継承が多い）  
scrollViewDidZoom() は呼び出されるためここで imageView の拡大縮小を行うようにしています。  
canvasView.addSubview(imageView) では imageView が前面に来て手書きの線が表示できないため  
canvasView.sendSubviewToBack(imageView) で画像を背景に表示し  
canvasView.isOpaque = false  
canvasView.backgroundColor = .clear  
で canvasView を透明にして背景画像を表示するようにしています。

[code]

    import UIKit
    import PencilKit
    
    class ViewController: UIViewController, PKCanvasViewDelegate {
        private lazy var canvasView: PKCanvasView = setupCanvasView()
        private lazy var imageView: UIImageView = setupImageView()
        
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view.
            self.canvasView.addSubview(self.imageView)
            self.canvasView.sendSubviewToBack(self.imageView)
            self.view.addSubview(self.canvasView)
        }
        func scrollViewDidZoom(_ scrollView: UIScrollView) {
            if scrollView == self.canvasView {
                self.imageView.frame.size = CGSize(width: self.view.frame.width * scrollView.zoomScale, height: self.view.frame.height * scrollView.zoomScale)
                self.imageView.center = CGPoint(x: scrollView.contentSize.width / 2, y: scrollView.contentSize.height / 2)
            }
        }
        private func setupCanvasView() -> PKCanvasView {
            let canvasView: PKCanvasView = PKCanvasView(frame: self.view.frame)
            canvasView.delegate = self
            canvasView.maximumZoomScale = 3.0
            canvasView.isOpaque = false
            canvasView.backgroundColor = .clear
            canvasView.contentOffset = CGPoint.zero
            canvasView.contentSize = self.view.frame.size
            canvasView.tool = PKInkingTool(.pen, color: .black, width: 30)
            // PKToolPicker: ドラッグして移動できるツールパレット (ペンや色などを選択できるツール)
            if let window = UIApplication.shared.windows.first {
                if let toolPicker = PKToolPicker.shared(for: window) {
                    toolPicker.addObserver(canvasView)
                    toolPicker.setVisible(true, forFirstResponder: canvasView)
                    canvasView.becomeFirstResponder()
                }
            }
            return canvasView
        }
        private func setupImageView() -> UIImageView {
            let imgView = UIImageView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
            imgView.image = UIImage(named: "foo.jpg")
            imgView.contentMode = UIView.ContentMode.scaleAspectFit
            return imgView
        }
    }
    
[/code]


CC BY-SA 4.0
2020-09-14T15:19:39.057
70389
2020-09-20T16:02:28.257
2020-09-20T16:02:28.257
41756
41756
69565
answer