Быстро создавайте масштабируемые приложения для iOS

Я выступал с докладом о масштабируемых приложениях для iOS на SwiftHeroes 2021, в котором я говорю о четырех уровнях сложности приложения, заканчивая тем, что мы до сих пор не знаем, каким будет пятый уровень.

У меня есть одно предположение: я считаю, что приложение iOS «пятого уровня» будет приложением, содержащим столько модулей и архитектурных компонентов, что любое небольшое добавление потребует написания огромного количества шаблонного кода. В таком приложении переход с четвертого на пятый уровень будет определяться способностью приложения генерировать большую часть своих шаблонов и требований.

Обратите внимание, что я не говорю о генерации кода в смысле таких инструментов, как Sourcery, где вы определяете шаблоны для общих действий, отправляете ввод и получаете вывод. Я говорю о создании кода для вещей, которые вы даже не знаете, каковы входные данные.

Да, это возможно! Я занимаюсь «интеллектуальной генерацией кода» уже пару лет и хочу показать вам, как этого добиться с помощью SourceKit.

Пример: создание списка зависимостей библиотеки внедрения зависимостей

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

struct ProfileFeature: Feature {
    @Dependency var client: HTTPClientProtocol
    func build(fromRoute route: Route?) -> UIViewController {
        return ProfileViewController(client: client)
    }
}

Однако есть одна вещь, которая немного раздражает в этом: поскольку безопасность типов является важным аспектом библиотеки, вам нужно вручную сообщить RouterService обо всех доступных функциях в вашем приложении:

routerService.register(feature: ProfileFeature.self)
routerService.register(feature: SomeOtherFeature.self)

(Фактическая регистрация немного отличается от этой, но я упростил ее, чтобы не слишком искажать тему.)

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

Мы можем улучшить это, автоматизируя этот шаг с помощью генерации кода. Если вы поддерживаете список функций где-то в файле, вы можете использовать Sourcery для создания этого шаблона, но я хотел бы показать вам как сгенерировать этот код, не имея любая предварительная информация о списке функций приложения. Никаких специальных файлов yaml, никаких специальных вызовов, просто запустите инструмент и получите этот код.

Представляем SourceKit

SourceKit - это механизм подсветки синтаксиса Swift. Что ж, я полагаю, что технически движком является сам Swift, но SourceKit - это то, как Xcode может реализовать все свои функции Swift IDE, такие как форматирование, переход к определенным символам и, конечно же, подсветку синтаксиса. Часть инструментов Swift, SourceKit - это библиотека C, которая абстрагирует компилятор Swift и поставляется внутри цепочки инструментов Swift при загрузке Xcode. Это означает, что вам не нужно загружать специальный двоичный файл, чтобы следовать этому руководству - если у вас есть Xcode, у вас также есть SourceKit.

Как и Swift, SourceKit имеет открытый исходный код, и вы можете использовать его вне Xcode, чтобы предоставить любому проекту возможности Swift IDE. Несколько лет назад проекты на базе SourceKit были обычным явлением, и наиболее известным из них был фреймворк SourceKitten от JP Simard, который позволил вам использовать SourceKit непосредственно в Swift. Фактически, многие популярные инструменты, связанные с кодом, такие как SwiftLint, Jazzy и даже сам Sourcery, все еще используют SourceKitten под капотом. В последнее время я не видел, чтобы какие-либо проекты, связанные с SourceKit, разрабатывались, но вы все еще можете использовать его для создания интеллектуальных проектов.

SourceKit работает через формат запроса / ответа. Инструмент может получать многие типы запросов, связанных с IDE, такие как открытие файлов, индексация содержимого, автозаполнение, поиск определения символа, форматирование и т. Д. Отправив структурированный объект запроса, вы получите структурированный ответ с результатом того, что вы просите.

Фактически вы можете увидеть SourceKit в действии, запустив Xcode с включенным ведением журнала SourceKit:

export SOURCEKIT_LOGGING=3 && /Applications/Xcode.app/Contents/MacOS/Xcode > log.txt

С флагом SOURCEKIT_LOGGING Xcode начнет сбрасывать каждый сделанный запрос в SourceKit. Попробуйте выполнить некоторые стандартные действия, например, дождаться автозаполнения, и посмотрите, как SourceKit это делает! Журналы будут содержать много шума, но если вам нужен указатель, поищите вызовы запроса editor_open, который выполняется всякий раз, когда вы открываете файл, подобный следующему:

Как видите, ответ на этот запрос содержит токенизированную структуру файла Swift. С его помощью мы можем определить, что этот файл объявляет TestPrivate, что TestPrivate - это final private class, что final class - это встроенные ключевые слова Swift, где они были определены, длина каждого блока кода и так далее. Это то, что Xcode использует для придания ключевым словам особого цвета перед тем, как ваш файл будет правильно проиндексирован, и это самый простой запрос, который он имеет!

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

Создание проекта с помощью SourceKit

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

Для начала загрузите этот образец проекта Swift Package Manager на базе SourceKit, который содержит все необходимое для начала работы.

Проект содержит две цели: Csourcekitd и MyProject, где находится сам проект. Причина, по которой нам нужно определить цель для SourceKit, заключается в том, что, хотя у нас есть доступ к SourceKit в цепочке инструментов, мы не знаем как его называть. У этой цели есть файл заголовка, который содержит все функции, поддерживаемые SourceKit, наряду с минимальной настройкой, необходимой для абстрагирования библиотеки C в модуль Swift.

Кроме того, MyProject содержит абстракции Swift, которые могут обрабатывать инициализацию и использование SourceKit. Он определяет, например, структуры данных, которые вам необходимо передать, константы для каждой соответствующей строки в SourceKit (запросы, ключи и значения) и небольшие утилиты для обработки этих типов. Если вам интересно, откуда все это взялось, все эти файлы взяты из самого Swift! Абстракции были специально взяты из SourceKit-LSP, который представляет собой Swift-абстракцию SourceKit для IDE, поддерживающих протоколы языкового сервера. Прежде чем продолжить, я рекомендую вам быстро взглянуть на содержимое файлов в примере проекта, чтобы вы могли понять, почему они там есть.

Извлечение функций и создание шаблона

Если мы предположим, что целью этого инструмента является чтение исходных текстов проекта, поиск Feature определений и дамп где-нибудь сгенерированный код, мы могли бы определить что-то вроде этого:

(Я предполагаю, что вы знаете, как предоставить массив files, но если вы этого не сделаете, вы можете искать файлы .swift с помощью FileManager или получать ввод напрямую с помощью swift-argument-parser. Для этого учебник, вы также можете просто жестко закодировать строку пути к файлу.)

Короче говоря, цель этого кода - перебрать массив файлов, найти Feature объявлений в каждом из них и передать этот результат другому методу.

Чтобы реализовать findFeature(inFile:), давайте начнем с определения editor_open запроса к SourceKit:

Оба объекта запроса и ответа - CustomStringConvertible, поэтому их распечатка покажет вам все подробности. Если вы запустите это для данного файла, вы увидите результат, аналогичный тому, который я показал выше. Если вам интересно, откуда я знаю, какие аргументы передать запросу, то это потому, что я посмотрел, как Xcode вызывает его, используя упомянутые выше советы. К сожалению, я не думаю, что для этих запросов существует какая-либо фактическая документация, кроме проверки журналов Xcode, но если вы попытаетесь выполнить запрос, в котором чего-то не хватает, SourceKit сообщит вам.

Короче говоря, нам нужно пройти по токенам файла и определить, содержит ли он один или несколько enums, унаследованных от протокола Feature. Чтобы понять, как это сделать, нужно передать SourceKit файл, который соответствует нашим потребностям, и посмотреть, что SourceKit отвечает следующим образом:

Из этого ответа мы можем определить, что нам необходимо:

  • Итерировать key.substructure (рекурсивно, потому что объявление может быть глубже по структуре)
  • Убедитесь, что kind source.lang.swift.decl.enum
  • Убедитесь, что inheritedtypes содержит Feature
  • Если да, запишите название функции.

К счастью для нас, в Swift-абстракции SourceKit есть несколько методов, которые могут помочь нам с вышеуказанным. Вот как мы можем выполнить итерацию key.substructure и проверить, представляет ли элемент объявление перечисления:

var features = [String]()
response.recurse(uid: keys.substructure) { dict in
    let kind: SKUID? = dict[keys.kind]
    guard kind?.uid == values.decl_enum else {
        return
    }
}

После этого мы можем проверить inheritedtypes, прочитав его значение как SKResponseArray:

guard let inheritedtypes: SKResponseArray = dict[keys.inheritedtypes] else {
    return
}
for inheritance in (0..<inheritedtypes.count).map({ inheritedtypes.get($0) }) {
}

И из каждого элемента в массиве мы можем проверить, является ли его имя Feature, и добавить его в наш массив ответов, если он истинен. Вот как выглядит полный код:

Отсюда вы, вероятно, могли бы экспортировать этот результат и использовать Sourcery для генерации фактического кода, но поскольку мы уже создали для этого проект, я думаю, что проще просто сгенерировать код самостоятельно:

В этом примере мы генерируем registerFeatures метод, который содержит весь регистрационный код внутри него. Мы могли бы заменить наш текущий код вызовом этого метода и настроить сценарий, который запускал бы этот инструмент каждый раз при добавлении новой службы.

А как насчет более сложных случаев?

Вы могли заметить одну вещь: этот код не принимает во внимание модуль файла, поэтому это не будет работать для приложений с несколькими модулями, и мы также не делаем никакого кеширования. Однако это проблемы, которые вы можете решить без прямого использования SourceKit, поэтому мы не будем вдаваться в их подробности здесь. Он должен показать вам, насколько сложно это сделать правильно и почему это, вероятно, должно быть проблемой только для приложений в очень продвинутых состояниях.

Что касается более продвинутого использования самого SourceKit, один из моих любимых запросов - это запрос index_source, который предоставляет вам «проиндексированную» версию вашего файла. Это похоже на запрос editor_open, но вместо того, чтобы просто печатать вам имена токенов, он показывает вам символ каждой ссылки.

Например, если вы просматриваете ссылку на тип, например let type: MyType, SourceKit сообщит вам, к какому модулю принадлежит MyType, имя файла, в котором он определен, конкретную строку / столбец, в которой объявлен тип, и многое другое. Это то, что обеспечивает функцию «перехода к определению» в Xcode, и именно поэтому цвет ваших файлов через некоторое время меняется. Xcode завершил его индексацию и теперь знает, откуда берется каждая ссылка.

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

Если вы хотите увидеть пример инструмента, который действительно взялся за это дело, посмотрите swiftshield. Это инструмент, который скрывает файлы Swift, и он полностью основан на запросе SourceKit index_sources. Я также считаю, что sourcekit-lsp - хорошая идея, так как он содержит реализации множества различных запросов SourceKit, на которые вы можете черпать вдохновение.

Первоначально опубликовано на http://swiftrocks.com.