子视图控制器的安全区域嵌入不会更新,如果部分超出屏幕

19

我在 iPhone X 的横屏模式下移动子视图控制器时遇到了安全区域的困难。

我的根视图控制器中有一个可移动的视图,其中包含一个嵌入的子视图控制器。实际应用程序是一个侧边栏菜单滑动界面。这里的示例代码是一个简化版本。

如果表格位于屏幕左侧,则安全区域布局规则会将其单元格内容视图缩进到右侧,以适应刘海区域。正确的。但是,如果我将子视图控制器从屏幕左侧移开,子控件的插页不会更新以重新布局内容。

我意识到,实际上,只要子视图控制器在其最终位置上完全显示,就一切正常运行。如果任何部分超出屏幕,则安全区域不会更新。

以下是演示问题的示例代码。这可以使用标准的 Single View App Xcode 模板项目:用所示代码替换 ViewController 文件代码。运行时,向右滑动表格将其从屏幕左侧移动到屏幕右侧。

请查看“constraint(...,multiplier:0.5)”行。这将可移动视图的宽度设置为相对于屏幕。在0.5时,表格完全适合屏幕,安全区域会随着其移动而更新。停靠在左侧时,表格单元格会遵守安全区域插页,而停靠在右侧时,表格单元格没有额外的插页是正确的。

一旦乘数超过0.5,即使在0.51时,向右滑动表格的一部分也会超出屏幕范围。在这种情况下,不会发生安全区域更新,因此表格单元格内容插页太大-即使表格左侧现在与安全区域无关。

更让人费解的是,如果它们不是 UIViewController 的视图,则布局在 UIView 上似乎可以正常工作。但我需要它能够与 UIViewControllers 一起使用。

有谁能解释如何使子视图控制器尊重正确的安全区域吗? 谢谢。

问题的截屏

可复制代码:

class ViewController: UIViewController {

    var leftEdgeConstraint : NSLayoutConstraint!
    var viewThatMoves : UIView!
    var myEmbeddedVC : UIViewController!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = UIColor.gray

        self.viewThatMoves = UIView()
        self.viewThatMoves.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(self.viewThatMoves)

        // Relayout during animation work with multiplier = 0.5
        // With any greater value, like 0.51 (meaning part of the view is offscreen), relayout does not happen
        self.viewThatMoves.widthAnchor.constraint(equalTo: self.view.widthAnchor, multiplier: 0.5).isActive = true
        self.viewThatMoves.heightAnchor.constraint(equalTo: self.view.heightAnchor).isActive = true
        self.viewThatMoves.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true
        self.leftEdgeConstraint = self.viewThatMoves.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0)
        self.leftEdgeConstraint.isActive = true

        // Embed child ViewController
        self.myEmbeddedVC = MyTableViewController()

        self.addChildViewController(self.myEmbeddedVC)
        self.myEmbeddedVC.view.translatesAutoresizingMaskIntoConstraints = false
        self.viewThatMoves.addSubview(self.myEmbeddedVC.view)
        self.myEmbeddedVC.didMove(toParentViewController: self)

        // Fill containing view
        self.myEmbeddedVC.view.leftAnchor.constraint(equalTo: self.viewThatMoves.leftAnchor).isActive = true
        self.myEmbeddedVC.view.rightAnchor.constraint(equalTo: self.viewThatMoves.rightAnchor).isActive = true
        self.myEmbeddedVC.view.topAnchor.constraint(equalTo: self.viewThatMoves.topAnchor).isActive = true
        self.myEmbeddedVC.view.bottomAnchor.constraint(equalTo: self.viewThatMoves.bottomAnchor).isActive = true

        let swipeLeftRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(recognizer:)))
        swipeLeftRecognizer.direction = .left
        let swipeRightRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(handleSwipe(recognizer:)))
        swipeRightRecognizer.direction = .right

        self.viewThatMoves.addGestureRecognizer(swipeLeftRecognizer)
        self.viewThatMoves.addGestureRecognizer(swipeRightRecognizer)
    }

    @objc func handleSwipe(recognizer:UISwipeGestureRecognizer) {
        UIView.animate(withDuration: 1) {

            if recognizer.direction == .left {
                self.leftEdgeConstraint.constant = 0
            }
            else if recognizer.direction == .right {
                self.leftEdgeConstraint.constant = self.viewThatMoves.frame.size.width
            }

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

            // Tried this: has no effect
            // self.myEmbeddedVC.viewSafeAreaInsetsDidChange()
        }
    }
}

class MyTableViewController: UITableViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        self.view.backgroundColor = UIColor.blue
        self.title = "Test Table"

        // Uncomment the following line to preserve selection between presentations
        // self.clearsSelectionOnViewWillAppear = false

        self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Left", style: .plain, target: nil, action: nil)
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Right", style: .plain, target: nil, action: nil)
    }

    // MARK: - Table view data source

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 25
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
        cell.contentView.backgroundColor = UIColor.green
        cell.backgroundColor = UIColor.yellow
        cell.textLabel?.text = "This is row \(indexPath.row)"
        cell.textLabel?.backgroundColor = UIColor.clear

        return cell
    }
}

你找到解决方案了吗?不过我正在寻找相反的效果,即当tableView完全在屏幕上时,保持原始的安全区域。 - zh.
不好意思,我已经提交了一个苹果漏洞报告,一年过去了,但苹果还没有做出任何评论。 - Ben
2
在iOS 13中也影响到了我。 - strangetimes
谢谢您发布这个问题,我已经尝试了大约三天来解决类似的问题。这确实是我在iOS上遇到的最令人困惑和愚蠢的错误。再次感谢。 - amleszk
3个回答

6

有点相关的是,我想让一个独立的视图控制器始终具有与另一个视图相同的插入量。在我的情况下,它始终是全屏的,所以我只使用了窗口,但这种方法也适用于任何其他视图。

private final class InsetView: UIView {
    override var safeAreaInsets: UIEdgeInsets {
        window?.safeAreaInsets ?? .zero
    }
}

final class MyViewController: UIViewController {
    override func loadView() {
        view = InsetView()
    }
}

这对我来说完美地运作。最初,我尝试在safeAreaInsetsDidChange中通过使附加的区域相应为视图控制器应该具有的区域和实际区域之间的差异来计算additionalSafeAreaInsets,但在滚动视图中这真的很抖动。


类似的问题已经使用这个方法解决了。 - the monk
不错的修复 - 在iOS 15上也能工作。还可以在Storyboard中使用,用这个自定义的InsetView替换VC的标准视图。记得将其公开并添加模块。 - Will Alexander

5

我已经通过一个丑陋的hack来“解决”这个问题。在父级视图控制器中,您需要执行以下操作:

- (void) viewSafeAreaInsetsDidChange {
  [super viewSafeAreaInsetsDidChange];

  // Fix for child controllers not receiving an update on safe area insets
  // when they're partially not showing
  for (UIViewController *childController in self.childViewControllers) {
    UIEdgeInsets prevInsets = childController.additionalSafeAreaInsets;
    childController.additionalSafeAreaInsets = self.view.safeAreaInsets;
    childController.additionalSafeAreaInsets = prevInsets;
  }
}

这会强制子视图控制器正确更新它们的safeAreaInsets。


1
虽然我也不是很喜欢这段代码,但它确实解决了我的问题。谢谢! - Matthew Knippen
很遗憾,对我来说无法在iOS 15上构建。 - Will Alexander

-1

这似乎是一种系统行为:如果子视图控制器的视图在垂直和/或水平方向上未完全位于其父视图控制器的边界内,则其安全区域的相应方向不会更新。

听起来,如果您希望子视图控制器超出其父视图控制器的边界,则应使用transform属性,但安全区域仍然无法完全正确地工作,但它将在旋转时更新。


这样做行不通 - 问题在于安全区域插图没有为子视图控制器更新,这意味着子视图控制器内的子子子视图(在我的情况下是UITableViewCells)没有更新其安全区域插图,因此它们在屏幕上显示不正确。 - strangetimes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接