Вставка безопасной области Child ViewController не обновляется, если частично находится за пределами экрана

У меня возникают трудности с безопасной зоной при перемещении дочернего ViewController на iPhone X в альбомной ориентации.

У меня есть корневой ViewController, и одно из его представлений можно перемещать и содержит встроенный дочерний ViewController. Настоящее приложение представляет собой скользящее боковое меню. Пример кода здесь представляет собой урезанную версию.

Если таблица расположена в левой части экрана, правила макета безопасной зоны смещают ее вставку содержимого ячейки вправо, чтобы сделать выемку. Верный. Но затем, если я перемещаю дочерний ViewController от левого края экрана, дочерние вставки не обновляются для ретрансляции содержимого.

Я понял, что на самом деле все работает хорошо, если дочерний ViewController полностью находится на экране в своем конечном положении. Если какая-либо его часть находится за пределами экрана, обновления безопасной зоны не происходит.

Вот пример кода, чтобы показать проблему. Это работает со стандартным проектом шаблона приложения Single View Xcode: замените код файла ViewController показанным кодом. При запуске смахивание таблицы вправо перемещает ее от левого края к правому краю экрана.

См. строку «ограничение (..., множитель: 0,5)». Это устанавливает ширину подвижного представления относительно экрана. При 0,5 стол полностью помещается на экране, а безопасная зона обновляется по мере его перемещения. При закреплении слева ячейки таблицы соответствуют вставке безопасной зоны, а при закреплении справа ячейки таблицы не имеют дополнительной вставки, что правильно.

Как только множитель превышает 0,5, даже при 0,51, при скольжении правая часть таблицы оказывается за кадром. В этом случае обновление безопасной области не происходит, и поэтому вставка содержимого ячейки таблицы слишком велика — она по-прежнему имеет вставку безопасной области размером 44 пикселя, даже несмотря на то, что левый край таблицы теперь находится далеко от безопасной области.

Чтобы усложнить головоломку, макет, похоже, отлично работает с UIViews, если они не являются представлением UIViewController. Но мне нужно, чтобы он работал с UIViewControllers.

Может ли кто-нибудь объяснить, как заставить дочерний ViewController соблюдать правильную безопасную зону? Спасибо.

Скрины проблемы

Код для воспроизведения:

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

person Ben    schedule 23.11.2017    source источник
comment
Вы нашли решение? Однако я ищу противоположный эффект, чтобы сохранить исходный tableView safeArea, когда он полностью отображается на экране.   -  person zh.    schedule 19.11.2018
comment
Нет, извини @zh. Я сообщил об ошибке Apple, и год спустя она все еще остается открытой, и Apple не дает никаких комментариев.   -  person Ben    schedule 22.11.2018
comment
Влияет и на меня в iOS 13   -  person strangetimes    schedule 10.10.2019


Ответы (3)


Мне удалось «исправить» это с помощью уродливого взлома. В родительском контроллере представления вам нужно сделать это:

- (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.

person strangetimes    schedule 09.10.2019
comment
Тоже не большой поклонник кода, но это решило проблему для меня. Спасибо! - person Matthew Knippen; 07.12.2020

Наполовину связанный, я хотел, чтобы вложенный контроллер представления всегда имел те же вставки, что и другое представление. В моем случае это всегда был полноэкранный режим, поэтому я просто использовал окно, но этот подход будет работать и с любым другим видом.

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

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

Это работает безупречно для меня. Первоначально я пытался вычислить additionalSafeAreaInsets в safeAreaInsetsDidChange, заставив дополнительные быть разницей между тем, что должен был иметь контроллер представления, и тем, что было на самом деле, но это было действительно нервно в представлении прокрутки.

person Sam Soffes    schedule 24.10.2020

Похоже, это системное поведение: если представление дочернего контроллера представления не полностью находится в границах его родительского контроллера представления в вертикальном и/или горизонтальном направлении, соответствующие направления его безопасной области не обновляются.

Похоже, если вы хотите, чтобы дочерний контроллер представления выходил за пределы родительского контроллера представления, вы должны использовать transform, но safe все равно будет вести себя не совсем корректно, однако будет обновляться при ротации.

person elmon    schedule 25.04.2018
comment
Это не сработает - проблема в том, что вставки безопасной области не обновляются для контроллера дочернего представления, что означает, что представления sub-sub-sub в контроллере дочернего представления (UITableViewCells в моем случае) не обновляют свои вставки безопасной области. , поэтому они неправильно отображаются на экране. - person strangetimes; 10.10.2019