54. Делаем превью для UIViewController
Многие приложения имеют в своем составе экраны, сделанные с использованием UIViewController. Чтобы проверить верстку, раньше приходилось запускать симулятор, а с появления SwiftUI достаточно сделать превью.
Сделаем UIViewController с несколькими вьюшками, принимающий на вход модель для отображения данных. Поскольку экран отображает данные модели, то в превью можно передать любые комбинации полей в модели, и без запуска симулятора можно проверить верстку во всех состояниях модели - это очень удобно.
Верстка экрана
final class VCExampleForPreview: UIViewController {
private let model: Model
/// Стек для всех вьюшек на экране
private lazy var stackView: UIStackView = {
let stack = UIStackView(
arrangedSubviews: [
imageView,
firstLabel,
secondLabel,
actionButton
]
)
stack.translatesAutoresizingMaskIntoConstraints = false
stack.alignment = .center
stack.axis = .vertical
stack.setCustomSpacing(30, after: actionButton)
stack.spacing = 16
return stack
}()
private var imageView: UIImageView {
let view = UIImageView()
view.image = .init(systemName: model.imageName)
view.tintColor = .black
return view
}
private var firstLabel: UILabel {
let label = UILabel()
label.text = model.title
label.font = .boldSystemFont(ofSize: 20)
return label
}
private var secondLabel: UILabel {
let label = UILabel()
label.text = model.subtitle
label.font = .systemFont(ofSize: 18)
label.numberOfLines = 0
return label
}
private lazy var actionButton: UIButton = {
.init(
configuration: .borderedProminent(),
primaryAction: .init(
title: model.buttonTitle,
handler: { _ in print("Нажали на кнопку!") }
)
)
}()
init(model: Model) {
self.model = model
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(stackView)
let demoItems = model.listItems.map { text -> UIView in
let label = UILabel()
label.text = text
label.textAlignment = .center
label.widthAnchor.constraint(equalToConstant: view.bounds.width).isActive = true
return label
}
demoItems.forEach(stackView.addArrangedSubview)
NSLayoutConstraint.activate([
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20),
stackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20)
])
}
}
Модель для экрана
extension VCExampleForPreview {
/// Модель с данными для отображения на экране
struct Model {
let title, subtitle, buttonTitle, imageName: String
let listCount: Int
var listItems: [String] {
(0..<listCount).map {
"Элемент # \($0) для экрана \(title) "
}
}
}
}
#Preview (iOS 17)
Если вам повезло (или вы читаете статью из будущего), и ваш проект поддерживает минимальную версию iOS 17, то при поддержке макросов (Xcode 15+) можно сразу сделать превью:
#Preview("Профиль") {
VCExampleForPreview(
model: .init(
title: "Профиль",
subtitle: "Информация о пользователе",
buttonTitle: "Загрузить профиль",
imageName: "person.fill",
listCount: 10
)
)
}
#Preview("Настройки") {
VCExampleForPreview(
model: .init(
title: "Настройки",
subtitle: "Кастомизация приложения",
buttonTitle: "Сбросить настройки",
imageName: "gear",
listCount: 5
)
)
}
#Preview (iOS < 17)
Если ваш проект поддерживает минимальную версию iOS ниже 17, то нужно будет воспользоваться UIViewControllerRepresentable, чтобы завести превью:
struct UIKitPreviewExample: UIViewControllerRepresentable {
let model: VCExampleForPreview.Model
init(model: VCExampleForPreview.Model) {
self.model = model
}
func makeUIViewController(context: Context) -> VCExampleForPreview {
.init(model: model)
}
func updateUIViewController(_ uiViewController: VCExampleForPreview, context: Context) {}
}
Пишем код для превью
#Preview("Профиль") {
UIKitPreviewExample(
model: .init(
title: "Профиль",
subtitle: "Информация о пользователе",
buttonTitle: "Загрузить профиль",
imageName: "person.fill",
listCount: 10
)
)
}
На первый взгляд все должно работать, но у меня превью не запускается. Жмем на кнопку для отображения отчета об ошибке превью и видим:
Решение проблемы
Нужно передать дефолтное значение в инициализатор репрезентабл-вьюхи. Сделать это можно двумя способами:
struct UIKitPreviewExample: UIViewControllerRepresentable {
let model: VCExampleForPreview.Model
init(
model: VCExampleForPreview.Model = .init(
title: "Профиль",
subtitle: "Информация о пользователе",
buttonTitle: "Загрузить профиль",
imageName: "person.fill",
listCount: 10
)
) {
self.model = model
}
// остальной код
}
extension VCExampleForPreview {
/// Модель с данными для отображения на экране
struct Model {
// остальной код
/// Статичное свойство для превью "Профиля"
static let optionA = Self(
title: "Профиль",
subtitle: "Информация о пользователе",
buttonTitle: "Загрузить профиль",
imageName: "person.fill",
listCount: 10
)
/// Статичное свойство для превью "Настроек"
static let optionB = Self(
title: "Настройки",
subtitle: "Кастомизация приложения",
buttonTitle: "Сбросить настройки",
imageName: "gear",
listCount: 5
)
}
}
Передаем статичное свойство в превью (такой вариант мне больше нравится):
struct UIKitPreviewExample: UIViewControllerRepresentable {
let model: VCExampleForPreview.Model
init(model: VCExampleForPreview.Model = .optionA) {
self.model = model
}
// остальной код
}
Итоговый вариант превью
Применим статичные свойства из модели сразу в превью:
#Preview("Профиль") {
UIKitPreviewExample(model: .optionA)
}
#Preview("Настройки") {
UIKitPreviewExample(model: .optionB)
}
Теперь превью работает исправно.
Альтернативный вариант превью (Xcode < 15)
Можно сделать превью с использованием PreviewProvider, и это будет выглядеть так:
struct OldPreviewExample: PreviewProvider {
static var previews: some View {
Group {
UIKitPreviewExample(model: .optionA)
.previewDisplayName("Профиль")
UIKitPreviewExample(model: .optionB)
.previewDisplayName("Настройки")
}
}
}
В качестве "бонуса" мы получили возможность создать несколько вкладок превью с использованием Group, в то время как при работе с макросом такой финт не прокатит.
Заключение
Даже если вы не можете использовать SwiftUI для верстки в своем проекте, вы можете использовать превью для упрощения работы.
Особенно это помогает в больших проектах, где сборка занимает больше 30 секунд. Первый раз превью запускается примерно такое же время, как и сборка на симулятор, зато при последующих изменениях превью загружается в пределах 10 секунд, и я смело оцениваю эту скорость на 10/10.
На более слабых маках превью могут запускаться дольше. Самый слабый девайс, на котором я запускал превью (и все работало идеально) - MacBook Air M1 (2020).
Если возникают проблемы с запуском превью, рекомендую ознакомиться с этой статьей, где я разобрал самые частые проблемы, ломающие превью.
Код для этой статьи можно посмотреть тут, а другие статьи - тут.