在现有的UIKit应用程序中包含SwiftUI视图


81

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

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

也就是说,我希望在同一应用程序中使用SwiftUI构建一些视图,并使用UIKit构建一些其他视图。当然不要混合两者。

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

彼此一起工作

Answers:


87

编辑05/06/19:添加了有关@@ partamento B在其答案中建议的UIHostingController的信息。归功于他!


在UIKit中使用SwiftUI

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

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

也可以根据需要改写UIHostingController和自定义它,例如,preferredStatusBarStyle如果无法SwiftUI按预期使用,则通过手动设置。

UIHostingController记录在这里


在SwiftUI中使用UIKit

如果UIKit应该在SwiftUI环境中使用现有视图,则可以使用该UIViewRepresentable协议!它已在此处记录,可以在Apple官方教程中实际使用。


兼容性

请注意,您无法SwiftUI在<iOS 13以下的iOS版本上使用,SwiftUI仅在iOS 13及更高版本上可用。有关更多信息,请参见此帖子。

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


6
UIViewRepresentable似乎相反,将aUIView添加到SwiftUI层次结构中
Departamento B

@DepartamentoB谢谢!您是对的,我会做相应的编辑
fredpi

@fredpi,有没有一种方法可以使用NavigationLink推送UIViewController?还是一旦开始在应用情节提要中使用SwiftUI视图,就必须继续使用此框架吗?
费尔南多(Fernando)

2
使用SwiftUI时,可以定位iOS 12及更低版本。当然,SwiftUI将仅由iOS 13使用,对于较低的iOS版本,您将需要创建UIKit视图(重复工作...)。但是对于那些正在迁移的人,可能会有所帮助。此处的更多信息:stackoverflow.com/a/58372597/840742
Renatus

24

UIHostingController

尽管目前尚未编写该类的文档,但UIHostingController<Content>似乎正是您所需要的:https : //developer.apple.com/documentation/swiftui/uihostingcontroller

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

let vc = UIHostingController(rootView: BenefitsSwiftUIView())

其中BenefitsSwiftUIView仅仅是默认的“Hello World”ViewSwiftUI。这完全符合您的预期。如果您继承了子类,它也可以工作UIHostingController


2
您无需明确指定通用参数,就可以推断出它;)
fredpi

是的,我刚刚意识到
Departamento B,

有一个对应的NSHostingController,现在-11b2-不起作用。如果要在基于情节提要的应用程序中使用SwiftUI,则需要关闭沙箱并使用NSHostingView,可以将其设置为窗口的contentView。(是的,我使用的是与UIHostingController相同的代码。iOS可以工作,macOS则不行。)
green_knight

@DepartamentoB我不能使用UIHostingController的子类。你知道怎么用吗?
sarunw

@sarunw不需要。如果您继承了子类,它也可以工作,但我给出的示例却没有。
Departamento B,

22

如果要将SwiftUI嵌入到UIKit视图控制器中,请使用容器视图。

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

参考


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对象)



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


2

如果您面临布局问题,则必须在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
    }
}

1

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

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

下面是从协议的代码片段ViewSwiftUI

///
/// 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+

1
这是相反的方式,UIView托管在视图中
Departamento B

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

0

这是我的方法:

创建一个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
    }
}

-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
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.