Включить представления SwiftUI в существующее приложение UIKit

Возможно ли строить представления с SwiftUI рядом с существующим приложением UIKit?

У меня есть приложение, написанное на Objective-C. Я начал миграцию на Swift 5. Мне интересно, могу ли я использовать SwiftUI вместе с моими существующими представлениями UIKit .xib.

То есть я хочу, чтобы некоторые представления, созданные с помощью SwiftUI, и некоторые другие представления, созданные с помощью UIKit в том же приложении. Не смешивая два, конечно.

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

Работаем рядом друг с другом

13 ответов

Решение

редактировать 06.05.19: Добавлена ​​информация о UIHostingController, предложенная @Departamento B в его ответе. Кредиты идут к нему!


Использование SwiftUI с UIKit

Можно использовать SwiftUI компоненты в существующих UIKit окружения, оборачивая SwiftUIView в UIHostingController нравится:

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

Также возможно переопределить UIHostingController и настроить его в соответствии со своими потребностями, например, установив preferredStatusBarStyle вручную, если он не работает через SwiftUI как и ожидалось.

UIHostingController задокументировано здесь.


Использование UIKit с SwiftUI

Если существующий UIKit представление должно быть использовано в SwiftUI окружающая среда, UIViewRepresentable Протокол здесь, чтобы помочь! Это задокументировано здесь, и его можно увидеть в действии в этом официальном руководстве Apple.


Совместимость

Обратите внимание, что UIKit а также SwiftUI компоненты могут использоваться только в сочетании, если приложение предназначено для iOS 13+, так как SwiftUI доступна только iOS 13+. Смотрите этот пост для получения дополнительной информации.

Если вы хотите встроить 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)
    }
}

Справка

UIHostingController

Хотя на данный момент документация для класса не была написана, UIHostingController<Content> кажется, что вы ищете: https://developer.apple.com/documentation/swiftui/uihostingcontroller

Я только что попробовал это в моем приложении со следующей строкой кода:

let vc = UIHostingController(rootView: BenefitsSwiftUIView())

где BenefitsSwiftUIView это просто по умолчанию "Hello World" View из SwiftUI, Это работает именно так, как вы ожидаете. Это также работает, если вы подкласс UIHostingController,

Вот как я это делаю:

Создайте адаптер 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
    }
}

Один элемент, о котором я еще не упоминал, включает в себя 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)

Если вы столкнулись с проблемами макета, вы должны добавить ограничения для просмотра 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 вид из устаревшего проекта Objective C, то эта техника отлично сработала для меня,

См. Раздел Добавление SwiftUI в приложения Objective-C

Престижность нашему другу, который это написал.

С раскадровкой

Ты можешь использовать 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

Вы можете добиться этого, просто выполнив следующие действия ...

  1. Создайте кнопку в viewController, представленном на mainStoryBoard, и импортируйте SwiftUI.

  2. Добавьте hostingViewController в mainStoryboard и перетащите Segue (показать Segue) с кнопки на hostingViewController.Перетащите переход из StoryBoard в SwiftUI

  3. Добавьте файл SwiftUI в свой проект, нажав Cmd+NСоздание файла SwiftUI.

  4. Добавьте переход формы segueAction, представленный в mainStoryBoard, в viewController.

  5. Напишите следующие строки кода в segueAction ...

      @IBSegueAction func ActionMe(_ coder: NSCoder) -> UIViewController? {
    return UIHostingController(coder: coder, rootView: SWIFTUI())
}
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

Вы можете использовать их вместе. Вы можете "передать" UIView в View по UIViewRepresentable соответствия. Подробности можно найти в официальном учебнике.

Тем не менее, вы также должны учитывать его совместимость.

Вот фрагмент кода из протокола View SwiftUI:

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

Конечно, вы можете использовать SwiftUI бок о бок с UIKit, в таком случае он имеет хорошую ценность. Потому что, если вы используете его отдельно, UIKit по сравнению со SwiftUI будет довольно плохим из-за того, что он анонсирован вместе с iOS 2.0. Со SwiftUI работают только новейшие версии iOS. В одном исследовании вы можете увидеть, как SwiftUI борется с UIKit. Но вместе они более эффективны для создания архитектуры мобильных приложений.

Другие продемонстрировали, как использовать UIHostingController.

Я могу показать, как вы можете представить UIViewController из SwiftUI 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
    }
}
Другие вопросы по тегам