为什么SwiftUI的UIHostingController会有额外的间距?

18

我正在尝试使用UIHostingController将SwiftUI视图添加到UIKit视图中,但会显示额外的间距(此示例是为了模拟生产应用中的问题)。以下是截图。

enter image description here

布局概述:

View
  UIStackView
    UIImageView
    UIView(red)
    UIHostingController
    UIView(blue)
问题:SwiftUI视图(UIHostingController)显示在红色和蓝色视图之间,在分隔线后显示额外的间距。该间距取决于SwiftUI视图的大小。

如果我减少行数(Hello World文本)或减小间距,它似乎可以正常工作。

这里是全源代码(https://www.sendspace.com/file/ux0xt7):

ViewController.swift(主视图)

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

样例视图.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()
        }
    }
}

示例视图控制器.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)
        ])
    }
}

提前致谢!


看起来像是一个 bug - 托管视图认为它在根目录中,并将 edgeAreaInsets 应用于内部的 SwiftUI 内容。 - Asperi
7个回答

20

我曾遇到类似问题,像@Asperi一样提到,这是由于在SwiftUI视图中应用了额外的安全区域插图。

简单地向SwiftUI视图添加edgesIgnoringSafeArea()无法解决该问题。

相反,您可以使用以下UIHostingController扩展来修复此问题:

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

使用方法如下:

let hostingController = UIHostingController(rootView: SampleView(), ignoreSafeArea: true)

解决方案来源:https://defagos.github.io/swiftui_collection_part3/#fixing-cell-frames

这很深奥,它能通过App Store审核吗?经过几天的尝试和研究,我发现这是在SwiftUI中快照小视图的唯一方法。这里建议的方法适用于较大的视图,但会在较小的视图上方留下31像素的白色空间。 - pommy
2
@pommy 我们正在生产中使用这个hack,目前为止还没有遇到任何与App Store审核相关的问题。 - Alexander Sandberg
1
感谢您提供的解决方案!非常有帮助。起初让我困惑的是,有时候这个偏移量被应用了,但有时候却没有。但似乎与 UITableView/UICollectionView 有关,有时候当 SwiftUI 的 UIHostingController 添加到层次结构中时,它恰好处于安全区域的边界上。相关 Radar - Hendrix
除非你还覆盖keyboardWillShowWithNotification:方法,否则此功能无法正常工作。请参阅此代码片段https://gist.github.com/steipete/da72299613dcc91e8d729e48b4bb582c - bze12

9
虽然来自 Alexander 的解决方案:https://dev59.com/XlEG5IYBdhLWcg3wOnSy#70339424 对我很有用。但在生产环境中,运行时的 swizzling / 更改始终让我感到有些紧张。
因此,我选择了 UIHostingController 的子类方法,在底部安全区域插图发生变化时,可以使用 additionalSafeAreaInsets 来“添加”当前底部安全区域插图的负数。检查 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)
        }
    }
}

3

Swift 5.5 解决方案

尝试

UIHostingController(rootView: SampleView().edgesIgnoringSafeArea(.all))

这对我在Xcode 15.0上,部署目标为iOS 16.0的情况下也起作用。 - undefined

1

让托管控制器的视图再次更新其布局:

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()
    }

}

它不起作用。 - Daniel
奇怪。你不能只是在 VStack 底部添加一个填充而不是使用 Divider 吗?或者最终尝试将其包装在 AnyView 中,然后在托管控制器中使用它:let hostingController = UIHostingController(rootView: AnyView(SampleView())) - valeCocoa
添加分隔符是为了显示由于错误而产生的额外间距。 - Daniel
@Danny,你尝试过使用AnyView包装吗? - valeCocoa
是的,它不起作用。 - Daniel

1
也许你可以尝试这样做。
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
    }
}

0
你的里程可能会有所不同,但是尝试在UIViewUIHostingController视图上设置.insetsLayoutMarginsFromSafeArea = false

-1

不要使用 UIView(red),你可以直接使用 Color.red,同样的,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)
        }
 
    }
}

结果:

enter image description here


这是一个简单的演示应用程序,模拟了真实应用程序中的问题。如果您将Color.red或blue视图替换为其他视图,如图像,则可以轻松看到布局会破坏。 - Daniel
我相信任何你想要的东西都可以用红色和蓝色替换,而不会有任何问题!也许你使用的方法不对!@Danny - ios coder
唯一的Swift UI视图是带有白色背景的中间视图。上面的红色和蓝色视图是我示例应用程序中的UIKit视图。当Swift UI视图与UIScrollView内的UIKit视图混合使用时会出现这种情况。 - Daniel
如果您只使用少量视图,则不会发生这种情况。 - Daniel
没问题,这是你的设计和计划,你说不使用某个可能是最好的方式,或者使用正确的方式来使用它。 - ios coder
我正在寻找一种解决方法,无论我在UIKit视图中托管哪些SwiftUI视图,都不会破坏布局。目前,这高度依赖于SwiftUI视图。 - Daniel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接