swiftuitableviewswiftuiuitableviewsectionheader

UITableViewHeaderFooterView with SwiftUI content getting automatic safe area inset


I've got a basic UITableView with some cells. I'm using a SwiftUI View as content for both my cells and section headers. Strangely, only the section header that appears to touch the bottom of the screen on an iPhone XS Max seems to get a rawSafeAreaInset of 16pts (checked Debug View Hierarchy). My cells are working as expected.

To see what's going one, I have added a dummy blue SwiftUI rectangle to the contentView, and then placed a red UIView on top, both views set to the same constraints. The UITableView has been set to use automatic dimensions for headers and cells.

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),
    ])
  }
}

As you can see in the image below, the red overlay is visible for the top two headers (with empty cells for demonstration), but the last one on the screen has its blue SwiftUI rectangle shifted upwards!

enter image description here

This SwiftUI view seems to be getting some safeAreaInset somehow, and there seems to be no way to turn this off. The inset also does not go away if you scroll up. It stays there forever. I tried turning off safe area insets for the SwiftUI view, but that doesn't help either:

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

How do I get rid of this inset? As I mentioned - it's only happening to UITableViewHeaderFooterViews and not UITableViewCells.

The debug view hierarchy reveals a bogus bottom padding modifier based on the safe area insets:

enter image description here


Solution

  • Hosting the view in this subclassed UIHostingController did the trick for me!

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