【问题标题】:iOS Autolayout adaptive UI problemsiOS Autolayout 自适应 UI 问题
【发布时间】:2017-05-30 03:27:02
【问题描述】:

我想根据宽度是否大于高度来更改布局。但我不断收到布局警告。我观看了两个关于自适应布局的 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)>

【问题讨论】:

  • 你为什么不试试锚布局。 --> Apple Doc
  • 需要支持ios 8
  • 是的,它支持 ios 8,甚至我在 swift 3.x 中完成了。
  • 你的意思是@available(iOS 9.0, *) open class NSLayoutAnchor : NSObject??
  • 对不起,我弄错了,它支持9.x。你说的对。 对不起 :(

标签: ios swift autolayout adaptive-layout


【解决方案1】:

据我所知,警告实际上是因为移动约束为时已晚。旋转到横向时出现警告 - 仍然设置 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】:

我想出了与@Samantha 大致相同的答案。但是,在拨打super 之前,我不需要拨打deactivate。这是使用 trait 集合的替代方法,但 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】:

    这是有问题的部分:

    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

    这四个约束在横向模式下一起中断,因为屏幕的总高度将小于450

    您现在要做的是在更改为横向模式之前更新约束,并且当您从纵向切换到横向时它会起作用。但是,当您从横向切换到纵向时,您过早地更改了约束,并且在短时间内,您仍然处于横向时会遇到纵向约束,从而触发该警告。

    请注意,您的约束本质上没有任何问题。如果您想修复警告,我认为最好的解决方案是降低您的约束之一的优先级,例如高度限制:

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

    【讨论】:

    • 即使这样可以解决问题。我觉得 Mike Sand 的回答是更干净的方法。因为我需要在约束中添加很多 @ 999。不过谢谢我学到了很多
    【解决方案4】:

    你是对的,你做的约束工作太早了。 &lt;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()

    另外,记得在初始布局中检查方向 - 你在 viewDidLoad() 中调用 layoutPortrait() 认为这当然不是问题。

    新/更改方法:

    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()
    }
    

    【讨论】:

    • 我认为您还应该始终删除 viewWillLayoutSubviews 中的约束,因为在某些情况下视图会在不转换的情况下进行布局。
    • @Sulthan 说得好。 viewWillTransition(to size:with coordinator:) 将获得其中许多,但其他事情可能会触发布局传递,并且实际上应该始终在添加新约束之前调用删除约束/清空数组。我已经更新了我的答案。
    【解决方案5】:

    如果布局约束问题是由于与UIView-Encapsulated-Layout-WidthUIView-Encapsulated-Layout-Height 约束冲突的固定(或以编程方式修改的)宽度或高度约束引起的,则牢不可破 约束导致您的太大,需要(优先级 1000)约束被抛出。

    您可以通过简单地降低冲突约束的优先级(可能只是降低到 999)来经常解决问题,这将适应任何临时冲突,直到旋转完成并且后续布局将继续使用您需要的宽度或高度。

    【讨论】:

      猜你喜欢
      • 2018-09-20
      • 2015-09-28
      • 1970-01-01
      • 2020-08-07
      • 2019-01-30
      • 2016-02-23
      • 2013-05-12
      • 2015-05-18
      • 1970-01-01
      相关资源
      最近更新 更多