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



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

Пожалуй, самым опасным врагом разработчика является скука. Когда становится скучно, работа перестает приносить радость, и мы допускаем множество ошибок. Именно поэтому в своей команде я всегда стараюсь поддерживать атмосферу.


Недавно мы начали активно использовать в проектах 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
}
}


226   0  

Comments

    Ничего не найдено.