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

问题描述 投票:0回答:8

我正在尝试使用 UIHostingController 将 SwiftUI 视图添加到 UIKit 视图,它显示了额外的间距(此示例是为了模拟生产应用程序上的问题而制作的)。这是截图。

enter image description here

布局概览:

View
  UIStackView
    UIImageView
    UIView(red)
    UIHostingController
    UIView(blue)

问题: 快速 UI 视图 (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)
    }
}

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

提前致谢!

swift swiftui autolayout ios15 uihostingcontroller
8个回答
21
投票

我也遇到了类似的问题,就像 @Asperi mentions 一样,由于 SwiftUI 视图应用了额外的安全区域插入。

遗憾的是,仅将

edgesIgnoringSafeArea()
添加到 SwiftUI 视图是行不通的。

相反,您可以使用以下

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


11
投票

虽然亚历山大的解决方案:https://stackoverflow.com/a/70339424/3390353对我有用。在运行时调整/更改内容总是让我在生产环境中感到有点紧张。

因此,我采用了 UIHostingController 子类的方法,当底部安全区域插入更改时,我可以使用 extraSafeAreaInsets 来“添加”当前底部 SafeArea Inset 的负值。通过检查仅在

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

5
投票

Swift 5.5 解决方案

尝试

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

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

}

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
投票

您的里程可能会有所不同,但请尝试在

.insetsLayoutMarginsFromSafeArea = false
UIView
 视图上设置 
UIHostingController


0
投票

从 iOS 16.4 开始,您可以尝试一下 safeAreaRegions:

hostingController.safeAreaRegions = []

-2
投票

您可以简单地使用

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

结果:

© www.soinside.com 2019 - 2024. All rights reserved.