【问题标题】:Include SwiftUI views in existing UIKit application在现有的 UIKit 应用程序中包含 SwiftUI 视图
【发布时间】:2019-10-19 09:22:00
【问题描述】:

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

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

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

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

彼此合作

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    edit 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 并根据自己的需要对其进行自定义,例如。 G。如果 preferredStatusBarStyle 无法按预期工作,则手动设置 preferredStatusBarStyle

    UIHostingController 已记录在 here


    在 SwiftUI 中使用 UIKit

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


    兼容性

    请注意,您不能在 SwiftUI,因为 SwiftUI 仅适用于 iOS 13 及更高版本。请参阅this 帖子了解更多信息。

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

    【讨论】:

    • UIViewRepresentable 似乎相反,可以将UIView 添加到SwiftUI 层次结构中
    • @DepartamentoB 谢谢!你是对的,我会相应地编辑
    • @fredpi,有没有办法使用 navigationLink 来推送 UIViewController?或者一旦你开始在应用故事板中使用 SwiftUI 视图,你就必须继续使用这个框架?
    • 在使用 SwiftUI 时可以针对 iOS 12 及更低版本。当然,SwiftUI 将仅由 iOS 13 使用,对于较低的 iOS 版本,您需要制作 UIKit 视图(重复工作......)。但对于那些正在迁移的人,可能会有所帮助。更多信息:stackoverflow.com/a/58372597/840742
    【解决方案2】:

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

    Reference

    【讨论】:

    • 这是这里最完整的答案。此外,在 iOS 15 上使用模拟器时,它恰好比其他正确放置 SwiftUI 视图的方法更好。
    【解决方案3】:

    UIHostingController

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

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

    let vc = UIHostingController(rootView: BenefitsSwiftUIView())
    

    其中BenefitsSwiftUIView 只是来自SwiftUI 的默认“Hello World”View。这完全符合您的预期。如果您将UIHostingController 子类化,它也可以工作。

    【讨论】:

    • 你不需要明确指定泛型参数,它可以被推断出来;)
    • 是的,我刚刚意识到
    • 有一个对应的 NSHostingController,现在 - 11b2 - 不起作用。如果您想在基于故事板的应用程序中使用 SwiftUI,您需要关闭沙盒并使用可以设置为窗口的 contentView 的 NSHostingView。 (是的,我使用与 UIHostingController 相同的代码。iOS 可以工作,macOS 不能。)
    • @DepartamentoB 我不能使用 UIHostingController 的子类。你知道怎么用吗?
    • @sarunw 你不需要。如果你子类化它可以工作,但我给出的例子没有。
    【解决方案4】:

    我还没有看到提到的一个项目,它涉及 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 对象)

    【讨论】:

      【解决方案5】:

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

      Adding SwiftUI to Objective-C Apps

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

      【讨论】:

      • 他让我明白了:“我这辈子从来没有写过 Swift 的一行代码!” :)
      【解决方案6】:

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

      【讨论】:

        【解决方案7】:

        我是这样做的:

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

        【讨论】:

          【解决方案8】:

          使用故事板

          您可以在界面生成器中使用HotingViewController 组件:

          那么,如果你有一个像这样的简单HotingController

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

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


          带代码

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

          然后你就可以像普通的UIViewController一样使用它了


          重要提示

          不要忘记将SwiftUI 导入到您需要UIHostingController 的地方

          【讨论】:

          • 这不起作用,至少在 iOS 15 上是这样
          【解决方案9】:

          您可以一起使用它们。您可以通过UIViewRepresentable 一致性将UIView“转移”到View。详情可见official tutorial

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

          这是 SwiftUI 的 Protocol View 的代码 sn-p:

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

          【讨论】:

          • 反之亦然,UIView 托管在 View 中
          【解决方案10】:
          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
          

          【讨论】:

            【解决方案11】:

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

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

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

            3. 通过单击 Cmd+N 在您的项目中添加 SwiftUI 文件 Creating SwiftUI file

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

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

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

            【讨论】:

            • 这是我认为最好的答案。可以更详细一点,但它的工作原理简单而出色。
            【解决方案12】:

            当然,您可以将 SwiftUI 与 UIKit 一起使用,在这种情况下,它的价值很高。因为如果单独使用的话,UIKit 和 SwiftUI 相比是相当差的,因为它是和 iOS 2.0 一起发布的。使用 SwiftUI,只能使用最新的 iOS 版本。在one study,你可以看到 SwiftUI 是如何与 UIKit 竞争的。但结合起来,它们在创建移动应用架构方面更加强大。

            【讨论】:

            • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
            【解决方案13】:

            Others 一直在展示如何使用 UIHostingController

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

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

            【讨论】:

              猜你喜欢
              • 2020-10-19
              • 1970-01-01
              • 2022-07-11
              • 1970-01-01
              • 2020-10-21
              • 2019-10-31
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多