Пожалуй, самым опасным врагом разработчика является скука. Когда становится скучно, работа перестает приносить радость, и мы допускаем множество ошибок. Именно поэтому в своей команде я всегда стараюсь поддерживать атмосферу.
Недавно мы начали активно использовать в проектах SwiftUI. Большинство из этих проектов должны быть совместимы с iOS 13, поэтому мы решили, что будет неразумно писать все приложение в SwiftUI. Первая итерация была слишком “сырой”, особенно по части навигации, и было бы слишком рискованно поставлять все приложение, полностью написанное с помощью SwiftUI. Более того, нам не хотелось тратить все наработки, созданные в UIKit. Поэтому мы решили пойти на смешивание этих инструментов.
Добавить SwiftUI в UIKit несложно, если воспользоваться простым расширением. Но начнем с нашего примера.
Весь готовый проект можно найти на моей странице Patreon.
Цель
Основная идея заключается в создании небольших представлений в SwiftUI и их внедрении в контроллеры, или представления, UIKit, а именно в канонический UIViewControllers
, находящийся в проекте Storyboard
, передавая данные между этими двумя компонентами.
Представление SwiftUI
Создадим простое представление. Массив поддерживающих нажатие элементов Text
с переключаемым цветом фона:
import SwiftUI
struct MyView: View {
@State var items: [String]
@State private var buttonStates: [String : Bool] = [:]
func getStateFor(_ key: String) -> Bool {
buttonStates[key] ?? false
}
var body: some View {
HStack {
ForEach(items, id:\.self) { itm in
Text(itm)
.frame(minWidth: 120)
.padding()
.background(getStateFor(itm) ? Color.green.gradient : Color.orange.gradient)
.cornerRadius(10)
.shadow(radius: 10)
.onTapGesture {
let state = getStateFor(itm)
buttonStates[itm] = !state
print(itm)
}
}
}
.background(.clear)
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(items: ["First", "Second"])
}
}
Очень примитивное представление, но это не важно.
Вот результат:
Магические расширения
Добавить представление SwiftUI в UIViewController довольно легко — достаточно проделать ряд конкретных шагов.
- Создать в представлении SwiftUI
UIHostingController
в качестве егоrootView
. - Установить этот контроллер в качестве чистого фона.
- Добавить в базовое представление UIKit
UIViewController
представлениеUIHostingController
. - Заполнить все доступное пространство с помощью autolayout.
Последовательность действий всегда одна и та же, поэтому мы можем написать extension
для View
и затем просто его переиспользовать.
import SwiftUI
extension View {
func injectIn(controller vc: UIViewController) {
let controller = UIHostingController(rootView: self)
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.view.backgroundColor = .clear
vc.view.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: vc.view.leadingAnchor),
controller.view.topAnchor.constraint(equalTo: vc.view.topAnchor),
controller.view.trailingAnchor.constraint(equalTo: vc.view.trailingAnchor),
controller.view.bottomAnchor.constraint(equalTo: vc.view.bottomAnchor)
])
}
}
Код получается довольно простым и, на мой взгляд, не требует пояснения.
Теперь пора его использовать, внедрив представление SwiftUI в контроллер основного представления.
Откроем класс UIViewController
(напомню, что мы работаем над проектом, созданным с помощью Storyboards
) и просто изменим код во viewDidLoad
:
import UIKit
class ViewController: UIViewController {
var myView: MyView?
override func viewDidLoad() {
super.viewDidLoad()
// Релаизуем магию!
myView = MyView(items: ["First", "Second"])
myView?.injectIn(controller: self)
}
}
Получится такой результат:
Все работает, но можно его улучшить. Сейчас представление SwiftUI занимает все пространство UIViewController
, но зачастую нам нужно внедрить компонент только в часть экрана, возможно, используя дополнительные представления SwiftUI в одном контроллере. Решение здесь простое — мы добавим еще одно расширение для внедрения на этот раз представления SwiftUI в представление UIKit
:
import SwiftUI
extension View {
func injectIn(view: UIView) {
let controller = UIHostingController(rootView: self)
controller.view.translatesAutoresizingMaskIntoConstraints = false
controller.view.backgroundColor = .clear
view.addSubview(controller.view)
NSLayoutConstraint.activate([
controller.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
controller.view.topAnchor.constraint(equalTo: view.topAnchor),
controller.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
controller.view.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
}
Это расширение очень похоже на предыдущее. Применим его.
Добавьте UIView
в Storyboard
, установите его ограничения (картинка ниже) и подключите в качестве IBOutlet
в контроллере представлений.
А вот код контроллеров представлений:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var buttonsView: UIView!
var myView: MyView?
override func viewDidLoad() {
super.viewDidLoad()
// Выполнение дополнительных настроек после загрузки представления.
myView = MyView(items: ["First", "Second"])
myView?.injectIn(view: buttonsView)
buttonsView.backgroundColor = .clear
}
}
Обратите внимание на строку 13, где я устанавливаю цвет фона на .clear
, чтобы скрыть любой присутствующий фон UIView
. Вот результат:
Получение данных из представления SwiftUI
Тем не менее недостаточно просто отражать представление SwiftUI в представлении UIKit. В реальном компоненте вам наверняка потребуется получать от него данные и использовать их в контроллере основного представления.
Для этого нужно просто добавить в представление SwiftUI замыкание (строки 8 и 26), после чего все заработает:
import SwiftUI
struct MyView: View {
@State var items: [String]
@State private var buttonStates: [String : Bool] = [:]
let onTap: (String) -> Void
func getStateFor(_ key: String) -> Bool {
buttonStates[key] ?? false
}
var body: some View {
HStack {
ForEach(items, id:\.self) { itm in
Text(itm)
.frame(minWidth: 120)
.padding()
.background(getStateFor(itm) ? Color.green.gradient : Color.orange.gradient)
.cornerRadius(10)
.shadow(radius: 10)
.onTapGesture {
let state = getStateFor(itm)
buttonStates[itm] = !state
onTap(itm)
}
}
}
.background(.clear)
}
}
struct MyView_Previews: PreviewProvider {
static var previews: some View {
MyView(items: ["First", "Second"]) { _ in }
}
}
Затем измените контроллер представлений:
import UIKit
class ViewController: UIViewController {
@IBOutlet weak var buttonsView: UIView!
var myView: MyView?
override func viewDidLoad() {
super.viewDidLoad()
myView = MyView(items: ["First", "Second"]) { selectedString in
print(selectedString)
}
myView?.injectIn(view: buttonsView)
buttonsView.backgroundColor = .clear
}
}
Читайте также:
- Стратегии Async/Await и MainActor
- Как создать анимацию колебания с помощью UIKit
- Тонкости представления нижнего всплывающего экрана в iOS 15
Читайте нас в Telegram, VK и Дзен
Перевод статьи Alessandro Manilii: Using SwiftUI in UIKit