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!
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 UITableViewHeaderFooterView
s and not UITableViewCell
s.
The debug view hierarchy reveals a bogus bottom padding modifier based on the safe area insets:
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
}
}