UITableViewHeaderFooterView with SwiftUI content getting automatic safe area inset
Asked Answered
P

5

6

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

Pilatus answered 1/5, 2020 at 23:40 Comment(3)
I believe this is a SwiftUI bug. I’ve reported it for now and re-written the layout in UIKit as it seems unavoidable for now.Pilatus
Frustratingly there is a var disableSafeArea: Bool on UIHostingView, but since it's a private class we can't set it :( I assume it's what they are using in the implementation of List etc.Wolfe
openradar.appspot.com/radar?id=4929616280027136Wolfe
O
2

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
    }
}
Ouphe answered 17/6, 2020 at 3:39 Comment(3)
Thanks! This looks like the only solution, but is incredibly hacky. I don't personally like swizzling methods. Would also be good to log this as a bug with Apple (I already have) so the more the votes, the sooner they fix this.Pilatus
Beware that this will apply to ALL swiftUI hosting controllers. Does the trick if you are only using SwiftUI for components in a UIKit appWolfe
I've tried this and it 100% causes other issues with unrelated UIHostingControllers. I cannot recommendFeingold
H
0

I was struggling with similar issue with UICollectionView and fixed the following custom UIHostingController that has viewSafeAreaInsetsDidChange override method.

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

I found that the content view moved upwards as much as the safe area's bottom inset so tried to adjust the top inset with that value. and ended up getting the result I expected. the top inset made the view move back to expected place.

Unfortunately, I can't give you any clear explaination of how it works under the hood because of lack of my knowledge about that. It's just the result derived from a lot of experiments I had tried. hope it's useful for anyone struggling with it.

Hopkins answered 11/1, 2021 at 2:58 Comment(0)
R
0

When I do not add UIHostingController to a parent view controller, this problem disappears. I'm not saying this is the correct solution, but it might be better than the swizzling solution.

Redheaded answered 1/3, 2021 at 18:24 Comment(0)
F
0

I'm still in the process of testing this but so far it seems to be working. Instead of doing method swizzling I set explicit dimension constraints on a UIHostingController subclass (in my case only height was needed).

public final class UIHostingControllerWithoutSafeArea<T>: UIHostingController<T> where T: View {
    
    var height: NSLayoutConstraint?
    
    public override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
    
        if let height = height {
            height.constant = view.bounds.height
        } else {
            height = view.heightAnchor.constraint(equalToConstant: view.bounds.height)
            height?.activate()
        }

        view.setNeedsLayout()
        view.layoutIfNeeded()
    }
    

}

PS: If you really prefer the method swizzling approach I would recommend this version instead.

Feingold answered 4/10, 2022 at 19:59 Comment(0)
H
-1

I think this post may be related to this one. As I wrote there I found a fix by adding the following to my UIHostingController subclass:

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

Hopefully this helps others as well.

Heroism answered 24/3, 2022 at 17:35 Comment(1)
this does not solve the issueGenital

© 2022 - 2024 — McMap. All rights reserved.