Можно ли создавать представления с помощью SwiftUI рядом с существующим приложением UIKit?
У меня есть существующее приложение, написанное на Objective-C. Я начал переход на Swift 5. Мне интересно, могу ли я использовать SwiftUI вместе с моими существующими представлениями UIKit .xib.
То есть я хочу, чтобы некоторые представления, созданные с помощью SwiftUI, и некоторые другие представления, созданные с помощью UIKit, были в одном приложении. Не смешивая их, конечно.
SomeObjCSwiftProject/
SwiftUIViewController.swift
SwiftUIView.xib
UIKitViewController.swift
UIKitView.xib
Работая рядом друг с другом





edit 06.05.19: добавлена информация о UIHostingController, предложенная @Departamento B в его ответе. Кредиты идут к нему!
Можно использовать компоненты SwiftUI в существующих средах UIKit, обернув SwiftUIView в UIHostingController следующим образом:
let swiftUIView = SomeSwiftUIView() // swiftUIView is View
let viewCtrl = UIHostingController(rootView: swiftUIView)
Также возможно переопределить UIHostingController и настроить его под свои нужды, например. грамм. установив preferredStatusBarStyle вручную, если он не работает через SwiftUI, как ожидалось.
UIHostingController задокументировано здесь.
Если существующее представление UIKit необходимо использовать в среде SwiftUI, протокол UIViewRepresentable поможет! Это задокументировано здесь, и его можно увидеть в действии в официальном руководстве Apple по это.
Совместимость
Обратите внимание, что вы не можете использовать SwiftUI на версиях iOS < iOS 13, так как SwiftUI доступно только на iOS 13 и выше. Дополнительную информацию см. в сообщении это.
Если вы хотите использовать SwiftUI в проекте с целью ниже iOS 13, вам нужно пометить свои SwiftUI структуры атрибутом @available(iOS 13.0.0, *).
@DepartamentoB Спасибо! Вы правы, я отредактирую соответственно
@fredpi, есть ли способ использовать navigationLink для отправки UIViewController? Или как только вы начнете использовать представления SwiftUI в раскадровке своего приложения, вам придется продолжать использовать этот фреймворк?
При использовании SwiftUI можно ориентироваться на iOS 12 и ниже. Конечно, SwiftUI будет использоваться только iOS 13, для более ранних версий iOS вам нужно будет создавать представления UIKit (дублировать работу...). Но для тех, кто мигрирует, может быть полезно. Подробнее здесь: stackoverflow.com/a/58372597/840742
Как удалить SomeSwiftUIView() из parentViewController?
Вы можете использовать их вместе. Вы можете «перенести» UIView в View с помощью UIViewRepresentable соответствия. Подробности можно найти в файле официальный учебник.
Тем не менее, вы также должны учитывать его совместимость.
Вот фрагмент кода из протокола 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.
/// ...
}
Так что обратной совместимости нет.
Это наоборот, когда UIView размещается в представлении.
Хотя на данный момент документация по классу не написана, UIHostingController<Content> похоже это то, что вы ищете: https://developer.apple.com/documentation/swiftui/uihostingcontroller
Я только что попробовал это в своем приложении со следующей строкой кода:
let vc = UIHostingController(rootView: BenefitsSwiftUIView())
Где BenefitsSwiftUIView — это просто «Hello World» по умолчанию View из SwiftUI. Это работает именно так, как вы ожидаете. Это также работает, если вы подкласс UIHostingController.
Вам не нужно явно указывать общий параметр, его можно вывести;)
Да, я только что понял, что
Есть соответствующий NSHostingController, который сейчас — 11b2 — не работает. Если вы хотите использовать SwiftUI в приложении на основе раскадровки, вам нужно отключить песочницу и использовать NSHostingView, который вы можете установить в качестве содержимого вашего окна. (Да, я использую тот же код, что и с UIHostingController. iOS работает, macOS — нет.)
@DepartamentoB Я не могу использовать подкласс UIHostingController. Вы знаете, как это использовать?
@sarunw Тебе не нужно. Это также работает, если вы подкласс, но пример, который я привел, не работает.
@DepartamentoB Я знаю, что ваш ответ не для подкласса, но я хочу знать, как заставить его работать с подклассом. Я не могу заставить его работать таким образом.
Вы можете просто создать подкласс 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
Другие демонстрировали, как использовать 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
}
}
Один элемент, о котором я еще не упоминал, включает в себя 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>
Основные элементы, которые необходимо синхронизировать:
SceneDelegateUISceneConfigurationПосле этого я смог загрузить свое недавно созданное представление 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)
}
}
Это самый полный ответ здесь. Кроме того, он работает лучше других методов для правильного размещения представления SwiftUI при использовании симулятора на iOS 15.
Если вы хотите создать представление SwiftUI из устаревшего проекта Objective C, то этот метод отлично сработал для меня,
См. Добавление SwiftUI в приложения Objective-C
Спасибо нашему другу, который это написал.
он достал меня: «Я никогда в жизни не написал ни строчки на Swift!» :)
Вы можете использовать компонент 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.
если вы столкнулись с проблемами макета, вы должны добавить ограничения для представления 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
}
}
/**
* 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
}
}
Вы можете добиться этого, просто выполнив следующие шаги...
Создайте кнопку в viewController, представленную на mainStoryBoard, и импортируйте SwiftUI.
Добавьте hostingViewController в mainStoryboard и перетащите переход (показать переход) с кнопки на hostingViewController.Перетащите переход из StoryBoard в SwiftUI.
Добавьте файл SwiftUI в свой проект, нажав Cmd+N Создание файла SwiftUI
Добавьте переход формы segueAction, представленный в mainStoryBoard, в viewController.
Напишите следующие строки кода в segueAction...
@IBSegueAction func ActionMe(_ coder: NSCoder) -> UIViewController? {
return UIHostingController(coder: coder, rootView: SWIFTUI())
}
Это, на мой взгляд, лучший ответ. Может быть немного сложнее, но это работает просто и отлично.
UIViewRepresentable, похоже, делает наоборот, позволяя добавитьUIViewв иерархиюSwiftUI