带有 SwiftUI 内容的 UITableViewHeaderFooterView 获得自动安全区域插入

UITableViewHeaderFooterView with SwiftUI content getting automatic safe area inset

我有一些基本的 UITableView 细胞。我使用 SwiftUI View 作为单元格和 header 部分的内容。奇怪的是,在 iPhone XS Max 上只有似乎触及屏幕底部的部分 header 似乎获得了 16pts 的 rawSafeAreaInset(已检查 Debug View Hierarchy)。我的细胞按预期工作。

为了看看发生了什么,我在 contentView 中添加了一个虚拟的蓝色 SwiftUI 矩形,然后在顶部放置了一个红色的 UIView,两个视图都设置了相同的约束。 UITableView 已设置为对 header 和单元格使用自动尺寸。

public class SectionHeader: UITableViewHeaderFooterView {
  public static let reusableIdentifier = "Section"

  private var innerHostedViewController: UIHostingController<AnyView>!

  public override init(reuseIdentifier: String?) {
    super.init(reuseIdentifier: reuseIdentifier)

    setupHeader()
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  private func setupHeader() {
    self.backgroundView = UIView()
    self.backgroundView?.backgroundColor = UIColor.green.withAlphaComponent(0.2)

    innerHostedViewController = UIHostingController(rootView: AnyView(Rectangle().fill(Color.blue).frame(height: 48)))
    innerHostedViewController.view.translatesAutoresizingMaskIntoConstraints = false
    innerHostedViewController.view.frame = self.contentView.bounds
    contentView.addSubview(innerHostedViewController.view)
    innerHostedViewController.view.backgroundColor = .clear

    let vv = UIView()
    vv.translatesAutoresizingMaskIntoConstraints = false
    vv.backgroundColor = .red
    contentView.addSubview(vv)

    NSLayoutConstraint.activate([
      vv.topAnchor.constraint(equalTo: self.contentView.topAnchor),
      vv.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
      vv.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
      vv.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),

      innerHostedViewController.view.topAnchor.constraint(equalTo: self.contentView.topAnchor),
      innerHostedViewController.view.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
      innerHostedViewController.view.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
      innerHostedViewController.view.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
    ])
  }
}

如下图所示,前两个 header 可以看到红色叠加层(带有用于演示的空单元格),但屏幕上最后一个的蓝色 SwiftUI 矩形已移动向上!

这个 SwiftUI 视图似乎以某种方式获得了一些 safeAreaInset,而且似乎没有办法关闭它。如果向上滚动,插图也不会消失。它永远留在那里。我尝试为 SwiftUI 视图关闭安全区域插入,但这也无济于事:

innerHostedViewController = UIHostingController(rootView: AnyView(Rectangle().fill(Color.blue).frame(height: 48).edgesIgnoringSafeArea(.all)))

如何去掉这个插图?正如我提到的 - 它只发生在 UITableViewHeaderFooterViews 而不是 UITableViewCells.

调试视图层次结构揭示了一个基于安全区域插入的虚假底部填充修饰符:

在这个子类中托管视图 UIHostingController 对我有用!

/// https://twitter.com/b3ll/status/1193747288302075906
class FixSafeAreaInsetsHostingViewController<Content: SwiftUI.View>: UIHostingController<Content> {
    func fixApplied() -> Self {
        self.fixSafeAreaInsets()
        return self
    }

    func fixSafeAreaInsets() {
        guard let _class = view?.classForCoder else {
            fatalError()
        }

        let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { (sself: AnyObject!) -> UIEdgeInsets in
            return .zero
        }
        guard let method = class_getInstanceMethod(_class.self, #selector(getter: UIView.safeAreaInsets)) else { return }
        class_replaceMethod(_class, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))

        let safeAreaLayoutGuide: @convention(block) (AnyObject) -> UILayoutGuide? = { (sself : AnyObject!) -> UILayoutGuide? in return nil }

        guard let method2 = class_getInstanceMethod(_class.self, #selector(getter: UIView.safeAreaLayoutGuide)) else { return }
        class_replaceMethod(_class, #selector(getter: UIView.safeAreaLayoutGuide), imp_implementationWithBlock(safeAreaLayoutGuide), method_getTypeEncoding(method2))
    }

    override var prefersStatusBarHidden: Bool {
        return false
    }
}

我一直在努力解决 UICollectionView 的类似问题,并修复了以下具有 viewSafeAreaInsetsDidChange 覆盖方法的自定义 UIHostingController。

class TopInsetCounterHostingController<ContentView: SwiftUI.View>: UIHostingController<ContentView> {
    override func viewSafeAreaInsetsDidChange() {
        additionalSafeAreaInsets = .init(top: view.safeAreaInsets.bottom, left: 0, bottom: 0, right: 0)
    }
}

我发现内容视图向上移动的幅度与安全区域的底部插图一样多,因此尝试使用该值调整顶部插图。并最终得到了我预期的结果。顶部插图使视图移回预期位置。

遗憾的是,由于缺乏相关知识,我无法向您清楚地解释它在幕后是如何工作的。这只是我尝试过的大量实验得出的结果。希望它对任何挣扎的人都有用。

当我没有将 UIHostingController 添加到父视图控制器时,这个问题就消失了。我并不是说这是正确的解决方案,但它可能比 swizzling 解决方案更好。

我认为这个post可能与这个one有关。正如我在那里写的那样,我通过将以下内容添加到我的 UIHostingController 子类中找到了修复方法:

override func viewDidLoad() {
    super.viewDidLoad()
    edgesForExtendedLayout = []
}

希望这对其他人也有帮助。