iOS Autolayout自适应UI问题

时间:2017-01-15 01:37:58

标签: ios swift autolayout adaptive-layout

我想根据宽度是否大于高度来更改布局。但我不断收到布局警告。我在自适应布局上观看了两个WWDC视频,这些视频帮了很多但没有解决问题。我使用以下代码创建了一个简单版本的布局。这只是让人们可以重现我的问题。代码在iPhone 7 plus上运行。

import UIKit

class ViewController: UIViewController {

    var view1: UIView!
    var view2: UIView!

    var constraints = [NSLayoutConstraint]()

    override func viewDidLoad() {
        super.viewDidLoad()

        view1 = UIView()
        view1.translatesAutoresizingMaskIntoConstraints = false
        view1.backgroundColor = UIColor.red

        view2 = UIView()
        view2.translatesAutoresizingMaskIntoConstraints = false
        view2.backgroundColor = UIColor.green

        view.addSubview(view1)
        view.addSubview(view2)

        layoutPortrait()
    }

    func layoutPortrait() {
        let views = [
            "a1": view1,
            "a2": view2
        ]

        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a1]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a2]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)
        NSLayoutConstraint.activate(constraints)
    }

    func layoutLandscape() {
        let views = [
            "a1": view1,
            "a2": view2
        ]

        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|[a1(a2)][a2(a1)]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1]|", options: [], metrics: nil, views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a2]|", options: [], metrics: nil, views: views)
    NSLayoutConstraint.activate(constraints)
    }

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

         NSLayoutConstraint.deactivate(constraints)

         constraints.removeAll()

         if (size.width > size.height) {
             layoutLandscape()
         } else {
            layoutPortrait()
         }

     } 

}

当我旋转几次时,xcode会记录警告。我想我的布局切换到了早期,因为它仍然在变化,但我已经将肖像的高度设置为大或者其他东西。有人知道我做错了吗?

约束警告:(从横向回到肖像时发生)

[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:0x618000081900 V:|-(0)-[UIView:0x7ff68ee0ac40]   (active, names: '|':UIView:0x7ff68ec03f50 )>",
    "<NSLayoutConstraint:0x618000081d60 UIView:0x7ff68ee0ac40.height == 450   (active)>",
    "<NSLayoutConstraint:0x618000083cf0 V:[UIView:0x7ff68ee0ac40]-(0)-[UIView:0x7ff68ee0ade0]   (active)>",
    "<NSLayoutConstraint:0x618000083d40 V:[UIView:0x7ff68ee0ade0]-(0)-|   (active, names: '|':UIView:0x7ff68ec03f50 )>",
    "<NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x618000081d60 UIView:0x7ff68ee0ac40.height == 450   (active)>

5 个答案:

答案 0 :(得分:2)

你是对的,你过早地做了约束工作。 <NSLayoutConstraint:0x600000085d70 'UIView-Encapsulated-Layout-Height' UIView:0x7ff68ec03f50.height == 414 ...... 是视图控制器在景观中视图高度的系统创建约束 - 为什么它是414,正好是iPhone 5.5(6/7 Plus)在景观中的高度。旋转尚未开始,450高度约束的垂直约束发生冲突,您将收到警告。

因此,您必须在完成旋转后添加约束。 viewWillLayoutSubviews是逻辑位置,因为当视图大小准备好但在屏幕上出现任何内容之前调用它,并且可以为系统保存它本来可以进行的布局传递。

但是,要使警告静音,您仍需要删除viewWillTransition(to size: with coordinator:)中的约束。系统在调用viewWillLayoutSubviews()之前执行新的自动布局计算,当从纵向移动到横向时,您将以另一种方式获得警告。

编辑:正如@sulthan在评论中指出的那样,由于其他事情可以触发布局,因此在添加约束之前还应始终删除约束。如果[constraints]阵列被清空,则无效。由于在停用后清空数组是一个至关重要的步骤,因此它应该采用自己的方法,因此clearConstraints()

另外,请记住检查初始布局中的方向 - 您在layoutPortrait()中调用viewDidLoad()当然不是问题。

新/改变方法:

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    // Should be inside layoutPortrait/Landscape methods, here to keep code sample short.
    clearConstraints() 

    if (self.view.frame.size.width > self.view.frame.size.height) {
        layoutLandscape()
    } else {
        layoutPortrait()
    }
}

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

    clearConstraints()
}

/// Ensure array is emptied when constraints are deactivated. Can be called repeatedly. 
func clearConstraints() {
    NSLayoutConstraint.deactivate(constraints) 
    constraints.removeAll()
}

答案 1 :(得分:0)

据我所知,警告实际上是因为约束被移动太晚。旋转到横向时会出现警告 - 450约束仍然设置且无法满足,因为视图的高度太小。

我尝试了几件事,看起来效果最好的是在较小的视图上设置约束,这样较大的视图的高度仍然是450.

而不是

constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)

我用过

let height = view.frame.height - 450
constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1][a2(\(height))]|", options: [], metrics: nil, views: views)

然后在viewWillTransitionToSize

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    NSLayoutConstraint.deactivate(self.constraints)
    self.constraints.removeAll()

    coordinator.animate(alongsideTransition: { (context) in

        if (size.width > size.height) {
            self.layoutLandscape()
        } else {
            self.layoutPortrait()
        }
    }, completion: nil)

} 

修改

另一件对我有用的事情(也许是一个更一般的答案,虽然我不确定它是否推荐):在调用super.viewWillTransition之前停用约束,然后在过渡期间动画其余部分。我的理由是,在旋转开始之前技术上立即消除约束,因此一旦应用程序开始旋转到横向(此时旧的高度约束将不可满足),约束已经停用并且新的约束可以采取效果。

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        NSLayoutConstraint.deactivate(constraints)
        super.viewWillTransition(to: size, with: coordinator)
        constraints.removeAll()

        coordinator.animate(alongsideTransition: { (context) in
            if (size.width > size.height) {
                self.layoutLandscape()
            } else {
                self.layoutPortrait()
            }

        }, completion: nil)            
    } 

答案 2 :(得分:0)

我想出了和@Samantha一样的答案。但是,在调用deactivate之前,我无需致电super。这是使用特征集合的替代方法,但transitionToSize方法的工作方式类似。

override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
    super.willTransition(to: newCollection, with: coordinator)
    NSLayoutConstraint.deactivate(constraints)

    constraints.removeAll()

    coordinator.animate(alongsideTransition: { context in
        if newCollection.verticalSizeClass == .compact {
            self.layoutLandscape()
        } else {
            self.layoutPortrait()
        }
    }, completion: nil)
}

答案 3 :(得分:0)

这是有问题的部分:

constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450)][a2]|", options: [], metrics: nil, views: views)

此行生成4个约束:

  1. a1距离最远0
  2. a1的高度为450
  3. a1a2距离为0
  4. a2到底部距离为0
  5. 四种约束一起在横向模式下中断,因为屏幕的整体高度将小于450

    您现在正在做的是在更改为横向模式之前更新约束,并且在从纵向切换到横向时起作用。但是当你从横向切换到肖像时,你过早地改变了你的约束,并且在短时间内,你仍然在风景中得到肖像约束,触发警告。

    请注意,您的约束没有任何实质性错误。如果你想修复你的警告,我认为最好的解决方案是减少你的一个约束的优先级,例如:高度约束:

    constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|[a1(450@999)][a2]|", options: [], metrics: nil, views: views)
    

答案 4 :(得分:0)

如果布局约束问题是由于固定的(或以编程方式修改的)宽度或高度约束与ThemeOverlay.AppCompat.Dark.ActionBarUIView-Encapsulated-Layout-Width约束冲突而引起的,则说明该问题牢不可破约束会导致您抛弃了太大的必需约束(优先级1000)。

您可以通过简单地降低冲突约束的优先级(可能仅为999)来解决问题,该优先级将解决临时冲突,直到旋转完成,随后的布局将按照您需要的宽度或高度进行。