Подскажите, пожалуйста, а чем вызван такой высокий уровень абстракции? Кажется, что дерево контроллеров задается один раз и меняется довольно редко. А если и меняется, то все равно не целиком, а лишь переносятся целые ветки с места на место. В таком случае менять конкретный роутер не так уж и сложно. А с такой высокоуровневой абстракцией повышается и порог входа и стоимость реализации, а ради чего все? Да, вы указали Ваши требования к идеальному роутингу, но что стоит за ними? Какого причины такого усложнения?
Да, дерево навигации редко полностью перестраивается, но Nivelir и не меняет дерево полностью. Это просто DSL, который позволяет описывать сложную навигацию простыми переходами, начиная с найденного в иерархии контейнера.
Он очень полезен для глобальной навигации, например, для диплинков. В статье есть пример открытия диплинка на экран чата, который взят из нашего приложения, и его логику довольно неприятно делать стандартными средствами, получится гораздо больше сложного и плохо переиспользуемого кода, в котором очень легко допустить тяжело воспроизводимую ошибку.
Для локальной навигации модуля, в которой все ограничивается презентом и пушом, Nivelir выступает скорее как более компактная и переиспользуемая альтернатива средствам UIKit. Например, можно один раз объявить роут на модальное отображение экрана авторизации и везде его переиспользовать в минимум кода. Плюс консистентность и совместимость с глобальным роутингом.
А с такой высокоуровневой абстракцией повышается и порог входа и стоимость реализации, а ради чего все? Да, вы указали Ваши требования к идеальному роутингу, но что стоит за ними? Какого причины такого усложнения?
На самом деле, Nivelir как раз делает простым описание навигации, его легко читать и поддерживать - в этом и есть его смысл. Сложным наверняка показался из-за статьи, слишком много деталей. Причины стоят за нашими требованиями, у нас довольно сложная навигация в проекте, и мы хотели упростить ее по максимуму на стороне использования.
Думаю, с этой информации и стоило начинать статью, чтобы помочь читателю правильно выбрать инструмент.
Согласен, хотелось больше информации донести про внутреннее устройство. Но про блага использования мы еще подготовим материал, в котором постараюсь привести примеры навигации c Nivelir и без него.
Но ведь слоистая архитектура... Модель вложена в контент, вложенный в ячейку. А значит модель, не имеет права знать ни о контент-вьюхе, ни о ячейке.
А где здесь нарушение слоистой архитектуры? Обе сущности находятся в одном слое UI, модель тупая и нужна для инкапсуляции конфигурируемых полей вью и выступает ее простым контрактом. В слое бизнес-логики другие модели DTO, которые уже не знают про вью.
То же самое относится к размерам, инсетам и (О! Боже!) обработчикам действий пользователей. Даже количество линий в метке и то моделью задается. Вы опустили на уровень ниже ответственности более высокого уровня. `UIEdgeInsets` не зря в ObjC имел префикс UI. Сразу было понятно, что не стоит эту штуку опускать ниже. То же самое касается даже числа линий в метке при отображении текста. Вьюха. Вьюха эти параметры определяет. Возьмите другую вьюху с другими настройками и подставьте ее вместо текущей, и вам не придется менять модель. Совсем. И не придется писать код, проксирующий настройки из модели во вьюху. Совсем. Это ведь бесполезный код. Лишний, вредный.
Почему вью не может быть конфигурируемой количеством строк и другими параметрами? Плодить отдельные вью под каждый кейс использования - это уже овер-инжиниринг.
`UIEdgeInsets` не зря в ObjC имел префикс UI. Сразу было понятно, что не стоит эту штуку опускать ниже.
По этой логике даже картинку в UIImageView нельзя положить, она принимает UIImage, у него тоже есть префикс UI.
Именно поэтому вся ваша история и несовместима с InterfaceBuilder.
Если поднимите назад на уровень выше все вещи слоя представления, то ваша схема прекрасно будет работать даже с ячейками, сверстанными в IB. По крайней мере у нас работает.
На самом деле ничего не мешает верстать саму вью в IB, просто это будет уже менее удобно.
Для настройки вьюх поглядите паттерн декоратор. Все, что вы опустили в модельку, разместиться в декораторе, располагающемся на уровне представления.
Не совсем понимаю, что будет декорироваться в этом случае?
Если декорировать экземпляр вью, то это оверхэд, так как нам не нужно создавать вью при каждом обновлении, нам нужно задать ее параметры и скормить их data source коллекции, который уже посчитает дифф и создаст ячейку, только если это нужно.
Если декорировать тип вью, то чем это отличается от подхода из статьи?
Бонусом вы потеряете жесткую связь 1 к 1 от вьюхи к ее модели и сможете оперировать протоколами, вешая один и тот же протокол, необходимый вьюхе, на разные модельки, что даст вам большую гибкость при работе с данными и отменит жесткий меппинг из одних структур в другие.
О каком маппинге речь?
Мы умышленно пришли к связи 1 к 1, так как это уменьшает количество кода на стороне использования и дает строгую типизацию с автодополнением в Xcode.
Ну и мелочь: `ConteinerItem` -> `ContainerItem` все же.
Но до появления поддержки xcframework в Carthage исключали архитектуру arm64 для симулятора через этот файл согласно инструкции, которую, судя по статье, вы тоже используете.
Интересный опыт, спасибо за статью! Тоже меняли CocoaPods на Carthage.
Есть пара вопросов:
Вы все сторонние зависимости линкуете статически? Не рассматривали вариант изменения настроек сборки через файл xcconfig, который можно передать в переменной окружения XCODE_XCCONFIG_FILE?
XcodeGen как-то самостоятельно резолвит и включает ресурсы статических фреймворков из папки проекта в Checkouts?
Судя по указанным путям, вы пытаетесь выполнить команды не в корневой папке демо-проекта.
Я уже молчу о том, что вы пытаетесь редактировать проект перед тем, как его сгенерировать (например, tuist edit НЕ работает с голым Project.swift файлом, нужно сначала выполнить tuist init)
Актуальные версии Tuist прекрасно работают с голым Project.swift, какую версию вы используете?
tuist init - это команда создания нового проекта, в статье мы мигрируем существующий и эта команда выдаст ошибку:
Can't initialize a project in the non-empty directory at path /Users/almaz/Development/TuistDemo.
Да, статья ни в коем случае не является призывом хоронить другие практики, мы их тоже продолжаем использовать, когда это действительно удобнее.
Changeable здорово выручает нас с редактированием больших или часто изменяемых моделей, и мы решили, что сообществу этот подход тоже пригодится. Как минимум полезно для расширения кругозора и знакомства с возможностями Swift на практике реального проекта.
Имеется в виду, что экземпляр становится открытым для нежелательных изменений в будущем по неосторожности.
Например, в случае захвата экземпляров структур в замыкании фактически это происходит по ссылке, пока не будет явно указано обратное ([steveJobs] in):
func doSomething(closure: @escaping () -> Void) {
DispatchQueue.main.async {
closure()
}
}
let steve = User(id: 1, name: "Steve", age: 21)
var steveJobs = steve
steveJobs.name = "Steve Jobs"
doSomething {
print(steveJobs.age) // Распечатается 41
}
steveJobs.age = 41
Разработчики могут легко пропустить такие тонкости, ожидая захват копированием того состояния, который был до выполнения метода doSomething. В случае же немутабельных экземпляров в этом плане все безопаснее.
Да, Property Wrappers тоже помогают упростить работу с хранилищами + решают 2 первые проблемы стандартной реализации UserDefaults. Ничего не мешает добавить атрибут `@propertyWrapper` к объявлению `KeyValueContainer` и использовать по такому же принципу, но тогда гарантировать уникальность ключей придется самостоятельно.
Здесь "HH" - это hh.ru?
Да, дерево навигации редко полностью перестраивается, но Nivelir и не меняет дерево полностью. Это просто DSL, который позволяет описывать сложную навигацию простыми переходами, начиная с найденного в иерархии контейнера.
Он очень полезен для глобальной навигации, например, для диплинков. В статье есть пример открытия диплинка на экран чата, который взят из нашего приложения, и его логику довольно неприятно делать стандартными средствами, получится гораздо больше сложного и плохо переиспользуемого кода, в котором очень легко допустить тяжело воспроизводимую ошибку.
Для локальной навигации модуля, в которой все ограничивается презентом и пушом, Nivelir выступает скорее как более компактная и переиспользуемая альтернатива средствам UIKit. Например, можно один раз объявить роут на модальное отображение экрана авторизации и везде его переиспользовать в минимум кода. Плюс консистентность и совместимость с глобальным роутингом.
На самом деле, Nivelir как раз делает простым описание навигации, его легко читать и поддерживать - в этом и есть его смысл. Сложным наверняка показался из-за статьи, слишком много деталей. Причины стоят за нашими требованиями, у нас довольно сложная навигация в проекте, и мы хотели упростить ее по максимуму на стороне использования.
Согласен, хотелось больше информации донести про внутреннее устройство. Но про блага использования мы еще подготовим материал, в котором постараюсь привести примеры навигации c Nivelir и без него.
А где здесь нарушение слоистой архитектуры? Обе сущности находятся в одном слое UI, модель тупая и нужна для инкапсуляции конфигурируемых полей вью и выступает ее простым контрактом. В слое бизнес-логики другие модели DTO, которые уже не знают про вью.
Почему вью не может быть конфигурируемой количеством строк и другими параметрами? Плодить отдельные вью под каждый кейс использования - это уже овер-инжиниринг.
По этой логике даже картинку в UIImageView нельзя положить, она принимает UIImage, у него тоже есть префикс UI.
На самом деле ничего не мешает верстать саму вью в IB, просто это будет уже менее удобно.
Не совсем понимаю, что будет декорироваться в этом случае?
Если декорировать экземпляр вью, то это оверхэд, так как нам не нужно создавать вью при каждом обновлении, нам нужно задать ее параметры и скормить их data source коллекции, который уже посчитает дифф и создаст ячейку, только если это нужно.
Если декорировать тип вью, то чем это отличается от подхода из статьи?
О каком маппинге речь?
Мы умышленно пришли к связи 1 к 1, так как это уменьшает количество кода на стороне использования и дает строгую типизацию с автодополнением в Xcode.
Спасибо, опечатка в статье, поправим.
Мы переехали с CocoaPods на Carthage вместе с переездом на Tuist.
Всех тонкостей, увы, не знаю, но для примера интеграции с CocoaPods закинул отдельную ветку в репу демо-проекта: https://github.com/almazrafi/TuistDemo/tree/cocoapods
Из того, что меняется относительно SPM-варианта:
Добавлен Podfile: в нем, кроме подов и таргетов, необходимо указать пути к проектам и воркспейсу.
В файлы xcconfig добавлены импорты файлов конфигурации CocoaPods. В реальном проекте скорее всего можно было иначе разрулить.
В зависимости таргета в манифестах Tuist добавлен путь к папке с Podfile (корневая папка):
dependencies: [.cocoapods(path: .relativeToRoot("."))]
.В остальном все так же, при чем команда
tuist generate
сама вызываетpod install
.Для статической линковки не использовали.
Но до появления поддержки xcframework в Carthage исключали архитектуру arm64 для симулятора через этот файл согласно инструкции, которую, судя по статье, вы тоже используете.
Не совсем, Tuist - это как раз прокаченный Swift PM с тем же синтаксисом, но более богатой функциональностью для модуляризации.
Интересный опыт, спасибо за статью! Тоже меняли CocoaPods на Carthage.
Есть пара вопросов:
Вы все сторонние зависимости линкуете статически? Не рассматривали вариант изменения настроек сборки через файл xcconfig, который можно передать в переменной окружения XCODE_XCCONFIG_FILE?
XcodeGen как-то самостоятельно резолвит и включает ресурсы статических фреймворков из папки проекта в Checkouts?
Судя по указанным путям, вы пытаетесь выполнить команды не в корневой папке демо-проекта.
Актуальные версии Tuist прекрасно работают с голым Project.swift, какую версию вы используете?
tuist init - это команда создания нового проекта, в статье мы мигрируем существующий и эта команда выдаст ошибку:
Да, статья ни в коем случае не является призывом хоронить другие практики, мы их тоже продолжаем использовать, когда это действительно удобнее.
Changeable
здорово выручает нас с редактированием больших или часто изменяемых моделей, и мы решили, что сообществу этот подход тоже пригодится. Как минимум полезно для расширения кругозора и знакомства с возможностями Swift на практике реального проекта.Имеется в виду, что экземпляр становится открытым для нежелательных изменений в будущем по неосторожности.
Например, в случае захвата экземпляров структур в замыкании фактически это происходит по ссылке, пока не будет явно указано обратное (
[steveJobs] in
):Разработчики могут легко пропустить такие тонкости, ожидая захват копированием того состояния, который был до выполнения метода
doSomething
. В случае же немутабельных экземпляров в этом плане все безопаснее.