web-dev-qa-db-ja.com

制約の変更やアニメーションにsetNeedsUpdateConstraintsを呼び出す必要がないのはなぜですか?

読み:

これから answer

これは、ビューの変更をアニメーション化するために受け入れられた回答が示唆するものです。

_addBannerDistanceFromBottomConstraint.constant = 0

UIView.animate(withDuration: 5) {
    self.view.layoutIfNeeded()
}

フレームを変更していないのときにlayoutIfNeededを呼び出すのはなぜですか。制約を変更しているので、(これに従って 他の答え )代わりにsetNeedsUpdateConstraintsを呼び出す必要はありませんか?

同様に、この高い評価 answer は言う:

後で何かが変更されて制約の1つが無効になった場合は、制約をすぐに削除し、setNeedsUpdateConstraintsを呼び出す必要があります。

観察:

実際に両方を使ってみました。 setNeedsLayout my viewを使用すると、左側に正しくアニメーション化されます

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func animate(_ sender: UIButton) {

        UIView.animate(withDuration: 1.8, animations: {
            self.centerXConstraint.isActive = !self.centerXConstraint.isActive
            self.view.setNeedsLayout()
            self.view.layoutIfNeeded()
        })
    }

    @IBOutlet weak var centerYConstraint: NSLayoutConstraint!
    @IBOutlet var centerXConstraint: NSLayoutConstraint!
}

ただし、setNeedsUpdateConstraintsを使用してもアニメーション化されませんアニメーションは、ビューを左にすばやく移動します

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func animate(_ sender: UIButton) {

        UIView.animate(withDuration: 1.8, animations: {
        self.centerXConstraint.isActive = !self.centerXConstraint.isActive
            self.view.setNeedsUpdateConstraints()
            self.view.updateConstraintsIfNeeded()    
        })
    }        

    @IBOutlet weak var centerYConstraint: NSLayoutConstraint!
    @IBOutlet var centerXConstraint: NSLayoutConstraint!
}

アニメーションが必要ない場合は、view.setNeedsLayoutまたはview.setNeedsUpdateConstraintsのいずれかを使用して、左に移動します。しかしながら:

  • view.setNeedsLayoutを使用すると、ボタンがタップされた後、viewDidLayoutSubviewsブレークポイントに到達します。しかし、updateViewConstraintsブレークポイントに到達することはありません。これにより、制約がどのように更新されるかについて困惑します...
  • view.setNeedsUpdateConstraintsを使用すると、ボタンがタップされた後、updateViewConstraintsブレークポイントに達し、、次にviewDidLayoutSubviewsブレークポイントに達します。これは理にかなっており、制約が更新されてから、layoutSubviewsが呼び出されます。

質問:

私の見解に基づく:制約を変更した場合、それが有効になるためにはsetNeedsUpdateConstraintsを呼び出さなければなりませんが、私の見解では間違っています。アニメーション化するには、次のコードで十分です。

self.view.setNeedsLayout()
self.view.layoutIfNeeded()

どうして?

それから、私はおそらく、内部で他の方法で制約を更新しているのではないかと考えました。そのため、override func updateViewConstraintsoverride func viewDidLayoutSubviewsにブレークポイントを設定しましたが、viewDidLayoutSubviewsのみがブレークポイントに到達しました。

では、自動レイアウトエンジンはこれをどのように管理していますか?

14
Honey

これは、iOS開発者の間でよくある誤解です。

これが自動レイアウトの「ゴールデンルール」の1つです。

"更新制約"について気にしないでください。

neverこれらのメソッドのいずれかを呼び出す必要があります:

  • setNeedsUpdateConstraints()
  • updateConstraintsIfNeeded()
  • updateConstraints()
  • updateViewConstraints()

except非常にまれなケースであり、非常に複雑なレイアウトでアプリの速度が低下する(または、意図的にレイアウトの変更を非典型的な方法で実装することを選択した)。

レイアウトを変更する好ましい方法

通常、レイアウトを変更する場合は、ボタンをタップした直後、または変更をトリガーしたイベントが発生した直後に、レイアウトの制約をアクティブ化/非アクティブ化または変更します。ボタンのアクションメソッド内:

_@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
    toggleLayout()
}

func toggleLayout() {
    isCenteredLayout = !isCenteredLayout

    if isCenteredLayout {
        centerXConstraint.isActive = true 
    } else {
        centerXConstraint.isActive = false
    }
}
_

As Apple puts it in their Auto Layout Guide

ほとんどの場合、影響のある変更が発生した直後に制約を更新する方がよりクリーンで簡単です。これらの変更を後のメソッドに延期すると、コードがより複雑になり、理解が難しくなります。

もちろん、この制約の変更をアニメーションでラップすることもできます。まず、制約の変更を実行してから、アニメーションクロージャーでlayoutIfNeeded()を呼び出して、変更をアニメーション化します。

_@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
    // 1. Perform constraint changes:
    toggleLayout()
    // 2. Animate the changes:
    UIView.animate(withDuration: 1.8, animations: {
        view.layoutIfNeeded()
    }
}
_

制約を変更すると、システム自動は遅延レイアウトパスをスケジュールします。つまり、システムは近い将来にレイアウトを再計算します。 did自分で制約を更新(変更)するだけなので、setNeedsUpdateConstraints()を呼び出す必要はありません。更新する必要があるのはレイアウト、つまりすべてのビューのフレームですnotその他の制約。

無効化の原則

前述のように、iOSレイアウトシステムは通常、制約の変更にすぐには反応せず、遅延レイアウトパスをスケジュールするだけです。これはパフォーマンス上の理由によるものです。次のように考えてください。

食料品を買い物に行くときは、カートに商品を入れますが、すぐには支払いません。代わりに、必要なものがすべて揃ったと感じるまで、他のアイテムをカートに入れます。その後、レジ係に進み、すべての食料品を一度に支払います。はるかに効率的です。

この遅延レイアウトパスのため、レイアウトの変更を処理するために必要な特別なメカニズムがあります。私はそれを無効化の原則と呼んでいます。これは2ステップのメカニズムです。

  1. 何かを無効としてマークします。
  2. 何かが無効な場合は、何らかのアクションを実行して、それを再度有効にします。

レイアウトエンジンに関しては、これは以下に対応します。

  1. setNeedsLayout()
  2. layoutIfNeeded()

そして

  1. setNeedsUpdateConstraints()
  2. updateConstraintsIfNeeded()

メソッドの最初のペアimmediate(遅延されない)レイアウトパスになります:最初にレイアウトを無効にし、次に無効な場合はレイアウトをすぐに再計算します(もちろん、そうです)。

通常、レイアウトパスがすぐに発生するか、数ミリ秒後に発生するかを気にしないため、通常はsetNeedsLayout()を呼び出してレイアウトを無効にし、遅延レイアウトパスを待機します。これにより、制約に他の変更を加え、レイアウトを少し後で更新できますが、一度にすべてを更新できます(→ショッピングカート)。

レイアウトを再計算する必要がある場合のみ、layoutIfNeeded()を呼び出す必要があります今すぐ。これは、新しいレイアウトの結果のフレームに基づいて他の計算を実行する必要がある場合です。

メソッドの2番目のペアは、updateConstraints()(ビューまたはupdateViewConstraints() onビューコントローラ)。しかし、それは通常あなたがしてはいけないことです。

レイアウトを一括で変更する

レイアウトがreally遅く、レイアウトの変更によりUIが遅く感じられる場合のみ、上記の方法とは異なる方法を選択できます。ボタンをタップするだけで直接制約を更新するのではなく、変更したいものの「メモ」と、制約を更新する必要がある別の「メモ」を作成します。

_@IBAction func toggleLayoutButtonTapped(_ button: UIButton) {
    // 1. Make a note how you want your layout to change:
    isCenteredLayout = !isCenteredLayout
    // 2. Make a note that your constraints need to be updated (invalidate constraints):
    setNeedsUpdateConstraints()
}
_

これにより、遅延レイアウトパスがスケジュールされ、updateConstraints()/updateViewConstraints()がレイアウトパス中に呼び出されることが保証されます。したがって、他の変更を行ってsetNeedsUpdateConstraints()を1,000回呼び出すこともできます。制約は引き続き更新されますonce次のレイアウトパス中にのみ。

ここで、updateConstraints()/updateViewConstraints()をオーバーライドし、現在のレイアウト状態に基づいて必要な制約の変更(つまり、上記の「1.」で「メモ」したもの)を実行します。

_override func updateConstraints() {
    if isCenteredLayout {
        centerXConstraint.isActive = true 
    } else {
        centerXConstraint.isActive = false
    }

    super.updateConstraints()
}
_

繰り返しになりますが、これはレイアウトが非常に遅く、数百または数千の制約を処理する場合の最後の手段です。私のプロジェクトでupdateConstraints()を使用するにはneverが必要です。

これにより、状況が少し明確になることを願っています。

追加のリソース:

42
Mischa

私はそれを簡単に説明しようとします:

最初に覚えておかなければならないのは、制約を更新してもビューのレイアウトがすぐに更新されないことです。すべてのレイアウトに時間がかかる可能性があるため、これはパフォーマンス上の理由によるものであり、実行する必要のある変更を「メモ」してから、単一のレイアウトパスを実行します。

さらに一歩進んで、制約に影響を与える何かが変更されたときに制約を更新することさえできず、制約を更新する必要があることを通知するだけです。 (ビューをレイアウトせずに)制約自体を更新する場合でも時間がかかり、同じ制約でも両方の方法(アクティブと非アクティブ)が変わる可能性があります。

SetNeedsUpdateConstraints()が行うことのすべてを考慮すると、ビューに関する制約は、次のレイアウトパスの前に再計算する必要があるというフラグが付けられます。 。次に、独自のバージョンのupdateConstraints()メソッドを実装して、現在のアプリの状態などに基づいて制約に必要な変更を実際に加える必要があります。

したがって、システムが次のレイアウトパスを実行する必要があると判断した場合、setNeedsUpdateConstraints()が呼び出された(またはシステムが更新の必要性を判断した)場合は、それらの変更を行うために呼び出されたupdateConstraints()の実装を取得します。これは、レイアウトが完了する前に自動的に行われます。

現在、setNeedsLayout()とlayoutIfNeeded()は似ていますが、実際のレイアウト処理自体を制御するためのものです。

ビューのレイアウトに影響を与える何かが変更されると、setNeedsLayout()を呼び出して、そのビューに「フラグ」を立て、次のレイアウトパス中にレイアウトを再計算させることができます。したがって、(おそらくsetNeedsUpdateConstraints()とupdateConstraints()を使用する代わりに)制約を直接変更する場合は、setNeedsLayout()を呼び出して、ビューのレイアウトが変更され、次のレイアウトパス中に再計算する必要があることを示すことができます。

LayoutIfNeeded()が行うことは、システムが次に実行する必要があるとシステムが判断するのを待つのではなく、レイアウトパスをその場で強制的に実行することです。すべての現在の状態に基づいて、ビューのレイアウトの再計算を強制します。また、このこぶしを行うと、setNeedsUpdateConstraints()でフラグが設定されているものは、最初にupdateConstraints()実装を呼び出します。

したがって、システムがレイア​​ウトパスを実行するか、アプリがlayoutIfNeeded()を呼び出すまで、レイアウトの変更は行われません。

実際には、何かが本当に複雑で、ビューの制約を直接更新し、setNeedsLayout()とlayoutIfNeeded()を使用して取得できる場合を除き、setNeedsUpdateConstraints()を使用して独自のバージョンのupdateConstraints()を実装する必要はほとんどありません。

つまり、要約すると、制約の変更を有効にするためにsetNeedsUpdateConstraintsを呼び出す必要はありません。実際、制約を変更すると、システムがレイア​​ウトパスの時間になったと判断したときに、制約が自動的に有効になります。

アニメーションを作成するときは、レイアウトをすぐに変更するのではなく、時間の経過に伴う変化を確認するために、発生していることを少しだけ制御したいとします。簡単にするために、1秒かかる(ビューが画面の左から右に移動する)アニメーションがあるとします。制約を更新して、ビューを左から右に移動しますが、それがすべてだった場合は、システムがレイア​​ウトパスの時間であると判断したとき、ある場所から別の場所にジャンプします。したがって、代わりに次のようなことを行います(testViewがself.viewのサブビューであると仮定)。

_testView.leftPositionConstraint.isActive = false // always de-activate
testView.rightPositionConstraint.isActive = true // before activation
UIView.animate(withDuration: 1) {
    self.view.layoutIfNeeded()
}
_

それを分解してみましょう:

まず、この_testView.leftPositionConstraint.isActive = false_は、ビューを左側の位置に維持する制約をオフにしますが、ビューのレイアウトはまだ調整されていません。

次に、この_testView.rightPositionConstraint.isActive = true_は、ビューを右側の位置に維持する制約をオンにしますが、ビューのレイアウトはまだ調整されていません。

次に、アニメーションをスケジュールし、そのアニメーションの各「タイムスライス」の間にself.view.layoutIfNeeded()を呼び出すと言います。つまり、アニメーションが更新されるたびに_self.view_のレイアウトパスを強制し、アニメーション全体の位置に基づいてtestViewレイアウトを再計算します。つまり、アニメーションの50%の後、レイアウトは50%になります。最初の(現在の)レイアウトと必要な新しいレイアウトの間。

このようにすると、アニメーションが有効になります。

だから全体的な要約では:

setNeedsConstraint()-ビューに影響を与えるものが変更されたため、ビューの制約を更新する必要があることをシステムに通知するために呼び出されます。制約は、システムがレイア​​ウトパスが必要であるとユーザーが判断するか、ユーザーが強制するまで、実際には更新されません。

updateConstraints()-ビューがアプリの状態に基づいて制約を更新するために実装する必要があります。

setNeedsLayout()-これは、ビューのレイアウトに影響を与える何か(おそらく制約)が変更され、次のレイアウトパス中にレイアウトを再計算する必要があることをシステムに通知します。その時のレイアウトには何も起こりません。

layoutIfNeeded()-スケジュールされた次のシステムを待つのではなく、ビューのレイアウトパスを実行します。この時点で、ビューとそのサブビューのレイアウトは実際に再計算されます。

2つの質問に直接回答できるように編集してください。

1)私の測定値に基づく:制約を変更した場合、それが有効になるには、setNeedsUpdateConstraintsを呼び出す必要がありますが、私の観察に基づいており、これは誤りです。アニメーション化するには次のコードで十分です:

_self.view.setNeedsLayout()
self.view.layoutIfNeeded()
_

どうして?

まず、読み取り値を誤解しているため、setNeedsUpdateConstraintsをまったく使用する必要はありません。次に、それらが十分である(アニメーションブロック内にあると想定)。setNeedsLayout()フラグは、_self.view_がそのレイアウト(およびそのサブビューレイアウト)を再計算して 'layoutIfNeeded()を計算する必要があるためです。 'レイアウトは強制的に即座に行われるため、アニメーションブロック内の場合は、アニメーションの更新ごとに行われます。

2)それから私は、おそらく何かの裏側で、​​他の方法で制約を更新しているのではないかと思いました。したがって、オーバーライドfunc updateViewConstraintsとオーバーライドfunc viewDidLayoutSubviewsにブレークポイントを配置しましたが、viewDidLayoutSubviewsのみがブレークポイントに到達しました。

では、自動レイアウトエンジンはこれをどのように管理していますか?

これの元の例で示すのが最善です:

__addBannerDistanceFromBottomConstraint.constant = 0

UIView.animate(withDuration: 5) {
    self.view.layoutIfNeeded()
}
_

最初の行では、定数を変更して制約を更新しています(setNeedsUpdateConstraintsを使用する必要はありません)が、ビューのレイアウト(つまり、実際のフレームの位置とサイズ)はまだ変更されていません。アニメーションの現在のタイムフレームに基づいて_self.view_のレイアウトを更新するアニメーションブロック内でself.view.layoutIfNeeded()を呼び出す場合。この時点で、ビューのフレーム位置/サイズが計算および調整されます。

それがより明確になることを願っていますが、実際には、質問の本文であなたの質問に詳細に回答していますが、説明が詳細すぎるかもしれません。

わかりやすくするために、画面上のすべてのビューには、サイズと位置の両方を制御するフレームがあります。このフレームは、プロパティを介して手動で設定するか、設定した制約を使用して計算されます。メソッドに関係なく、ビューの位置とサイズを決定するのはフレームではなく、制約です。制約は、ビューのフレームを計算するためにのみ使用されます。

さらに明確にするために、同じことを達成するが、2つの異なる方法を使用する2つの例を追加します。両方にtestViewがあり、メインビューのコントローラービューの中央に配置されます(これらは変更されず、例では効果的に無視できます)。そのwidthConstraintにはheightConstrainttestViewもあり、ビューの高さと幅を制御するために使用されます。 expanded boolプロパティは、testViewが展開されているかどうかを決定し、展開状態と折りたたみ状態を切り替えるために使用されるtestButtonがあります。

それを行う最初の方法はこれです:

_class ViewController: UIViewController {
    @IBOutlet var testView: UIView!
    @IBOutlet var testButton: UIButton!
    @IBOutlet var widthConstraint: NSLayoutConstraint!
    @IBOutlet var heightConstraint: NSLayoutConstraint!

    var expanded = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func testButtonAction(_ sender: Any) {
        self.expanded = !self.expanded

        if self.expanded {
            self.widthConstraint.constant = 200
            self.heightConstraint.constant = 200
        } else {
            self.widthConstraint.constant = 100
            self.heightConstraint.constant = 100
        }
        self.view.layoutIfNeeded() // You only need to do this if you want the layout of the to be updated immediately.  If you leave it out the system will decide the best time to update the layout of the test view.
    }

}
_

ここで、ボタンがタップされると、expanded boolプロパティが切り替えられ、定数を変更することで制約がすぐに更新されます。次に、layoutIfNeededが呼び出されてtestViewのレイアウトがすぐに再計算されます(したがって、表示が更新されます)。ただし、これを省略して、新しい制約に基づいてレイアウトを再計算することはできません。必要な場合の値。

次に、同じことを行う別の方法を示します。

_class ViewController: UIViewController {
    @IBOutlet var testView: UIView!
    @IBOutlet var testButton: UIButton!
    @IBOutlet var widthConstraint: NSLayoutConstraint!
    @IBOutlet var heightConstraint: NSLayoutConstraint!

    var expanded = false

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func testButtonAction(_ sender: Any) {
        self.expanded = !self.expanded
        self.view.setNeedsUpdateConstraints()
    }

    override func updateViewConstraints() {
        super.updateViewConstraints()
        if self.expanded {
            self.widthConstraint.constant = 200
            self.heightConstraint.constant = 200
        } else {
            self.widthConstraint.constant = 100
            self.heightConstraint.constant = 100
        }
    }
}
_

ここでボタンがタップされると、 'expanded' boolプロパティが切り替えられ、updateConstraintsIfNeededを使用して、レイアウトを再計算する前に制約を更新する必要があることをシステムに通知します(いつでも可能です)システムはそれが必要であると判断します)。システムがビューのレイアウトを再計算するためにこれらの制約を知る必要がある場合(システムが決定するもの)、自動的にupdateViewConstraintsを呼び出し、この時点で制約が新しい値に変更されます。

したがって、これらを試すと、どちらも基本的に同じように機能しますが、ユースケースは異なります。

方法1を使用すると、(前述のように)layoutIfNeededを次のようにアニメーションブロックにラップできるため、アニメーションが可能になります。

_    UIView.animate(withDuration: 5) {
        self.view.layoutIfNeeded()
    }
_

これにより、前回のレイアウト計算以降の制約の変更に基づいて、システムが初期レイアウトと新しいレイアウトの間でアニメーションを実行します。

方法2を使用すると、絶対に必要になるまで制約を変更する必要を延期することができます。制約が本当に複雑(多く)である場合、または制約を必要とする可能性のある多くのアクションが発生する可能性がある場合は、制約を変更する必要があります。次のレイアウトの再計算が必要になる前に変更する(必要のないときに継続的に制約を変更しないようにするため)。変更をアニメーション化する機能はありませんが、これを行うと、制約の複雑さのためにとにかくすべての処理が遅くなるため、おそらく問題ではありません。

これがさらに役立つことを願っています。

2

setNeedsUpdateConstraintsは、行った変更に基づいて変更される制約を更新します。たとえば、ビューに水平距離の制約がある隣接ビューがあり、その隣接ビューが削除された場合、制約は現在無効です。この場合、その制約を削除してsetNeedsUpdateConstraintsを呼び出す必要があります。基本的に、すべての制約が有効であることを確認します。これはビューを再描画しません。詳しくは こちら をご覧ください。
一方、 setNeedsLayoutは、再描画するビューをマークし、アニメーションブロック内に配置すると、描画がアニメーション化されます。

2
Santhosh R