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

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

我有一个用 Objective-C 编写的现有应用程序。我已经开始迁移到 Swift 5了。我想知道我是否可以在现有的 UIKit 旁边使用 SwiftUI。十字路口的景色。

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

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

并肩作战

76595 次浏览

编辑05/06/19: 按照@Departamento B 在他的回答中的建议,添加了关于 UIHostingController 的信息!


在 UIKit 中使用 SwiftUI

人们可以在现有的 UIKit环境中使用 SwiftUI组件,方法是将 SwiftUI View封装到 UIHostingController中,如下所示:

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

它也可以覆盖 UIHostingController和定制它到一个人的需要,例如,通过设置 preferredStatusBarStyle手动如果它不工作通过 SwiftUI预期。

UIHostingController被记录为 给你


在 SwiftUI 中使用 UIKit

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


兼容性

请注意,您不能在 iOS 版本 < iOS 13上使用 SwiftUI,因为 SwiftUI只能在 iOS 13及以上版本上使用。有关更多信息,请参见 这个文章。

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

你们可以一起使用。你可以通过 UIViewRepresentable一致性将 UIView“转移”到 View。详情可以在 正式教程中找到。

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

下面是 SwiftUI的 Protocol View的代码片段:

///
/// 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 +
  • Watch OS 6.0 +

UIHostingController

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

我刚刚在我的应用程序中用下面的代码行尝试了一下:

let vc = UIHostingController(rootView: BenefitsSwiftUIView())

其中 BenefitsSwiftUIView只是 SwiftUI中默认的“ Hello World”View。这跟你想的一模一样。它也可以工作,如果你子类 UIHostingController

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

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

在我的场景中,我使用一个现有的基于 Swift & UIKit 的应用程序,并将其完全迁移为一个 iOS13 & 纯 SwiftUI 应用程序,因此向后兼容性对我来说不是问题。

在对应用程序进行必要的更改之后:

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

并添加一个 SceneDepate 类:

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

我遇到了一个问题,我的场景代理没有被调用。通过在我的 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>

还有一个截图: enter image description here

保持同步的主要项目有:

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

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

如果要将 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)
}
}

参考文献

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

参见 在 Objective-C 应用程序中添加 SwiftUI

这是我们的朋友写的。

和故事板一起

你可以在 Interface Builder 中使用 ABc0组件:

Preview

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

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

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

Preview 2


用代码

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

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


重要提示

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

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

我是这么做的:

创建 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;
}
}

以下是包装视图的代码

对于这种非常简单的情况,Wrated 视图使用手动布局(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
}
}

您可以通过以下步骤来实现这一点..。

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

  2. 在 mainStoryboard 中添加 hostingViewController,并从按钮拖动 Segue (show Segue)到 hostingViewController。把故事板拖到 SwiftUI

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

  4. 在 viewController 中添加 mainStoryBoard 中显示的 segueAction 表单 segue。

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

@IBSegueAction func ActionMe(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(coder: coder, rootView: SWIFTUI())
}