Как стать автором
Обновить
19
0
Алмаз Ибрагимов @almazrafi

iOS разработчик

Отправить сообщение

Сейчас появляется  всё больше кейсов использования КММ в мобильных приложениях в крупных компаниях:

- QIWI

- HH

Здесь "HH" - это hh.ru?

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

Да, дерево навигации редко полностью перестраивается, но 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` все же.

Спасибо, опечатка в статье, поправим.

Мы переехали с 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 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` и использовать по такому же принципу, но тогда гарантировать уникальность ключей придется самостоятельно.
Спасибо! Мы уже думаем над подробным выводом всех шагов, обязательно добавим в ближайшие релизы.

Информация

В рейтинге
Не участвует
Откуда
Москва, Москва и Московская обл., Россия
Работает в
Зарегистрирован
Активность