

初めて投稿いたします。  
当方autoLayoutがよく理解できておらず、皆様のお力をお借りしたいと思い投稿いたしました。  
現在テスト的に以下の動作をするアプリを作っています。  
＜テスト環境＞  
iOS12.1.4  
xcode 10.1  
swift 4.2  
＜動作内容＞  
１：２つのビューがある（添付コードでは緑、赤）  
２：タップすると画面一杯のサイズに拡大される  
３：閉じるを押すと、元のサイズに戻る  
４：ビュー拡大時にスワイプすると、ビューは横移動（この時もう一方を移動前に拡大）  
というサンプルなのですが  
４のもう一方のビューの閉じるを押しても、元のサイズに戻りません。

デバッグしていると４にて閉じるを押した時  
ViewController.viewへの制約に  
拡大中の制約が２つ重複している現象が発生していました。

なぜこのような現象が発生するのか、どのように修正すればいいのか分かっておりません。  
どなたかご教授して頂きたいと思います。

サンプルコードですが以下に全文を乗せさせて頂きます。  
何卒よろしくお願い致します。

[code]

    import UIKit
    
    class ViewController: UIViewController, CustomViewDelegate {
    
        // 赤ビュー
        lazy var red: CustomView = CustomView.init(frame: CGRect.zero)
    
        // 緑ビュー
        var green: CustomView = CustomView.init(frame: CGRect.zero)
    
        // 保持用
        var saveCon : [NSLayoutConstraint] = []
        var conGreen : [NSLayoutConstraint] = []    // 緑ビュー制約
        var conRed : [NSLayoutConstraint] = []      // 赤ビュー制約
    
        // 拡大
        func scaleUp(upView: CustomView) {        
            // 最前面に移動
            self.view.bringSubviewToFront(upView)
    
            // 赤ビューの制約無効
            if upView == red{
                NSLayoutConstraint.deactivate(conRed)
            }
            // 緑ビューの制約無効
            else{
                NSLayoutConstraint.deactivate(conGreen)
            }
    
            // 新規制約生成（画面いっぱいに広げる）、有効にする
            let constA = upView.topAnchor.constraint(equalTo: view.topAnchor)
            let constB = upView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            let constC = upView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
            let constD = upView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
    
            // 作成した制約保持（縮小時に使用するため）
            saveCon = [constA, constB, constC, constD]
    
            NSLayoutConstraint.activate(saveCon)
    
            UIView.animate(withDuration: 0.3, animations: {
                self.view.layoutIfNeeded()
            }, completion: { finished in
    
            })
        }
    
        // 縮小
        func scaleDown(downView: CustomView) {
            // 保持していた制約を使用して無効にする
            NSLayoutConstraint.deactivate(saveCon)
    
            // 赤ビューの元々の制約有効
            if downView == red{
                NSLayoutConstraint.activate(conRed)
            }
            // 緑ビューの元々の制約有効
            else{
                NSLayoutConstraint.activate(conGreen)
            }
            UIView.animate(withDuration: 0.3, animations: {
                self.view.layoutIfNeeded()
            }, completion: { finished in
    
            })
        }
    
        func swipeDirection(sender: UISwipeGestureRecognizer){
            let notSwipeView = sender.view != red ? red : green
    
            // スワイプされていないビュー:１つ後ろにinsertする
            //self.view.insertSubview(notSwipeView, at: 0)
    
            // スワイプされていないビュー:現在の制約無効
            if notSwipeView == red{
                NSLayoutConstraint.deactivate(conRed)
            }
            else{
                NSLayoutConstraint.deactivate(conGreen)
            }
    
            // スワイプされていないビュー:制約有効（拡大）
            let conNotSwipe1 = notSwipeView.topAnchor.constraint(equalTo: view.topAnchor)
            let conNotSwipe2 = notSwipeView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
            let conNotSwipe3 = notSwipeView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
            let conNotSwipe4 = notSwipeView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
            NSLayoutConstraint.activate([conNotSwipe1, conNotSwipe2, conNotSwipe3, conNotSwipe4])
    
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.01)
            {
                // スワイプビュー:制約無効（拡大）
                NSLayoutConstraint.deactivate(self.saveCon)
    
                // スワイプされていないビュー:制約保持（このビューを縮小するときに使用する）
                self.saveCon = [conNotSwipe1, conNotSwipe2, conNotSwipe3, conNotSwipe4]
    
                // 移動処理
                var currentConst : [NSLayoutConstraint]?
                // 左にスワイプ
                if sender.direction == UISwipeGestureRecognizer.Direction.left{
                    let conMove1 = sender.view!.topAnchor.constraint(equalTo: self.view.topAnchor)
                    let conMove2 = sender.view!.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
                    let conMove3 = sender.view!.widthAnchor.constraint(equalToConstant: 768)
                    let conMove4 = sender.view!.trailingAnchor.constraint(equalTo: self.view.leadingAnchor)
                    currentConst = [conMove1, conMove2, conMove3, conMove4]
                }
                // 右にスワイプ
                else if sender.direction == UISwipeGestureRecognizer.Direction.right{
                    let conMove1 = sender.view!.topAnchor.constraint(equalTo: self.view.topAnchor)
                    let conMove2 = sender.view!.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
                    let conMove3 = sender.view!.widthAnchor.constraint(equalToConstant: 768)
                    let conMove4 = sender.view!.leadingAnchor.constraint(equalTo: self.view.trailingAnchor)
                    currentConst = [conMove1, conMove2, conMove3, conMove4]
                }
                // スワイプビュー:制約有効(移動)
                NSLayoutConstraint.activate(currentConst!)
    
                UIView.animate(withDuration: 0.3, animations: {
                    self.view.layoutIfNeeded()
                }, completion: { finished in
                    // スワイプビュー:制約無効
                    NSLayoutConstraint.deactivate(currentConst!)
    
                    // スワイプビュー:元々の制約有効（縮小）
                    if sender.view! == self.red{
                        NSLayoutConstraint.activate(self.conRed)
                    }
                    else{
                        NSLayoutConstraint.activate(self.conGreen)
                    }
    
                    // スワイプビュー:後ろに移動する
                    self.view.insertSubview(sender.view!, at: 0)
                })
            }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 赤ビュー
            red.backgroundColor = UIColor.red
            red.myDelegate = self
            view .addSubview(red)
            red.translatesAutoresizingMaskIntoConstraints = false
            let constRedA = red.topAnchor.constraint(equalTo: view.topAnchor, constant: 200)
            let constRedB = red.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 100)
            let constRedC = red.widthAnchor.constraint(equalToConstant: 200)
            let constRedD = red.heightAnchor.constraint(equalToConstant: 200)
            conRed = [constRedA, constRedB, constRedC, constRedD]   // 赤ビューの初期制約セット
            NSLayoutConstraint.activate(conRed)
    
            // 緑ビュー
            green.backgroundColor = UIColor.green
            green.myDelegate = self
            view .addSubview(green)
            green.translatesAutoresizingMaskIntoConstraints = false
            let constGreenA = green.topAnchor.constraint(equalTo: view.topAnchor, constant: 200)
            let constGreenB = green.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -100)
            let constGreenC = green.widthAnchor.constraint(equalToConstant: 200)
            let constGreenD = green.heightAnchor.constraint(equalToConstant: 200)
            conGreen = [constGreenA, constGreenB, constGreenC, constGreenD] // 緑ビューの初期制約セット
            NSLayoutConstraint.activate(conGreen)
        }
    
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
        }
    }
    
    
    
    
    
    // ----------------------------------------------------
    // CustomViewDelegate
    // ----------------------------------------------------
    protocol CustomViewDelegate : class {
        func scaleUp(upView: CustomView)      // 拡大
        func scaleDown(downView: CustomView)    // 縮小
        func swipeDirection(sender: UISwipeGestureRecognizer) // スワイプ
    }
    
    extension CustomViewDelegate
    {
        func scaleUp(upView: CustomView){}
        func scaleDown(downView: CustomView){}
        func swipeDirection(sender: UISwipeGestureRecognizer){}
    }
    
    // ----------------------------------------------------
    // CustomViewクラス
    // ----------------------------------------------------
    class CustomView: UIView , UIGestureRecognizerDelegate{
        weak var myDelegate : CustomViewDelegate?
    
        // MARK: 拡大・縮小
        lazy var scaleUpGes : UITapGestureRecognizer = {
            let ges: UITapGestureRecognizer = UITapGestureRecognizer.init(target: self, action: #selector(scaleUp(_:)))
            ges.numberOfTapsRequired = 1
            ges.cancelsTouchesInView = false
            ges.delegate = self
            return ges
        }()
        @objc func scaleUp(_ sender: UITapGestureRecognizer){
            myDelegate?.scaleUp(upView: self)
            scaleUpGes.isEnabled = false
        }
    
        // 縮小（とりあえずボタン用意）
        lazy var scaleDownBtn : UIButton = {
            let btn : UIButton = UIButton(type: .custom)
            btn.setTitle("閉じる", for: .normal)
            btn.titleLabel?.font = UIFont.boldSystemFont(ofSize: 20.0)
            btn.setTitleColor(UIColor.black, for: .normal)
            btn.addTarget(self, action: #selector(scaleDown(_:)), for: .touchUpInside)
            btn.translatesAutoresizingMaskIntoConstraints = false
            return btn
        }()
        @objc func scaleDown(_ sender: UIButton){
            myDelegate?.scaleDown(downView: self)
            scaleUpGes.isEnabled = true
        }
    
    
        func setAutoLayout_scaleDownBtn(){
            scaleDownBtn.widthAnchor.constraint(equalToConstant: 120).isActive = true
            scaleDownBtn.heightAnchor.constraint(equalToConstant: 40).isActive = true
            scaleDownBtn.topAnchor.constraint(equalTo: topAnchor, constant: 10).isActive = true
            scaleDownBtn.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 10).isActive = true
        }
    
        // MARK: スワイプ
        lazy var swipe : [UISwipeGestureRecognizer] = {
            var ar : [UISwipeGestureRecognizer] = []
            let direction:[UISwipeGestureRecognizer.Direction] = [.left, .right]
            direction.forEach{ value in
                let sw = UISwipeGestureRecognizer.init(target: self, action: #selector(swipeMove(_:)))
                sw.numberOfTouchesRequired = 1
                sw.delegate = self
                sw.cancelsTouchesInView = false
                sw.direction = value
                ar.append(sw)
            }
            return ar
        }()
    
        @objc func swipeMove(_ sender: UISwipeGestureRecognizer){
            myDelegate?.swipeDirection(sender: sender)
        }
    
        required init?(coder aDecoder: NSCoder) {
            super.init(coder: aDecoder)
            //self.setUp()
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            self.setUp()
        }
    
        func setUp(){
            swipe.forEach{ value in             // スワイプジェスチャ
                addGestureRecognizer(value)
            }
            addGestureRecognizer(scaleUpGes)    // 拡大ジェスチャ
            addSubview(scaleDownBtn)            // 縮小ボタン
            setAutoLayout_scaleDownBtn()
        }
    }
    
[/code]


CC BY-SA 4.0
2019-02-25T02:14:27.393
53006
2019-02-26T19:09:34.367
2019-02-25T03:31:26.813
19110
32309
question
swift swift4
アニメーション実施後、autoLayoutの制約が重複する


現象を再現可能なコードを提示していただいているので試してみました。

直接の原因は「閉じる」を押した時に「タップすると画面一杯のサイズに拡大される」と「元のサイズに戻る」の両方の処理が走ってしまっていることです。

`print`文なんかを入れて動きをトレースするとすぐわかると思います。

[code]

    // 拡大
    func scaleUp(upView: CustomView) {
        print(#function)
        //...
    }
    
    // 縮小
    func scaleDown(downView: CustomView) {
        print(#function)
        //...
    }
    
[/code]

スワイプした後で「閉じる」を押すと

[code]

    scaleUp(upView:)
    scaleDown(downView:)
    
[/code]

のように表示されるはずです。

`CustomView`クラスの`scaleUp(_:)`の中には`scaleUpGes.isEnabled =
false`なんてコードが記載されていますが、スワイプで拡大されたviewではこのコードは実行されませんので、「タップで拡大」が有効になったままです。

例えば、`ViewController`クラスの`swipeDirection(sender:)`メソッド内のどこかにこんなコードを追加する必要があるでしょう。

[code]

    func swipeDirection(sender: UISwipeGestureRecognizer){
        let notSwipeView = sender.view != red ? red : green
        notSwipeView.scaleUpGes.isEnabled = false //<-
    
        //...
    }
    
[/code]

* * *

間接的には`saveCon`なんて「実行状態によって一体何が入っているのかよくわからなくなる変数」を導入してしまっていたり、全く同じ制約を操作を行う度に毎回作り直しているのも問題でしょう。

上記したように、ジェスチャーの有効・無効の制御さえ、一貫性を持たせるのは大変なのですから、「現在どのviewでどんな制約がかかっているか」なんてことを`saveCon`なんて変数一つで一貫性のあるように管理するのは大変です。ちょっとした想定外の操作をされただけで、無効化されるべき制約が有効のままで残ってしまい、ご経験されたような制約矛盾の状態になったりします。

あなたのコードの場合、せっかく`CustomView`を定義しているのですから、「現在有効な制約」をview自身に管理させるのが簡単で確実です。

`CustomView`にこんなプロパティを追加してやります。

[code]

    class CustomView: UIView, UIGestureRecognizerDelegate {
        weak var myDelegate : CustomViewDelegate?
    
        var activeConstraints: [NSLayoutConstraint] = [] {
            willSet {
                NSLayoutConstraint.deactivate(activeConstraints)
            }
            didSet {
                NSLayoutConstraint.activate(activeConstraints)
            }
        }
    
        //...
    }
    
[/code]

`CustomView`に制約を付ける場合には、このプロパティを操作するだけにして、直接`NSLayoutConstraint.deactivate(_:)`や`NSLayoutConstraint.activate(_:)`は呼ばないようにします。

ざっくりこんな感じ。

[code]

    // 拡大
    func scaleUp(upView: CustomView) {
        print(#function)
        // 最前面に移動
        self.view.bringSubviewToFront(upView)
    
        // 新規制約生成（画面いっぱいに広げる）、有効にする
        let constA = upView.topAnchor.constraint(equalTo: view.topAnchor)
        let constB = upView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        let constC = upView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
        let constD = upView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
    
        // 作成した制約保持
        upView.activeConstraints = [constA, constB, constC, constD]
    
        UIView.animate(withDuration: 0.3, animations: {
            self.view.layoutIfNeeded()
        }, completion: { finished in
    
        })
    }
    
    // 縮小
    func scaleDown(downView: CustomView) {
        print(#function)
        if downView == red {
            // 赤ビューの元々の制約有効
            red.activeConstraints = conRed
        } else{
            // 緑ビューの元々の制約有効
            green.activeConstraints = conGreen
        }
        UIView.animate(withDuration: 0.3, animations: {
            self.view.layoutIfNeeded()
        }, completion: { finished in
    
        })
    }
    
    func swipeDirection(sender: UISwipeGestureRecognizer){
        let notSwipeView = sender.view != red ? red : green
        notSwipeView.scaleUpGes.isEnabled = false
    
        // スワイプされていないビュー:制約有効（拡大）
        let conNotSwipe1 = notSwipeView.topAnchor.constraint(equalTo: view.topAnchor)
        let conNotSwipe2 = notSwipeView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        let conNotSwipe3 = notSwipeView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
        let conNotSwipe4 = notSwipeView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        notSwipeView.activeConstraints = [conNotSwipe1, conNotSwipe2, conNotSwipe3, conNotSwipe4]
    
        self.view.layoutIfNeeded()
        DispatchQueue.main.async {
            // 移動処理
            let currentConst : [NSLayoutConstraint]
            if sender.direction == .left {
                // 左にスワイプ
                let conMove1 = sender.view!.topAnchor.constraint(equalTo: self.view.topAnchor)
                let conMove2 = sender.view!.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
                let conMove3 = sender.view!.widthAnchor.constraint(equalToConstant: 768)
                let conMove4 = sender.view!.trailingAnchor.constraint(equalTo: self.view.leadingAnchor)
                currentConst = [conMove1, conMove2, conMove3, conMove4]
            } else {
                // 右にスワイプ
                let conMove1 = sender.view!.topAnchor.constraint(equalTo: self.view.topAnchor)
                let conMove2 = sender.view!.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
                let conMove3 = sender.view!.widthAnchor.constraint(equalToConstant: 768)
                let conMove4 = sender.view!.leadingAnchor.constraint(equalTo: self.view.trailingAnchor)
                currentConst = [conMove1, conMove2, conMove3, conMove4]
            }
            // スワイプビュー:制約有効(移動)
            (sender.view as! CustomView).activeConstraints = currentConst
    
            UIView.animate(withDuration: 0.3, animations: {
                self.view.layoutIfNeeded()
            }, completion: { finished in
                // スワイプビュー:元々の制約有効（縮小）
                if sender.view! == self.red {
                    self.red.activeConstraints = self.conRed
                } else{
                    self.green.activeConstraints = self.conGreen
                }
    
                // スワイプビュー:後ろに移動する
                self.view.insertSubview(sender.view!, at: 0)
            })
        }
    }
    
[/code]

(`viewDidLoad()`内で最初の制約を与える部分も同様ですが、省略しています。)

このような修正は必須ではありませんが、こうしておくと、先の`notSwipeView.scaleUpGes.isEnabled =
false`なんてのを追加し忘れても破綻なく動作するはずです。よろしければお試しください。

制約ベースのアニメーションを行おうとすると、どうしても制約を動的に操作する必要が出てくるのですが、`CustomView`のようなクラスを使用しない場合でも、「現在有効な制約」はviewごとに確実に管理することをお勧めします。


CC BY-SA 4.0
2019-02-26T19:09:34.367
53059
2019-02-26T19:09:34.367
13972
53006
answer