Включить представления 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
окружения, оборачивая SwiftUI
View
в 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
Вы можете добиться этого, просто выполнив следующие действия ...
Создайте кнопку в viewController, представленном на mainStoryBoard, и импортируйте SwiftUI.
Добавьте hostingViewController в mainStoryboard и перетащите Segue (показать Segue) с кнопки на hostingViewController.Перетащите переход из StoryBoard в SwiftUI
Добавьте файл SwiftUI в свой проект, нажав Cmd+NСоздание файла SwiftUI.
Добавьте переход формы segueAction, представленный в mainStoryBoard, в viewController.
Напишите следующие строки кода в 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
}
}