【问题标题】:Terrifyingly, layoutIfNeeded sequence acts differently on iPad versus iPhone; how to fix?可怕的是,layoutIfNeeded 序列在 iPad 和 iPhone 上的作用不同;怎么修?
【发布时间】:2026-01-11 08:50:01
【问题描述】:

启动 Xcode 并为清楚起见仅构建 9.3 通用应用程序。因此,将 9.3 iPad 与 9.3 iPhone 进行比较。为模拟器和设备构建 - 在两者上发布展览。

应用程序在所有四个方向上旋转。

有一个典型的情况,你会做这样的事情......

@IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!

var heightFraction:CGFloat = 0.6
    {
    didSet
        {
        if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
        if ( heightFraction < minHeight ) { heightFraction = minHeight }
        
        let h = view.bounds.size.height
        spaceshipHeightPerScreen.constant = h * heightFraction
        
        self.view.layoutIfNeeded()  // holy!  read on....
        }
    }

注意更改约束后的layoutIfNeeded()

继续典型的例子,你会得到类似的东西

override func viewDidLayoutSubviews()
    {
    super.viewDidLayoutSubviews()
    heightFraction = (heightFraction)
    // use "autolayout power" for perfection every pass.

    // now that basic height/position is set,
    save/load reactive positions...
    position detail stuff...
    }

检查一下...我整天都在这样做,只是碰巧使用 iPhone。

有趣的是,您不需要 layoutIfNeeded 调用。

@IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
    {
    didSet
        {
        if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
        if ( heightFraction < minHeight ) { heightFraction = minHeight }
        let h = view.bounds.size.height
        spaceshipHeightPerScreen.constant = h * heightFraction
        }
    }

工作正常。

但是在一天结束时,我把它放在一些 iPad 上,然后....一切都坏了!

每当您旋转横向/纵向时,都会出现问题。

头疼之后,我意识到您确实需要 layoutIfNeeded 调用,在 iPad 上。那是在相同的操作系统上。

确实,无论操作系统版本如何,该行为都会表现出来。它适用于所有 iPhone/所有 iPad。卧槽!

@IBOutlet weak var doorHeightPerScreen: NSLayoutConstraint!
var heightFraction:CGFloat = 0.6
    {
    didSet
        {
        if ( heightFraction > maxHeight ) { heightFraction = maxHeight }
        if ( heightFraction < minHeight ) { heightFraction = minHeight }
        let h = view.bounds.size.height
        spaceshipHeightPerScreen.constant = h * heightFraction
        self.view.layoutIfNeeded() //MUST HAVE, IN IPAD CASE!!!!!!
        }
    }

对我来说,非常令人不安他们会以不同的方式工作。

我想知道的是,是否有某个地方可以使它们工作相同?莫非是我的错?

两者之间是否还有其他已知差异 - 或者确实“已知”存在一些这样的错误?

我想不出我在任何地方做过什么奇怪或不寻常的事情,除了整个应用程序在第一个视图中具有override func supportedInterfaceOrientations() -&gt; UIInterfaceOrientationMask { return .All },如果您想将设备倒置,这是正常的;我怀疑它的相关性。除此之外,它是一个非常“干净”的新鲜应用。

它给了我一种矩阵故障的感觉——太可怕了。

这是什么原因造成的?


根据 RobM 的问题,初始 ViewController 上的 SimulatedMetrics 设置(属性选项卡)是...


应用的通用方案:第一个场景“通用”为全屏,设备大小。 “Live”有一个大小相同的容器(使用“Trailing”等/约束为零)。在 Live 中,有一个容器视图“Quad”,它的大小也完全符合“Live”,所以它也是全屏的。 Quad:UIViewController 展示了我描述的问题。 Quad 包含位于视图周围的各种对象(图像、自定义控件等)。当应用启动时,一切正常。

关于设备(或类似设备)的旋转:在更改约束之后(我不知道这是否相关):iPad 需要layoutIfNeeded 调用 IS(所有 iPad ),但对于 iPhone(所有 iPhone)NOT 是必需的。模拟器和设备上的行为是相同的。


另一个例子.....

我发现了另一个令人震惊的例子。

在 UICollectionView 中,自定义单元格(只是简单的静态大小的单元格)。如果您碰巧更改了约束条件(想象一下,在单元格内调整图标或产品照片的大小)。

在 IPAD 上必须确保在 layoutIfNeeded 中重新调整,否则 不会在单元格的第一次出现时工作

在 IPHONE 上它的行为肯定是不同的:它会在单元格第一次出现之前“为你做这件事”,如果你碰巧省略了它。

完全怪异!!

我在每台 iPad 和每台 iPhone 上都进行了测试。 (此外,异常行为恰好在设备或模拟器上表现出来:模拟器没有区别。)

【问题讨论】:

  • 您在使用情节提要吗?故事板中的视图大小是多少?
  • 我的意思是你的场景在故事板中的大小是多少?
  • 单击故事板中的初始视图控制器。打开属性检查器。模拟指标下的“大小”是什么?

标签: ios ipad autolayout nslayoutconstraint


【解决方案1】:

默认的模拟指标大小是“推断”的,它(如果场景不是转场或关系的目标)为您提供 600x600 的视图,这与任何 iOS 设备的屏幕大小都不对应。您有时将模拟指标尺寸更改为“iPhone 5.5 英寸”,可能与您的主要测试设备的尺寸相匹配。

当从情节提要(或 xib)加载视图时,它会以它在情节提要中的大小进行加载。然后可以通过其容器调整其大小(如果它是应用程序的根视图,则为 UIWindow,或者如果它是包含的视图控制器的根视图,则通过其父视图)。

在您的情况下,听起来您的主测试设备的屏幕与情节提要中的根视图具有相同的大小,因此测试设备不必像您期望的那样运行尽可能多的布局。

当您使用屏幕尺寸与故事板中根视图尺寸不同的测试设备时,测试设备必须进行更多布局。

我没有尝试重现您的问题,因此我并没有声称这是对您所看到内容的完整解释。很可能涉及到一个 iOS 错误。不过,这应该可以解释为什么您的应用在不同设备上的行为不同。我相信这也是 Apple 选择默认推断尺寸 600x600 的原因:由于没有设备屏幕是该尺寸,所有设备都必须进行相同数量的布局。

【讨论】:

  • 嗨,罗伯,太棒了!谢谢,我去调查一下。目前,似乎所有 iPhone(任何形状)和所有 iPad 之间都有区别。我去看看!
  • “当从情节提要(或 xib)加载视图时,它会以它在情节提要中的大小加载。”太惊人了,我不知道:我认为那里的设置完全不相关,只与故事板上的表现有关。
  • 嗯,我检查了它,它似乎并没有影响它:所以,(1)注释掉这行 - iPad 上的问题展示(完美适用于所有手机)。现在,将 Simulated Metrics / 初始场景更改为“iPad”。该问题仍然出现在 iPad 上(在所有手机上仍然完美)。 (2) 将代码行返回给程序:在所有 iPad/iPhone 上完美 (2B),并将模拟指标更改为“iPad”,在所有 iPad/iPhone 上仍然完美。相反,如果您将其视为“在行注释时也应该在 iPhone 上失败” - 无论模拟指标如何,都会发生相同的行为(“在 iPhone 上仍然失败”)......
  • 简而言之,不幸的是,情况似乎并非如此——在我的特殊情况下。它仍然是一个完全的谜!
  • 嗨@robmayoff。仅供参考,我在 iPad 与 iPhone 设备上发现了另一个奇怪差异的例子,即如何处理绘制序列。我将它添加到 Q. Freaky 的底部!
【解决方案2】:

我无法重现您所看到的;很高兴看到一个完整的例子。在我的模型中,我配置了一个视图控制器,该控制器具有一个具有单个子视图的视图,并具有控制子视图高度的约束。我根据视图大小更改了viewDidLayout 中的子视图高度约束。 iPhone 和 iPad 的行为是相同的,并且在任何视图上都无需调用 layoutIfNeeded

也就是说,我认为一旦视图完成其布局,您正在更改子视图约束 - 是吗?我认为更好的方法是在此之前通过viewWillTransitionToSize:withTransitionCoordinator: 布局您的子视图。

func viewWillTransitionToSize(_ size: CGSize,
    withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator)

这样,视图层次结构的自动布局可以一次性完成。该方法仅在视图改变大小时调用,因此在视图首次加载时不会调用;您必须在其他地方设置初始约束 - 因为它们取决于视图大小,也许您可​​以使用 viewWillAppear

或者,(可能更正确),子类化您的视图控制器的视图并覆盖updateConstraints。这是更改约束常量最合适的地方。

最后,在你的属性设置器中,你永远不应该调用 view.layoutIfNeeded()。如果有的话,您可以设置 view.setNeedsLayout() 以便布局在下一次 runloop 迭代中发生,并获取所有可能需要表示的更改。

【讨论】:

  • 啊!请注意,heightFraction 是动态的,它会不断变化。 (把它想象成你可以用滑块改变的东西,比如说。它在屏幕上显示宇宙飞船或任何东西,用户可以改变高度分数。)当然,(无论出于何种原因)改变约束是完全正常的(我意味着在运行时)在屏幕上移动东西......(对不起,我认为这从代码示例中很明显,它是一个属性和变量名。)因此,正如你所说的“我改变了子视图高度约束。 .." 只需添加一个滑块即可在使用时更改属性。
  • 在第 2、3、4 段中重新提出您的建议;我会调查的,谢谢。 但是请注意,手头的问题是 - 即使我在做一些非惯用的,甚至是“错误的” - 令人难以置信的是,它在两种设备类型上的工作方式不同! :O
  • 是你的最后一段 - 是的!这就是重点:我认为你不需要它,也没有它。在 iPhone 上完美运行。奇怪的是,在 iPad 上,你需要它! WTH
  • 嗨@TomSwift - 谢谢你,不要把那些赏金花在一个地方!尽管它实际上并没有找到针对所指出的奇怪问题的展览,但我认为这是最接近答案的地方,并且围绕该主题提供了大量有用的信息,因此再次感谢。我真的相信这只是一个轻微的 iOS 错误:一有时间,我就会在这里发布一个演示项目,它可以非常清楚地显示它,并且我会在这里发布一条消息。干杯!
  • 说@tomswift!我发现了另一个令人震惊的例子。在 UICollectionView 中,自定义(静态大小)单元格。如果您碰巧更改了约束(想象一下在单元格内调整图标或产品镜头的大小)。 在 IPADDO 必须确保在layoutIfNeeded 中重新调整,否则它将在单元格的第一次通过时不起作用。虽然 ON IPHONE 它的行为肯定不同,但如果你碰巧省略它,它会“为你做这件事”。怪怪的!