質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

87.92%

swift:CALayerとCATextLayerの重なる順番

解決済

回答 1

投稿

  • 評価
  • クリップ 0
  • VIEW 602

score 57

CALayerで丸、CATextLayerで番号を描き、タッチで選択して2つを同時に動かしたい。
しかし、なぜか先にCATextLayerの番号が選択されて、番号だけが動きます。
CALayerの丸を選択して動かすと番号の方も動きます。

重なっているレイヤの内、CALayerの丸の方を先に選択できれば解決すると思いますが、方法がわかりません。
CALayerを優先して選択する方法を教えて下さい。

self.addSublayer(textLayer)とself.addSublayer(ovalShapeLayer)の順番を変えても結果は同じでした。
どなたかご教授ください。
イメージ説明

import UIKit

class ViewController: UIViewController,UIGestureRecognizerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        effectiveScale = 1.0 //現在のスケールを初期化
        //MARK:- ボタンの設置
        let width = self.view.bounds.width
        let height = self.view.bounds.height
        //丸ボタンを生成
        let ovalBtn  = UIButton()
        ovalBtn.frame = CGRect(x: 0, y: 0, width: 100, height: 50)
        ovalBtn.center = CGPoint(x: width/3, y: height-30)
        ovalBtn.addTarget(self, action: #selector(self.ovalBtnTapped(sender:)), for: .touchUpInside)
        ovalBtn.setTitle("丸",for:.normal)
        ovalBtn.backgroundColor = UIColor.green
        self.view.addSubview(ovalBtn)
        //四角を生成するボタン
        let rectBtn = UIButton()
        rectBtn.frame = CGRect(x:0,y:0,width:100,height:50)
        rectBtn.center = CGPoint(x:width * 2 / 3,y:height - 30)
        rectBtn.addTarget(self, action: #selector(ViewController.rectBtnTapped(sender:)), for: .touchUpInside)
        rectBtn.setTitle("四角",for:.normal)
        rectBtn.backgroundColor = UIColor.red
        self.view.addSubview(rectBtn)
        //ピンチ時の処理
        let pinch = UIPinchGestureRecognizer()
        pinch.addTarget(self,action:#selector(ViewController.pinchGesture(sender:)))
        pinch.delegate = self
        self.view.addGestureRecognizer(pinch)
    }

    @objc func ovalBtnTapped(sender:UIButton){
        //丸を描く @objcを付けられたメソッド、プロパティにはSwiftの実装に加え、Objective-Cで用いられるふたつの隠し引数を持った関数を同時に作成するようになります。このことで@objcを付けたメソッド、プロパティはObective-Cからも正しく利用することができるようになります。
        let oval = MyShapeLayer()
        oval.frame = CGRect(x:100,y:100,width:30,height:30)
        oval.drawOval(lineWidth:1)
        self.view.layer.addSublayer(oval)
    }
    @objc func rectBtnTapped(sender:UIButton){
        //四角を描く
        let rect = MyShapeLayer()
        rect.frame = CGRect(x:40,y:40,width:50,height:50)
        rect.drawRect(lineWidth:1)
        self.view.layer.addSublayer(rect)
    }

    //MARK:- タッチした時
    //選択したレイヤーをいれておく
    private var selectLayer:CALayer!
    //最後にタッチされた座標をいれておく
    private var touchLastPoint:CGPoint!

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        selectLayer = nil //すでに選択されたあレイヤがあるかもしれないのでnilとしておく
        let touch : UITouch = touches.first! //touches.first! はタッチをしたことを取得
        let layer:CALayer = hitLayer(touch: touch)//後で作る関数 タッチされた場所にあるレイヤを取得
        let touchPoint:CGPoint = touch.location(in: self.view) //タッチされた座標を取得
        touchLastPoint=touchPoint //最後にタッチされた場所の座標を入れておく
        self.selectLayerFunc(layer:layer) //選択されたレイヤをselectLayerに入れる
    }

    //MARK:- タッチをした場所にあるレイヤーを取得するhitLayer(touch:)を作成
    //座標を変換してレイヤーを返却しています。
    //returnのhitTest部分で座標上にあるレイヤーを取得しています。
    func hitLayer(touch:UITouch) -> CALayer{
        print("hitLayer")
        var touchPoint:CGPoint = touch.location(in:self.view)
        touchPoint = self.view.layer.convert(touchPoint, to: self.view.layer.superlayer)
        return self.view.layer.hitTest(touchPoint)!
    }

    //MARK:- 選択されたレイヤーをselectLayerに入れている部分
    //タッチした座標に、view上でのせているレイヤーがない場合はselectLayerにnilをいれ、CALayerがあった場合にはそのレイヤーをそのレイヤーを格納しています。
    func selectLayerFunc(layer:CALayer?) {
        if((layer == self.view.layer) || (layer == nil)){
            selectLayer = nil
            print("空振り、なにもないところをタッチした")
            let oval = MyShapeLayer()
            oval.frame = CGRect(x:touchLastPoint.x-15,y:touchLastPoint.y-15,width:30,height:30)
            oval.drawOval(lineWidth:1)
            self.view.layer.addSublayer(oval)
//            selectLayer = // 怪しい
            return
        }
        selectLayer = layer
        print("レイヤをタッチした")
    }

    //MARK:- タッチが動いた時
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch:UITouch = touches.first!
        let touchPoint:CGPoint = touch.location(in: self.view)
        //直前の座標との差を取得
        let touchOffsetPoint:CGPoint = CGPoint(x:touchPoint.x - touchLastPoint.x,
                                               y:touchPoint.y - touchLastPoint.y)
        touchLastPoint = touchPoint

        if (selectLayer != nil){
            //hitしたレイヤーがあった場合
            let px:CGFloat = selectLayer.position.x
            let py:CGFloat = selectLayer.position.y
            //レイヤーを移動させる
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            selectLayer.position = CGPoint(x:px + touchOffsetPoint.x,y:py + touchOffsetPoint.y)
            selectLayer.borderWidth = 3.0
            selectLayer.borderColor = UIColor.green.cgColor
            CATransaction.commit()

        }
    }
    //タッチを終えた時
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if(selectLayer != nil){
            selectLayer.borderWidth = 0
        }
    }
    //タッチがキャンセルされた時
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        if(selectLayer != nil){
            selectLayer.borderWidth = 0        }
    }

    private var beginGestureScale:CGFloat! //ピンチ後のスケール
    private var effectiveScale:CGFloat! //現在のスケール

    //ピンチインアウトをした時によばれる。アクションの中にレイヤーを変形するコードをかきます。
    @objc func pinchGesture(sender:UIPinchGestureRecognizer){
           effectiveScale = beginGestureScale * sender.scale
           //選択されてるやつだけ
           if (selectLayer != nil){
               selectLayer.setAffineTransform(CGAffineTransform(scaleX: effectiveScale,y:effectiveScale))
           }
    }
    //ピンチが始まった時に、現在のスケールをbeginGestureScaleにいれておきます
    func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        if(gestureRecognizer.isKind(of:UIPinchGestureRecognizer.self)){
            beginGestureScale = effectiveScale
        }
        return true
    }
}
import UIKit

class MyShapeLayer: CALayer {
    //四角を描く関数 - CALayerの上に、そのCALayerのサイズと同じサイズのCAShapeLayerをのせています。
        func drawRect(lineWidth:CGFloat){
        let rect = CAShapeLayer()
        rect.strokeColor = UIColor.black.cgColor
        rect.fillColor = UIColor.clear.cgColor
        rect.lineWidth = lineWidth
        rect.path = UIBezierPath(rect:CGRect(x:0,y:0,width:self.frame.width,height:self.frame.height)).cgPath
        self.addSublayer(rect)
    }


    //丸を描く関数 - CALayerの上に、そのCALayerのサイズと同じサイズのCAShapeLayerをのせています。
    func drawOval(lineWidth:CGFloat){
        let ovalShapeLayer = CAShapeLayer()
        ovalShapeLayer.strokeColor = UIColor.blue.cgColor
        ovalShapeLayer.fillColor = UIColor.clear.cgColor
        ovalShapeLayer.lineWidth = lineWidth
        ovalShapeLayer.path = UIBezierPath(ovalIn: CGRect(x:0, y:0, width:self.frame.width, height: self.frame.height)).cgPath
    //番号を表示
        let textLayer = CATextLayer()
        textLayer.string = "999"
        textLayer.foregroundColor = UIColor.green.cgColor
        textLayer.fontSize = 10
        textLayer.frame = CGRect(x:1,y:1,width:self.frame.width, height: self.frame.height)

        self.addSublayer(textLayer)
        self.addSublayer(ovalShapeLayer)

    }

}
  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

0

CALayerで丸、CATextLayerで番号を描き、タッチで選択して2つを同時に動かしたい。
しかし、なぜか先にCATextLayerの番号が選択されて、番号だけが動きます。

コードを見た感じでは
MyShapeLayer(CALayer)に
・丸(CAShapeLayer)
・テキスト(CATextLayer)

の2つが乗っているように見えます。

CALayer.hitTest関数はそのレイヤそのものや子・孫といった階層まですべてヒットテストをしますので、
https://developer.apple.com/documentation/quartzcore/calayer/1410972-hittest
丸やテキストだけがヒットしてしまう事もあるかもしれません(CAShapeLayerは話を聞いている感じだとヒットしないのかもしれませんが)

なので、

func hitLayer(touch:UITouch) -> CALayer

の関数の実装を見直し、
view.layer.sublayersの中からMyShapeLayer型のものだけを抽出し(インスタンス①とします)、
その①のインスタンスに対してhitTest関数を呼び出し、ヒットしていることが判れば、①のインスタンスを選択レイヤーとして保持すれば期待通りの結果になるのではないかと思います。

以下疑似コードです。コンパイル通るかためしていませんが雰囲気だけでも伝われば。

for layer in view.layer.sublayers {
    if let myLayer = layer as? MyShapeLayer {
        if myLayer.hiiTest(...) != nil {
            return myLayer
        }
    }
}

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/01/24 22:13 編集

    takabosoftさん
    返信いただきありあがとうございます。
    なんとか回答いただいた内容を反映して思っていたとおりに動きました。
    スッキリしました。ありがとうございます。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 87.92%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る