Изнутри: Swift макрос — #Preview

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Макрос #Preview в языке Swift предоставляет удобный способ создания и предварительного просмотра компонентов пользовательского интерфейса. Он позволяет разработчикам быстро и легко создавать превью для своих View, чтобы визуально оценить, как они выглядят и взаимодействуют.

Сейчас доступно много информации о том, как писать макросы, много примеров и на удивление хорошая документация. Сегодня мы будем не создавать свой макрос, а подробно рассмотрим приватные макросы, предоставляемые Apple, а именно #Preview.

Как #Preview работает?

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

#Preview {
    Text("Simple text")
}

Превью уже работает, и мы можем увидеть наше представление. Однако нас интересует совсем другое — результат работы нового макроса #Preview, а именно генерируемый код.

Что генерирует макрос #Preview?

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

Для просмотра сгенерированного кода можно использовать Xcode, нажав на Expand Macro:

А для понимания, где генерируемый код располагается, мы можем воспользоваться командой swiftc:

swiftc TextPreviewView.swift

Не мог не отметить, что есть ещё вариант просмотра результата работы макроса с помощью тестов

При запуске команды мы получим сгенерированный файл @__swiftmacro_04TestA4View33_DC5l7PreviewfMf0 по пути /var/folders/zd/gc5dlw40000gq/T/swift-generated-sources/.

Открыв файл, мы увидим следующий код и сможем изучить результаты работы макроса (Xcode 15 Beta 2):

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
struct $s04TestA4View33_DC5l7PreviewfMf0_15PreviewRegistryfMu_: PreviewRegistry {
    static var fileID: String {
        "TextPreviewView.swift"
    }
    static var line: Int {
        17
    }
    static var column: Int {
        1
    }

    static var preview: Preview {
        Preview {
            Text("Simple text")
        }
    }
}

#if os(xrOS)
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
@objc final class $s04TestA4View33_DC5l7PreviewfMf0_17UVPreviewRegistryfMu_: UVPreviewRegistry {
    override var fileID: String {
        "TextPreviewView.swift"
    }
    override var line: Int {
        17
    }
    override var column: Int {
        1
    }

    override var preview: Preview {
        Preview {
            Text("Simple text")
        }
    }
}
#endif
// original-source-range: TextPreviewView.swift:17:1-19:2

Немного подробнее о сгенерированном коде

Код получился довольно не большим, но давайте добавим разъяснения того, что в нём и зачем.

Обратим внимание на два, практически одинаковых, объекта:

struct $s04TestA4View3... и class $s04TestA4View3…

Интересно что class доступен только для xrOS(VisionOS). Тяжело предположить зачем Apple сделала структуру и класс с одинаковыми параметрами, а не использовала просто структуру. Может быть class с пометкой @objcиспользуется для поиска Preview в runtime, но зачем тогда пракически идентичная структура - вопрос открытый.

Начнем с протокола, которому соответствует struct $s04TestA4View3....

PreviewRegistry

Это протокол PreviewRegistry, и мы можем просто посмотреть его документацию:

/// Registry protocol used to locate previews at runtime. Types conforming to this protocol are
/// generated for you by the expansion of the `#Preview` macros.
///
/// - Note: Previews should always be created using the `#Preview` macro syntax.
/// Behavior for preview registries defined directly is undefined.
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
public protocol PreviewRegistry {
    static var fileID: String { get }
    static var line: Int { get }
    static var column: Int { get }
    static var preview: Preview { get }
}

Из нее мы узнаем, что этот протокол необходим для поиска превью во время выполнения. Назначение полей fileIDline и column достаточно сложно предположить. Возможно, они нужны для определения конкретного View для превью, если их несколько в одном файле.

Preview

Интересно, что в PreviewRegistry у нас есть static var preview: Preview и в частности тип Preview. Информации о нём достаточно мало:

/// Base type for creating previews.
///
/// Extensions in SwiftUI, UIKit, AppKit, and WidgetKit provide
/// subject-matter-specific initializers.
@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
public struct Preview { }

Так как понять, что из себя представляет эта структура, мы пока не можем, то попробуем извлечь немного больше информации, используя инструмент для просмотра деталей объекта Mirror:

// Получение наименования переменных внутри стркутуры `Preview`
Mirror(reflecting: previewRegistry.preview).children.map(\.label)

Сопоставив данные из Mirror, я смог получить следующую структуру:

@available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *)
public struct Preview {
     var displayName: String?
     var source: ViewPreviewSource
     var fileID: String
     var line: Int
     var traits: [PreviewTrait]
}

Теперь с этим можно работать. Давайте рассмортим части Preview подробнее, и начнем с ViewPreviewSource.

ViewPreviewSource

Еще из интересного: в Preview есть переменная source: ViewPreviewSource, которая на самом деле является просто обёрткой для нашего представления. ViewPreviewSource используется как контейнер, в котором может лежать как SwiftUI View, так и UIKit UIView или UIViewController. Благодаря этому механизму достигается поддержка работы Preview с UIKit, чего раньше превью не поддерживали без использования дополнительных обёрток. Таким образом, приложение может эксплуатировать возможности обоих фреймворков и позволяет без проблем создавать превью интерфейсов без необходимости вводить дополнительные абстракции или преобразования. Снова обратившись к Mirror, получаем примерный вид ViewPreviewSource структуры:

struct ViewPreviewSource<T> {
     var makeView: () -> T
}

Зачем вообще углубляться?

Понимание того, как Apple использует свои же нововведения, очень сильно помогает в работе над собственными решениями. Например, этот разбор я проводил для проекта: