iOS - 在 Swift 3 中以编程方式删除和激活新约束时布局损坏

Posted

技术标签:

【中文标题】iOS - 在 Swift 3 中以编程方式删除和激活新约束时布局损坏【英文标题】:iOS - Broken layout when removing and activating new constraints programatically in Swift 3 【发布时间】:2017-07-14 06:43:23 【问题描述】:

在 Swift 3 中以编程方式处理约束让我非常沮丧。在非常基本的层面上,我的应用程序显示了许多具有初始约束的视图,然后在旋转时应用新约束,以允许视图调整大小并根据需要重新定位。不幸的是,这远非易事,因为我还是 ios 开发和 Swift 的新手。我花了很多时间尝试 *** 和其他地方提供的许多不同的解决方案,但我一直得到相同的结果(最后详述)。

我有一个视图控制器(我们称之为“主视图控制器”),它的根视图包含两个子视图,视图 A 和视图 B 容器。根视图的背景颜色为粉红色。

视图 A 内部包含一个标签,垂直和水平居中,以及橙色背景色。 View A 有 4 个约束 - [Leading Space to Superview]、[Top Space to Top Layout Guide]、[Trailing Space to Superview] 和 [Bottom Space to Bottom Layout Guide]。

View B Container 最初没有内容。它有 4 个约束 - [Width Equals 240]、[Height Equals 128]、[Leading Space to Superview] 和 [Leading Space to Superview]。


我还有另一个视图控制器(我们称之为“视图 B 视图控制器”)驱动视图 B 容器的内容。为了简单起见,这只是一个没有自定义逻辑的默认视图控制器。 View B View Controller 的根视图包含一个子视图 View B。

视图 B 与上面的视图 A 几乎相同 - 单个标签垂直和水平居中,背景颜色为蓝色。视图 B 有 4 个约束 - [Leading Space to Superview]、[Top Space to Superview]、[Trailing Space to Superview] 和 [Bottom Space to Superview]。


在主视图控制器类中,我维护了对视图 A 和视图 B Container 的 IBOutlet 引用,以及上面提到的它们各自的约束。在下面的代码中,主视图控制器实例化视图 B 视图控制器并将后续视图添加到视图 B 容器中,应用灵活的宽度/高度自动调整大小掩码以确保其填充可用空间。然后它触发对内部 _layoutContainers() 函数的调用,该函数根据设备的方向执行许多约束修改操作。当前的实现如下:

从视图 A 中移除已知约束 从视图 B 容器中移除已知约束 根据设备方向,根据特定设计激活 View A 和 View B Container 的新约束(在下面的代码 cmets 中有详细说明) 针对所有视图触发 updateConstraintsIfNeeded() 和 layoutIfNeeded()

当发生resize事件时,代码允许viewWillTransition()触发,然后在完成回调中调用_layoutContainers()函数,使设备处于一个新的状态,可以遵循必要的逻辑路径。

整个主视图控制器单元如下:

import UIKit

class ViewController: UIViewController 

    // MARK: Variables

    @IBOutlet weak var _viewAView: UIView!

    @IBOutlet weak var _viewALeadingConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewATopConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewATrailingConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewABottomConstraint: NSLayoutConstraint!

    @IBOutlet weak var _viewBContainerView: UIView!

    @IBOutlet weak var _viewBContainerWidthConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewBContainerHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewBContainerTopConstraint: NSLayoutConstraint!
    @IBOutlet weak var _viewBContainerLeadingConstraint: NSLayoutConstraint!

    // MARK: UIViewController Overrides

    override func viewDidLoad() 
        super.viewDidLoad()

        // Instantiate View B's controller
        let viewBViewController = self.storyboard!.instantiateViewController(withIdentifier: "ViewBViewController")
        self.addChildViewController(viewBViewController)

        // Instantiate and add View B's new subview 
        let view = viewBViewController.view
        self._viewBContainerView.addSubview(view!)
        view!.frame = self._viewBContainerView.bounds
        view!.autoresizingMask = [.flexibleWidth, .flexibleHeight]

        viewBViewController.didMove(toParentViewController: self)

        self._layoutContainers()
    

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) 
        super.viewWillTransition(to: size, with: coordinator)

        coordinator.animate(alongsideTransition: nil, completion:  _ in
            self._layoutContainers()
        )
    

    // MARK: Internal

    private func _layoutContainers() 

        // Remove View A constraints
        self._viewAView.removeConstraints([
            self._viewALeadingConstraint,
            self._viewATopConstraint,
            self._viewATrailingConstraint,
            self._viewABottomConstraint,
        ])

        // Remove View B Container constraints
        var viewBContainerConstraints: [NSLayoutConstraint] = [
            self._viewBContainerTopConstraint,
            self._viewBContainerLeadingConstraint,
        ]

        if(self._viewBContainerWidthConstraint != nil) 
            viewBContainerConstraints.append(self._viewBContainerWidthConstraint)
        
        if(self._viewBContainerHeightConstraint != nil) 
            viewBContainerConstraints.append(self._viewBContainerHeightConstraint)
        

        self._viewBContainerView.removeConstraints(viewBContainerConstraints)


        // Portrait:
        // View B - 16/9 and to bottom of screen
        // View A - anchored to top and filling the remainder of the vertical space

        if(UIDevice.current.orientation != .landscapeLeft && UIDevice.current.orientation != .landscapeRight) 

            let viewBWidth = self.view.frame.width
            let viewBHeight = viewBWidth / (16/9)
            let viewAHeight = self.view.frame.height - viewBHeight

            // View A - anchored to top and filling the remainder of the vertical space
            NSLayoutConstraint.activate([
                self._viewAView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
                self._viewAView.topAnchor.constraint(equalTo: self.view.topAnchor),
                self._viewAView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
                self._viewAView.bottomAnchor.constraint(equalTo: self._viewBContainerView.topAnchor),
            ])

            // View B - 16/9 and to bottom of screen
            NSLayoutConstraint.activate([
                self._viewBContainerView.widthAnchor.constraint(equalToConstant: viewBWidth),
                self._viewBContainerView.heightAnchor.constraint(equalToConstant: viewBHeight),
                self._viewBContainerView.topAnchor.constraint(equalTo: self.view.topAnchor, constant: viewAHeight),
                self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            ])
        

        // Landscape:
        // View B - 2/3 of screen on left
        // View A - 1/3 of screen on right
        else 
            let viewBWidth = self.view.frame.width * (2/3)

            // View B - 2/3 of screen on left
            NSLayoutConstraint.activate([
                self._viewBContainerView.widthAnchor.constraint(equalToConstant: viewBWidth),
                self._viewBContainerView.heightAnchor.constraint(equalToConstant: self.view.frame.height),
                self._viewBContainerView.topAnchor.constraint(equalTo: self.view.topAnchor),
                self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            ])

            // View A - 1/3 of screen on right
            NSLayoutConstraint.activate([
                self._viewAView.leadingAnchor.constraint(equalTo: self._viewBContainerView.trailingAnchor),
                self._viewAView.topAnchor.constraint(equalTo: self.view.topAnchor),
                self._viewAView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
                self._viewAView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
            ])
        

        // Fire off constraints and layout update functions

        self.view.updateConstraintsIfNeeded()
        self._viewAView.updateConstraintsIfNeeded()
        self._viewBContainerView.updateConstraintsIfNeeded()

        self.view.layoutIfNeeded()
        self._viewAView.layoutIfNeeded()
        self._viewBContainerView.layoutIfNeeded()
    


我的问题是,虽然应用程序的初始加载显示了预期的结果(视图 B 保持 16/9 比例并坐在屏幕底部,但视图 A 占用了剩余空间):

任何后续的旋转都会完全破坏视图并且不会恢复:

此外,一旦加载应用程序,就会引发以下约束警告:

TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<_UILayoutSupportConstraint:0x600000096c60 _UILayoutGuide:0x7f8d4f414110.height == 0   (active)>",
    "<_UILayoutSupportConstraint:0x600000090ae0 V:|-(0)-[_UILayoutGuide:0x7f8d4f414110]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
    "<NSLayoutConstraint:0x600000096990 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f413e60]   (active)>",
    "<NSLayoutConstraint:0x608000094e10 V:|-(456.062)-[UIView:0x7f8d4f413e60]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000096990 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f413e60]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.



TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<NSLayoutConstraint:0x600000096940 UIView:0x7f8d4f413e60.leading == UIView:0x7f8d4f40f9e0.leadingMargin   (active)>",
    "<NSLayoutConstraint:0x608000094e60 H:|-(0)-[UIView:0x7f8d4f413e60]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000096940 UIView:0x7f8d4f413e60.leading == UIView:0x7f8d4f40f9e0.leadingMargin   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.



TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<_UILayoutSupportConstraint:0x600000096d50 _UILayoutGuide:0x7f8d4f40f4b0.height == 0   (active)>",
    "<_UILayoutSupportConstraint:0x600000096d00 _UILayoutGuide:0x7f8d4f40f4b0.bottom == UIView:0x7f8d4f40f9e0.bottom   (active)>",
    "<NSLayoutConstraint:0x600000092e30 V:[UIView:0x7f8d4f40fd90]-(0)-[_UILayoutGuide:0x7f8d4f40f4b0]   (active)>",
    "<NSLayoutConstraint:0x608000092070 UIView:0x7f8d4f40fd90.bottom == UIView:0x7f8d4f413e60.top   (active)>",
    "<NSLayoutConstraint:0x608000094e10 V:|-(456.062)-[UIView:0x7f8d4f413e60]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
    "<NSLayoutConstraint:0x600000096e40 'UIView-Encapsulated-Layout-Height' UIView:0x7f8d4f40f9e0.height == 667   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000092e30 V:[UIView:0x7f8d4f40fd90]-(0)-[_UILayoutGuide:0x7f8d4f40f4b0]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.



TestResize[1794:51030] [LayoutConstraints] Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. 
    Try this: 
        (1) look at each constraint and try to figure out which you don't expect; 
        (2) find the code that added the unwanted constraint or constraints and fix it. 
(
    "<_UILayoutSupportConstraint:0x600000096c60 _UILayoutGuide:0x7f8d4f414110.height == 20   (active)>",
    "<_UILayoutSupportConstraint:0x600000090ae0 V:|-(0)-[_UILayoutGuide:0x7f8d4f414110]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>",
    "<NSLayoutConstraint:0x600000096850 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f40fd90]   (active)>",
    "<NSLayoutConstraint:0x608000093b50 V:|-(0)-[UIView:0x7f8d4f40fd90]   (active, names: '|':UIView:0x7f8d4f40f9e0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x600000096850 V:[_UILayoutGuide:0x7f8d4f414110]-(0)-[UIView:0x7f8d4f40fd90]   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.

感谢您阅读到这里!肯定有人遇到过(并希望解决)这个或类似的问题。任何帮助将不胜感激!

【问题讨论】:

你有没有试过让网点不那么弱? @Aakash 我已经尝试过非弱属性(对于视图和约束)并且没有区别;旋转仍然会破坏视图。 【参考方案1】:

与其尝试添加和删除约束,不如考虑调整优先级来转换您的视图。

所以对于您来说,默认布局有一个优先级为 900 的约束。然后添加第二个优先级为 1 的冲突约束。现在要切换显示模式,只需将第二个约束优先级移动到 900 以上,然后返回到下方以反转。只需更改优先级,即可在 Interface Builder 中轻松测试所有内容。

您还可以将更改放在动画块中以获得良好的平滑过渡。

-

要考虑使用的另一件事是尺寸等级。使用它,您可以指定特定约束仅适用于某些方向,这样您就可以完全“免费”获得所需的行为,只需在 IB 中进行设置。

【讨论】:

成功了!非常感谢@trapper 的建议,我重写了代码(参见pastebin.com/7qJ3FGnv)来初始化和存储所有适当的约束,然后根据应该显示哪组约束来切换所有约束的优先级(我使用了 999 和1 代表高/低)。我遇到的一个问题是将所需的约束设置为 1000 的优先级 - 正如 another *** post 中所述,您无法在必需值和非必需值之间进行更改。 是的,如果你想切换它,你不能使用 1000。此外,通过使用小于 999 的“默认”布局,您仍然可以在其上方放置一些东西,这就是我说使用 900 的原因。所以您只需要在一个约束上切换优先级,而不是更改两个。 顺便说一句,关于您的 pastebin,您可以使用大小类来执行此操作,并且根本不需要任何代码【参考方案2】:

部分问题是在_layoutContainers 中,您从情节提要中删除了约束并添加了现在,但在随后的轮换中,您不会删除之前添加的约束。您应该存储您创建的新约束,以便下次屏幕旋转时,您可以获取旧约束并删除它们。

此外,从 viewDidLoad 调用 _layoutContainers 在 VC 生命周期中还为时过早,因为视图框架还没有正确的值。您可以创建纵横比约束,因此您不必手动计算大小。

例如,

的纵向约束
// View B - 16/9 and to bottom of screen
NSLayoutConstraint.activate([
    self._viewBContainerView.heightAnchor.constraint(equalToConstant: self._viewBContainerView.widthAnchor, multiplier: 16.0 / 9.0),
    self._viewBContainerView.topAnchor.constraint(equalTo: self.view.bottomAnchor),
    self._viewBContainerView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
    // should there be a constraint for self._viewBContainerView.trailingAnchor?
])

【讨论】:

感谢您抽出宝贵时间阅读/回复 :) 我修改了我的代码以存储每个新约束并将对 _layoutConstraints() 的调用移至 viewDidAppear (pastebin.com/eDaar909) 但不幸的是我仍然在第一次旋转后获得相同的行为。 通过调试和单步执行代码,我注意到日志中的约束警告是在视图 A 约束被激活后首先生成的(pastebin.com/eDaar909 中的第 115 行)。我认为这可能是一个优先级问题,因为视图 ​​A 引用了视图 B 约束锚,但更改激活顺序似乎并没有改变这一点。

以上是关于iOS - 在 Swift 3 中以编程方式删除和激活新约束时布局损坏的主要内容,如果未能解决你的问题,请参考以下文章

在 iOS 10 + Swift 3 中以编程方式在我的 WKWebView 上方添加 UILabel

如何在 ios swift 中以编程方式创建自动布局等宽约束?

如何在ios swift中以编程方式创建自动布局等宽度约束?

在 iOS Swift 3 中以编程方式添加 Web 视图时如何在 UIWebView 中获取响应状态代码?

我可以在 Swift 5 和 IOS 12 中以编程方式更改 iOS 屏幕壁纸吗

在 SWIFT OS X 中以编程方式添加和删除 NSTextField