Личная поваренная книга SwiftUI рецептов.
Когда начинаешь разбираться в какой то обширной теме в программировании - количество скачанных проектов, созданных черновиков начинает превышать все мыслимые и немыслимые пределы. А потом всё перемешивается, теряется. Вроде помнил, что ты с этим работал, а где, когда? Я пробовал работать в Playground'ах, но они на мой взгляд не такие стабильные как обычный проект, отваливается подсветка, нет возможности нормально делать Debug. С недавних пор я завел единый проект для исследования SwiftUI, и все небольшие вещи закидываю туда. Это помогает держать все в одном месте, к тому же поиск внутри проекта намного удобней. Хоть SwiftUI и предоставляет Preview для быстрого просмотра View, даже позволяет их отлаживать, все же этого не всегда хватает. Хочется и на устройстве проверить. А если держать все эти View внутри одного проекта - надо при создании новой вьюхи проставлять ее как основную в SceneDelegate, что довольно быстро начинает утомлять. Как было бы круто, если бы мы могли видеть все наши тестовые View при запуске приложения и могли выбрать с чем работать. Фантастика скажете вы? Отнюдь )
Задачу думаю можно решить более чем одним путем. Навскидку - прикрутить Sourcery, но интересно было решить без вспомогательных инструментов.
Итак, что из себя представляет View и её Preview:
import SwiftUI
struct ContentView: View {
var body: some View {
Text("Hello, World!")
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Как мы видим предпросмотр для view обеспечивается структурой, которая имплементирует PreviewProvider, если кто не знал, можно даже внутри одного файла создавать сколь угодно структур/классов, которые будут имплементировать PreviewProvider и в зоне предпросмотра они отобразятся все. Может пригодиться, если хотим разбить наш ContentView_Previews на неколько с разными настройками (хотя можно это же сделать и внутри одной структуры имплементирующей PreviewProvider, но речь не об этом).
Что из себя представляет PreviewProvider - это протокол
/// Produces view previews in Xcode.
///
/// Xcode statically discovers types that conform to `PreviewProvider` and
/// generates previews in the canvas for each provider it discovers.
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol PreviewProvider : _PreviewProvider {
/// The type of the previews variable.
associatedtype Previews : View
/// Generates a collection of previews.
///
/// Example:
///
/// struct MyPreviews : PreviewProvider {
/// static var previews: some View {
/// return Group {
/// GreetingView("Hello"),
/// GreetingView("Guten Tag"),
///
/// ForEach(otherGreetings, id: \.self) {
/// GreetingView($0)
/// }
/// }
/// .previewDevice("iPhone X")
/// }
/// }
static var previews: Self.Previews { get }
/// Returns which platform to run the provider on.
///
/// When `nil`, Xcode infers the platform based on the file the
/// `PreviewProvider` is defined in. This should only be provided when the
/// file is in targets that support multiple platforms.
static var platform: PreviewPlatform? { get }
}
Главное, что можно извлечь из кода, это не простой протокол, а PAT: Protocol with Associated Type, что сразу усложняет дело. Я перепробовал много вариантов как обеспечить нужную функциональность с минимальными усилиями.
Начнем с того как вообще можно подобные вещи делать real time? В Objective-C мы могли делать все что угодно с помощью reflection - получать список всех кассов, исследовать их свойства. В swift это все дело сильно ограничили, и Mirror не даст нам всего необходимого. Поэтому пришлось смотреть в сторону objc_getClassList, это метод из Obj-C рантайма, который позволяет получить список всех классов. К сожалению такого нет для Swift структур, поэтому пришлось обходится тем, что дали.
Разберем решение по частям.
Не получится нормально работать с системным протоколом PreviewProvider из за того, что он PAT, поэтом создадим Erase версию этого протокола
import SwiftUI
protocol PreviewHolder {
static var anyPreviews: AnyView { get }
static var name: String { get }
static var starred: Bool { get }
}
extension PreviewHolder where Self: PreviewProvider {
static var anyPreviews: AnyView {
AnyView(previews)
}
static var name: String {
String(describing: self).replacingOccurrences(of: "_Previews", with: "")
}
static var starred: Bool { false }
}
1. Как видно я стер тип у previews, создав обертку anyPreviews, которая будет возвращать AnyView. Я не очень люблю такие штуки, потенциальная потеря производительности, но т.к. это не production код, то на это можно закрыть глаза.
2. name - свойство возвращающее имя нашей View, как оно будет отображатсья в списке, учитывая что все Preview имеют автоматом генерируемые имена ViewName_Previews
- можно
_Previews
отрезать.
3. Я добавил свойство starred, т.к. число View будет все увеличиваться, и начиная работать с новым куском кода хочется увидеть его сверху в списке. Это можно сделать переопределив у превьюхи для новой View это свойство, возвращая true.
Сам список выглядит довольно просто.
import SwiftUI
struct PreviewsList: View {
@State private var starred: [PreviewHolder.Type] = []
@State private var general: [PreviewHolder.Type] = []
var body: some View {
NavigationView {
List {
Section(header: Text("Starred")) {
SubList(elements: starred)
}
Section(header: Text("General")) {
SubList(elements: general)
}
}.navigationBarTitle("Catalog")
.onAppear {
let sorted = PreviewUtils.parse().sorted(by: { (lhs, rhs) -> Bool in
lhs.name < rhs.name
})
self.starred = sorted.filter { $0.starred }
self.general = sorted.filter { !$0.starred }
}
}
}
}
private struct SubList: View {
var elements: [PreviewHolder.Type]
var body: some View {
ForEach(0..<elements.count, id: \.self) { id in
return NavigationLink(destination: self.elements[id].anyPreviews) {
Text(self.elements[id].name)
}
}
}
}
struct PreviewsList_Previews: PreviewProvider {
static var previews: some View {
PreviewsList()
}
}
Все View сортируются по имени и разбивается на 2 списка, starred и обычные. Выглядеть все будет примерно так:

Ну и в SceneDelegate просто меняем основную вьюху
let contentView = PreviewsList()
Остался последний момент, как же сделать так, чтобы наши Preview попали в этот список:
- поменять struct на class
- добавить поддержку PreviewHolder
т.е. вместо
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
станет
class ContentView_Previews: PreviewProvider, PreviewHolder {
static var previews: some View {
ContentView()
}
}
Опционально можно переопределять name и starred.
Это решение написано за пару часов, чтобы по быстрому испытать идею. При желании его можно наворотить по полной, проставляя теги, дату создания для Preview, показывать список не основным, а в в Debug окне, что позволит использовать даже на боевом проекте (не забываем отключать в Release сборке). В общем все зависит от вашей фантазии )
Скачать проект с базовой реализацией можно здесь: Github