125

是否可以使用 SwiftUI 与现有的 UIKit 应用程序并排构建视图?

我有一个用 Objective-C 编写的现有应用程序。我已经开始迁移到 Swift 5。我想知道是否可以将 SwiftUI 与现有的 UIKit .xib 视图一起使用。

也就是说,我想要在同一个应用程序中使用 SwiftUI 构建的一些视图和使用 UIKit 构建的其他一些视图。当然不是把两者混在一起。

SomeObjCSwiftProject/
    SwiftUIViewController.swift
    SwiftUIView.xib
    UIKitViewController.swift
    UIKitView.xib

并肩工作

4

12 回答 12

133

编辑 19 年 5 月 6 日:添加了 @Departamento B 在他的回答中建议的有关 UIHostingController 的信息。功劳归他!


在 UIKit 中使用 SwiftUI

可以通过将 a 包装成这样的方式SwiftUI在现有环境中使用组件:UIKitSwiftUI ViewUIHostingController

let swiftUIView = SomeSwiftUIView() // swiftUIView is View
let viewCtrl = UIHostingController(rootView: swiftUIView)

也可以UIHostingController根据自己的需要覆盖和定制它,例如,preferredStatusBarStyle如果它不能SwiftUI按预期工作,则通过手动设置。

UIHostingController记录在这里


在 SwiftUI 中使用 UIKit

UIKit如果应该在SwiftUI环境中使用现有视图,UIViewRepresentable协议可以提供帮助!它被记录在这里,并且可以在这个Apple 官方教程中看到。


兼容性

请注意,您不能SwiftUI在 iOS 版本 < iOS 13 上使用,因为SwiftUI仅适用于 iOS 13 及更高版本。有关更多信息,请参阅帖子。

如果要在目标低于 iOS 13 的项目中使用,则需要使用属性SwiftUI标记SwiftUI结构。@available(iOS 13.0.0, *)

于 2019-06-03T20:09:52.690 回答
42

如果您想将 SwiftUI 嵌入到 UIKit 视图控制器中,请使用 Container View。

class ViewController: UIViewController {
    @IBOutlet weak var theContainer: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let childView = UIHostingController(rootView: SwiftUIView())
        addChild(childView)
        childView.view.frame = theContainer.bounds
        theContainer.addSubview(childView.view)
        childView.didMove(toParent: self)
    }
}

参考

于 2019-11-24T05:15:46.913 回答
26

UIHostingController

虽然目前该课程的文档尚未编写,但UIHostingController<Content>似乎是您正在寻找的内容:https ://developer.apple.com/documentation/swiftui/uihostingcontroller

我刚刚在我的应用程序中使用以下代码行进行了尝试:

let vc = UIHostingController(rootView: BenefitsSwiftUIView())

来自. BenefitsSwiftUIView_ View_ SwiftUI这完全符合您的预期。如果您将UIHostingController.

于 2019-06-05T10:30:35.560 回答
8

我还没有看到提到的一项,涉及 Xcode 11 beta 5 (11M382q) 涉及更新您的应用程序的 info.plist 文件。

对于我的场景,我正在采用现有的基于 Swift 和 UIKit 的应用程序并将其完全迁移为 iOS 13 和纯 SwiftUI 应用程序,因此向后兼容性对我来说不是问题。

对 AppDelegate 进行必要的更改后:

// MARK: UISceneSession Lifecycle
func application(_ application: UIApplication,
                 configurationForConnecting connectingSceneSession: UISceneSession,
                 options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default Configuration",
                                sessionRole: connectingSceneSession.role)
}

并添加一个 SceneDelegate 类:

import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: HomeList())
            self.window = window
            window.makeKeyAndVisible()
        }
    }
}

我遇到了一个问题,我的 SceneDelegate 没有被调用。通过将以下内容添加到我的 info.plist 文件中已解决此问题:

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <false/>
    <key>UISceneConfigurations</key>
    <dict>
        <key>UIWindowSceneSessionRoleApplication</key>
        <array>
            <dict>
                <key>UISceneClassName</key>
                <string></string>
                <key>UISceneDelegateClassName</key>
                <string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
                <key>UISceneConfigurationName</key>
                <string>Default Configuration</string>
                <key>UISceneStoryboardFile</key>
                <string>LaunchScreen</string>
            </dict>
        </array>
    </dict>
</dict>

还有一个截图可以看到: 在此处输入图像描述

要保持同步的主要项目是:

  • 委托类名,以便 Xcode 知道在哪里可以找到您的SceneDelegate文件
  • 配置名称,以便 AppDelegate 中的调用可以加载正确的UISceneConfiguration

这样做之后,我就可以加载我新创建的 HomeList 视图(一个 SwiftUI 对象)

于 2019-08-05T15:22:42.647 回答
4

如果您希望SwiftUI从遗留的 Objective C 项目中创建视图,那么这种技术对我来说非常有效,

请参阅将 SwiftUI 添加到 Objective-C 应用程序

感谢我们写这篇文章的朋友。

于 2019-12-02T00:28:30.540 回答
4

如果您遇到布局问题,则必须向 UIHostingController 的视图添加约束,

class ViewController: UIViewController {
    @IBOutlet weak var theContainer: UIView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let childView = UIHostingController(rootView: SwiftUIView())
        addChild(childView)
        childView.view.frame = theContainer.bounds
        theContainer.addConstrained(subview: childView.view)
        childView.didMove(toParent: self)
    }
}

使用这个扩展:

extension UIView {
    func addConstrained(subview: UIView) {
        addSubview(subview)
        subview.translatesAutoresizingMaskIntoConstraints = false
        subview.topAnchor.constraint(equalTo: topAnchor).isActive = true
        subview.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
        subview.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
        subview.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
    }
}
于 2020-07-27T03:59:04.100 回答
4

这是我的做法:

创建一个 SwiftUI 适配器

/**
 * Adapts a SwiftUI view for use inside a UIViewController.
 */
class SwiftUIAdapter<Content> where Content : View {

    private(set) var view: Content!
    weak private(set) var parent: UIViewController!
    private(set) var uiView : WrappedView
    private var hostingController: UIHostingController<Content>

    init(view: Content, parent: UIViewController) {
        self.view = view
        self.parent = parent
        hostingController = UIHostingController(rootView: view)
        parent.addChild(hostingController)
        hostingController.didMove(toParent: parent)
        uiView = WrappedView(view: hostingController.view)
    }

    deinit {
        hostingController.removeFromParent()
        hostingController.didMove(toParent: nil)
    }

}

添加到视图控制器如下:

class FeedViewController: UIViewController {
    
    var adapter : SwiftUIAdapter<FeedView>!

    override required init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
        adapter = SwiftUIAdapter(view: FeedView(), parent: self)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    /** Override load view to load the SwiftUI adapted view */
    override func loadView() {
        view = adapter.uiView;
    }
}

这是包裹视图的代码

对于这种非常简单的情况,包裹视图使用手动布局(layoutSubViews)而不是自动布局。

class WrappedView: UIView {

    private (set) var view: UIView!

    init(view: UIView) {
        self.view = view
        super.init(frame: CGRect.zero)
        addSubview(view)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        view.frame = bounds
    }
}
于 2020-12-15T23:14:09.170 回答
2

与故事板

您可以HotingViewController在界面构建器中使用组件:

预习

然后,如果你有一个HotingController这样的简单:

class MySwiftUIHostingController: UIHostingController<Text> {
    required init?(coder: NSCoder) {
        super.init(coder: coder, rootView: Text("Hello World"))
    }
}

您可以将其设置为控制器的自定义类:

预览 2


有代码

let mySwiftUIHostingController = UIHostingController(rootView: Text("Hello World"))

然后你可以像平常一样使用它UIViewController


重要的提示

不要忘记SwiftUI在需要的地方导入UIHostingController

于 2020-07-07T09:59:37.637 回答
1

你可以一起使用它们。您可以通过一致性“转移”UIView到。详细可以在官方教程中找到。ViewUIViewRepresentable

但是,您还应该考虑它的兼容性。

以下是SwiftUIView协议的代码片段:

///
/// You create custom views by declaring types that conform to the `View`
/// protocol. Implement the required `body` property to provide the content
/// and behavior for your custom view.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View : _View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    /// ...
}

所以它不向后兼容。

  • iOS 13.0+
  • macOS 10.15+
  • watchOS 6.0+
于 2019-06-04T02:47:51.127 回答
1
import Foundation
    #if canImport(SwiftUI)
    import SwiftUI

internal final class SomeRouter {
    fileprivate weak var presentingViewController: UIViewController!

    function navigateToSwiftUIView() {
       if #available(iOS 13, *) {
            let hostingController = UIHostingController(rootView: contentView())  
presentingViewController?.navigationController?.pushViewController(hostingController, animated: true)
            return
        }
        //Keep the old way when not 13.
}
#endif
于 2019-06-21T16:12:46.847 回答
1

您只需按照以下步骤即可实现此目的...

  1. 在 mainStoryBoard 上显示的 viewController 中创建一个按钮并导入 SwiftUI

  2. 在 mainStoryboard 中添加一个 hostingViewController 并将 Segue(显示 Segue)从按钮拖动到 hostingViewController。将 segue 从 storyBoard 拖到 SwiftUI

  3. 通过单击 Cmd+N 在项目中添加 SwiftUI 文件 创建 SwiftUI 文件

  4. 将 mainStoryBoard 中呈现的 segueAction 表单 segue 添加到 viewController。

  5. 在 segueAction 中编写以下代码行...

@IBSegueAction func ActionMe(_ coder: NSCoder) -> UIViewController? {
    return UIHostingController(coder: coder, rootView: SWIFTUI())
}
于 2021-05-13T10:12:52.230 回答
-3

其他人一直在展示如何使用UIHostingController

我可以展示如何从 SwiftUI UIViewControllerRepresentable 呈现UIViewController

struct YourViewControllerWrapper: UIViewControllerRepresentable {
    typealias UIViewControllerType = YourViewController

    func makeUIViewController(context: UIViewControllerRepresentableContext<YourViewControllerWrapper>) -> YourViewController {
        let storyBoard = UIStoryboard(name: "YourStoryboard", bundle: Bundle.main)
        return storyBoard.instantiateViewController(withIdentifier: "YourViewController") as! YourViewController
    }

    func updateUIViewController(_ uiViewController: YourViewController, context: UIViewControllerRepresentableContext<YourViewController>) {
        // do nothing
    }
}
于 2019-07-31T19:46:37.757 回答