Reputation: 597
I'm trying to add a SwiftUI view to UIKit view using UIHostingController and it shows extra spacing(This sample is made to simulate an issue on a production app). Here is the screenshot.
Layout overview:
View
UIStackView
UIImageView
UIView(red)
UIHostingController
UIView(blue)
Issue: The swift UI view (UIHostingController) is shown between the red and blue views, it shows extra spacing after the divider. The spacing changes depending on the size of the SwiftUI view.
If I reduce the number of rows (Hello World texts) or reduce the spacing, it seems working fine.
Here is full source code(https://www.sendspace.com/file/ux0xt7):
ViewController.swift(main view)
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var mainStackView: UIStackView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
addView()
}
private func addView() {
mainStackView.spacing = 0
mainStackView.alignment = .fill
let imageView = UIImageView()
imageView.image = UIImage(named: "mountain")
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
imageView.heightAnchor.constraint(equalToConstant: 260).isActive = true
mainStackView.addArrangedSubview(imageView)
let redView = UIView()
redView.backgroundColor = .red
redView.heightAnchor.constraint(equalToConstant: 100).isActive = true
mainStackView.addArrangedSubview(redView)
let sampleVC = SampleViewController()
//let size = sampleVC.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
//sampleVC.view.heightAnchor.constraint(equalToConstant: size.height).isActive = true
mainStackView.addArrangedSubview(sampleVC.view)
let blueView = UIView()
blueView.backgroundColor = .blue
blueView.heightAnchor.constraint(equalToConstant: 100).isActive = true
mainStackView.addArrangedSubview(blueView)
}
}
SampleView.swift
import SwiftUI
struct SampleView: View {
var body: some View {
VStack(spacing: 0) {
Text("Title")
VStack (alignment: .leading, spacing: 30) {
Text("Hello World1")
Text("Hello World2")
Text("Hello World3")
Text("Hello World4")
Text("Hello World5")
Text("Hello World6")
Text("Hello World7")
Text("Hello World8")
Text("Hello World9")
Text("Hello World10")
}
Divider()
}
}
}
struct SampleView_Previews: PreviewProvider {
static var previews: some View {
Group {
SampleView()
}
}
}
SampleViewController.swift
import UIKit
import SwiftUI
class SampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addView()
}
private func addView() {
let hostingController = UIHostingController(rootView: SampleView())
hostingController.view.backgroundColor = .clear
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0),
hostingController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
}
}
Thanks in advance!
Upvotes: 21
Views: 8763
Reputation: 1398
Starting iOS 16.4, you can give a try to safeAreaRegions:
hostingController.safeAreaRegions = []
Upvotes: 1
Reputation: 5116
Your mileage may vary, but try setting .insetsLayoutMarginsFromSafeArea = false
on the UIView
and UIHostingController
views.
Upvotes: 0
Reputation: 41
Maybe you can try like this.
import SwiftUI
// Ignore safearea for UIHostingController when wrapped in UICollectionViewCell
class SafeAreaIgnoredHostingController<Content: View>: UIHostingController<Content> {
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
// Adjust only when top safeAreaInset is not equal to bottom
guard view.safeAreaInsets.top != view.safeAreaInsets.bottom else {
return
}
// Set additionalSafeAreaInsets to .zero before adjust to prevent accumulation
guard additionalSafeAreaInsets == .zero else {
additionalSafeAreaInsets = .zero
return
}
// Use gap between top and bottom safeAreaInset to adjust top inset
additionalSafeAreaInsets.top = view.safeAreaInsets.bottom - view.safeAreaInsets.top
}
}
Upvotes: 1
Reputation: 181
While the solution from Alexander: https://stackoverflow.com/a/70339424/3390353 worked for me. Swizzling/ changing things on runtime always makes me a little nervous to do in a production environment.
So I went with an approach of subClass of UIHostingController and when the bottom safe area inset is changed I can use the additionalSafeAreaInsets to "add" the negative of the current bottom SafeArea Inset. With a check to only do this if safeAreaInsets.bottom > 0
.
class OverrideSafeAreaBottomInsetHostingController<ContentView: SwiftUI.View>: UIHostingController<ContentView> {
override func viewSafeAreaInsetsDidChange() {
super.viewSafeAreaInsetsDidChange()
if view.safeAreaInsets.bottom > 0 {
additionalSafeAreaInsets = .init(top: 0, left: 0, bottom: -view.safeAreaInsets.bottom, right: 0)
}
}
}
Upvotes: 11
Reputation: 87
Swift 5.5 Solution
Try
UIHostingController(rootView: SampleView().edgesIgnoringSafeArea(.all))
Upvotes: 5
Reputation: 1873
I had a similar problem, and this is, like @Asperi mentions, due to extra safe area insets applied to the SwiftUI view.
It sadly does not work to simply add edgesIgnoringSafeArea()
to the SwiftUI view.
Instead, you can fix this with the following UIHostingController
extension:
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)
}
}
}
And use it like this:
let hostingController = UIHostingController(rootView: SampleView(), ignoreSafeArea: true)
Solution credits: https://defagos.github.io/swiftui_collection_part3/#fixing-cell-frames
Upvotes: 21
Reputation: 344
Let the hosting controller's view update its layout once again:
class SampleViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addView()
}
private func addView() {
let hostingController = UIHostingController(rootView: SampleView())
hostingController.view.backgroundColor = .clear
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
addChild(hostingController)
view.addSubview(hostingController.view)
hostingController.didMove(toParent: self)
NSLayoutConstraint.activate([
hostingController.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
hostingController.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
hostingController.view.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 0),
hostingController.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)
])
hostingController.view.layoutIfNeeded()
}
}
Upvotes: 1
Reputation: 1
Instead having UIView(red)
you can simply use Color.red
same for UIView(blue)
:
struct SampleView: View {
var body: some View {
VStack(spacing: .zero) {
Color.red
Text("Title")
VStack (alignment: .leading, spacing: 30) {
Text("Hello World1")
Text("Hello World2")
Text("Hello World3")
Text("Hello World4")
Text("Hello World5")
Text("Hello World6")
Text("Hello World7")
Text("Hello World8")
Text("Hello World9")
Text("Hello World10")
}
Divider()
Color.blue.opacity(0.1)
}
}
}
Result:
Upvotes: -2