Cannot place SwiftUI view outside the SafeArea when embedded in UIHostingController
Asked Answered
O

7

19

I have a simple SwiftUI view that contains 3 text elements:

struct ImageDescriptionView: View {
    var title: String?
    var imageDescription: String?
    var copyright: String?

    var body: some View {
        VStack(alignment: .leading) {
        
            if let title = title {
                Text(title)
                    .fontWeight(.bold)
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
        
            if let imageDescription = imageDescription {
                Text(imageDescription)
                    .foregroundColor(.white)
                    .fontWeight(.medium)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
        
            if let copyright = copyright {
                Text(copyright)
                    .font(.body)
                    .foregroundColor(.white)
                    .frame(maxWidth: .infinity, alignment: .leading)
            }
        
        }
        .background(
            Color.blue
        )
    }
}

The SwiftUI View is embedded within a UIHostingController:

class ViewController: UIViewController {

    private var hostingController = UIHostingController(rootView: ImageDescriptionView(title: "25. November 2021", imageDescription: "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.", copyright: "Bild © Unknown"))

    override func viewDidLoad() {
        super.viewDidLoad()
        setUpHC()
    }

    private func setUpHC() {
        hostingController.view.backgroundColor = .red
    
        view.addSubview(hostingController.view)
        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            hostingController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        addChild(hostingController)
        hostingController.didMove(toParent: self)
    }

}

The result looks like this:

enter image description here

The UIHostingController is always bigger than the view. Also, it will always make the SwiftUI view respect the safe area (which in my case, I do not want)

The look I want:

(please don't comment the usability of the home indicator, that's not the case here)

enter image description here

What's the problem with UIHostingController? I tried setting .edgesIgnoreSafeArea(.all) on all Views within ImageDescriptionView, did not help.

Optimal answered 29/11, 2021 at 14:27 Comment(2)
I've seen this question pop up a few times this last week, and honestly it seems to be the one question that's not getting answers.Groovy
I just ran your code on Xcode 14 attached to an iPhone 13 pro simulator running both iOS 15.5 and iOS 16. Turns out they fixed it in the new OSOligarchy
O
0

As Alex mentioned in a comment to my question, it seems like Apple fixed it.

Xcode 14.3, iOS 16 Simulator

enter image description here

Optimal answered 24/5, 2023 at 9:12 Comment(0)
D
15

iOS 16.4 Update

There's now an official API in iOS 16.4 for the previously private API method:

viewController.safeAreaRegions = SafeAreaRegions()

The SafeAreaRegions() initialiser returns an empty collection. Default value of .safeAreaRegions is .all.

Backwards compatible answer

If you want to be backwards compatible and support iOS 13+, there is a workaround:

let controller = UIHostingController(rootView: buttonView)
let window = UIApplication.shared.windows.first(where: \.isKeyWindow)
let inverseSafeAreaInset = window?.safeAreaInsets.bottom ?? 0
controller.additionalSafeAreaInsets = UIEdgeInsets(top: 0, left: 0, bottom: -inverseSafeAreaInset, right: 0)

Note you can get the inverse offset from the parent view / VC in multiple ways. e.g. view.safeAreaInsets works once the view has been laid out, however the window approach works for all contexts.

Doubledecker answered 18/5, 2023 at 12:57 Comment(0)
M
14

On the UIHostingControllers property try the following

viewController._disableSafeArea = true

that should do the trick.

Myxomatosis answered 2/3, 2022 at 14:57 Comment(2)
Since it's a private API, anyone tested for a AppStore release?Cantwell
This works for me, did someone have the uploaded to the Appstore?Tam
P
3

I came across the same issue. You have to ignore the safe area at the SwiftUI view level.

var body: some View {
    VStack(alignment: .leading) {
        ...
    }
    .ignoresSafeArea(edges: .all) // ignore all safe area insets
}
Prisoner answered 22/7, 2022 at 14:28 Comment(1)
it does not give the same result as the solutions above. The part of the view that occupies the safearea becomes invisible/invisible while scrolling.Taproot
C
3

Got a discussion here, and the detail here

extension UIHostingController {
    convenience public init(rootView: Content, ignoreSafeArea: Bool) {
        self.init(rootView: rootView)
        
        if ignoreSafeArea {
            disableSafeArea()
        }
    }
    
    func disableSafeArea() {
        guard let viewClass = object_getClass(view) else { return }
        
        let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
        if let viewSubclass = NSClassFromString(viewSubclassName) {
            object_setClass(view, viewSubclass)
        }
        else {
            guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
            guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
            
            if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
                let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in
                    return .zero
                }
                class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
            }
            
            objc_registerClassPair(viewSubclass)
            object_setClass(view, viewSubclass)
        }
    }
}
Cobber answered 28/7, 2022 at 2:26 Comment(0)
S
1

Happened to me too. When I aligned my view with the frame it worked, but to make it work with autolayout I had to consider the height of the safe area to make it work with UIHostingController, even though I didn't have to do that with a standard view.

code:

hostingVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: view.safeAreaInsets.bottom).isActive = true
Sorn answered 4/10, 2022 at 11:20 Comment(0)
N
0

In my case, I had a circle view inside UIHostingController and its background automatically resized when it close to safe area. I could be able to avoid it by setting autoresizesSubviews to false.

UIHostingController.view.autoresizesSubview = false
Nudnik answered 22/3, 2023 at 7:40 Comment(0)
O
0

As Alex mentioned in a comment to my question, it seems like Apple fixed it.

Xcode 14.3, iOS 16 Simulator

enter image description here

Optimal answered 24/5, 2023 at 9:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.