Готов ли SwiftUI к запуску?

Apple недавно выпустила iOS 14, а это значит, что SwiftUI уже имеет необходимый годовой буфер для внедрения не только энтузиастами в их любимых проектах, но и корпоративными командами в своих бизнес-приложениях.

Буквально все говорят, что писать код SwiftUI - это весело, но SwiftUI - это игрушка или профессиональный инструмент? Если мы хотим относиться к нему серьезно, мы должны рассматривать его стабильность и гибкость как инструмент, а не как игрушку.

Когда самое подходящее время начать использовать SwiftUI в производственном коде?

На этот вопрос довольно сложно ответить, если вы начинаете новый крупный проект между 2020 и, возможно, 2022 годом!

Несмотря на все инновации, которые принес SwiftUI, даже в iOS 14, у нас все еще есть ошибки и отсутствие гибкости для настройки.

Хотя это можно смягчить, ситуативно обращаясь к UIKit, можете ли вы оценить, сколько кода в конечном итоге будет написано на UIKit? Может ли SwiftUI стать обузой в долгосрочной перспективе, если вам лучше просто писать все в UIKit?

Мы можем сделать ставку только на iOS 15, чтобы не было проблем со SwiftUI. Это означает, что только к 2022 году (с выходом iOS 16) у нас будет идеальный момент, чтобы расслабиться и полностью довериться SwiftUI.

В этой статье я подробно расскажу, как структурировать проект по двум сценариям:

  1. Вы поддерживаете iOS 11 или 12, но в обозримом будущем рассмотрите возможность переноса приложения на SwiftUI.
  2. Вы поддерживаете iOS 13+, но хотите контролировать риски, связанные со SwiftUI, и иметь возможность беспрепятственно вернуться к UIKit.

Прикрепленные рамки пользовательского интерфейса

Исторически сложилось так, что UI-фреймворки занимали центральное место в архитектуре мобильных приложений. Мы взяли UIKit и построили все вокруг него.

Рассмотрим свой последний проект UIKit и попытаемся оценить, сколько усилий потребуется, чтобы полностью избавиться от UIKit и заменить его другим фреймворком пользовательского интерфейса, например, AsyncDisplayKit?

Для большинства проектов это означало бы полную переписывание.

Веб-инженеры смеялись бы над нами, потому что у них всегда было множество UI-фреймворков. Так они научились применять «правило зависимости» и рассматривать пользовательский интерфейс как периферийную «деталь» в системе, точно так же, как конкретный тип базы данных, которую они используют.

Означает ли это, что мы, разработчики мобильных приложений, не отделили пользовательский интерфейс от бизнес-логики? Мы сделали, правда? MVC, MVVM, VIPER и т. Д. Были здесь, чтобы помочь нам, но мы все равно попали в ловушку.

Мобильные приложения редко отвечают за какую-либо основную бизнес-логику, например за расчет процентов по ссуде и ее утверждение. Компании хотят минимизировать здесь многочисленные риски, поэтому они запускают такую ​​логику на сервере.

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

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

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

API-интерфейсы как UIKit, так и SwiftUI привязаны к базе кода. Эти фреймворки подталкивают разработчиков к тому, чтобы сделать их суперцентральными, привязанными ко всему, используемыми непосредственно в местах, которые вообще не относятся к пользовательскому интерфейсу!

Возьмем, например, @FetchRequest в SwiftUI. Он смешивает CoreData детали модели прямо на уровне представления. Смотрится удобно. Но в то же время это серьезное нарушение множества принципов проектирования программного обеспечения и лучших практик в CS. Такой код экономит время в краткосрочной перспективе, но может нанести значительный ущерб проекту в долгосрочной перспективе.

Как насчет @AppStorage? Операции ввода-вывода данных прямо на уровне пользовательского интерфейса. Как это проверить? Можете ли вы легко определить конфликты имен ключей в контейнере? Можете ли вы легко перенести его на другой тип хранилища данных, например на Связку ключей?

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

А что с маршрутизацией экрана?

UIKit всегда шептал нам: «Пшш! Просто используйте presentViewController(:, animated:, completion:); не связывайся с этими мерзкими координаторами!

SwiftUI, с другой стороны, не шепчет. Он кричит на нас: «Слушайте сюда. Либо ты сделаешь это так, как я хочу, либо я убью твою семью самым изощренным способом! » (*)

Есть ли способ защитить нашу кодовую базу от этих варварских API?

Безусловно!

(*) Кричащие API обычно полезны - у программиста меньше шансов ошибиться. Однако такие API-интерфейсы становятся огромной проблемой, когда они не работают должным образом, как, например, в случае с проблемной программной навигацией SwiftUI.

Удаление уровня пользовательского интерфейса

Как видите, у фреймворков везде есть ловушки.

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

Если мы хотим, чтобы система была достаточно прочной, чтобы пережить переход от UIKit к SwiftUI (или наоборот), нам нужно убедиться, что граница между пользовательским интерфейсом и остальной системой - это не деревянный забор, а Великая стена!

Ничего не должно подкрасться, даже форматирование строк.

Можете ли вы преобразовать число с плавающей запятой 5434,35 в «5 434,35 доллара США» без UIKit или SwiftUI? Отлично - давайте сделаем это в другом месте!

Налагает ли API фреймворка для маршрутизации экрана тесную связь представления? Нам нужно ввести границу для их изоляции.

Нам нужно не только извлечь как можно больше логики из уровня пользовательского интерфейса, но также сделать так, чтобы компонент UIKit и его аналог SwiftUI были полностью совместимы с сокетом, к которому они подключены.

Как привести UIKit и SwiftUI к общему знаменателю?

Мы знаем, что SwiftUI полностью управляется данными и основан на реактивных привязках данных. К счастью, UIKit можно изменить таким образом с помощью MVVM и реактивных фреймворков.

Это означает, что источники данных, делегаты, целевые действия и остальные API-интерфейсы UIKit должны быть изолированы на уровне пользовательского интерфейса.

Строка import UIKit не должна появляться ни в одной ViewModel и за ее пределами.

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

Теперь. Какую реактивную структуру мы должны использовать для ViewModel? Мы знаем, что SwiftUI работает исключительно с Combine, а UIKit лучше всего поддерживается RxCocoa.

Возможен любой способ, поэтому это зависит от того, можете ли вы поддерживать iOS 13 (Combine) и насколько вы любите RxSwift.

Давайте рассмотрим оба!

Мост между RxSwift и SwiftUI

Combine доступен в iOS 13, что является препятствием для тех, кому все еще требуется поддержка iOS 11 или 12.

Здесь я расскажу о простом способе перехода (UIKit + RxSwift) на (SwiftUI + RxSwift).

Рассмотрим эту минимальную настройку:

Представление управляется данными: модель представления полностью контролирует изменения состояния представления.

Давайте перенесем этот экран в SwiftUI, не касаясь кода ViewModel.

Есть два способа заставить его работать:

  1. Определите новый ObservableObject с @Published переменными, привязанными к Driver (или Observable) из исходной ViewModel.
  2. Адаптируйте каждый Driver к Publisher и привяжите к @State внутри представления SwiftUI.

Привязка наблюдаемого к @Published

Для первого подхода нам нужно создать новую ObservableObject, которая отражает все наблюдаемые переменные из исходной ViewModel:

Код привязки значений между исходной ViewModel и адаптером должен быть максимально кратким. Вот как мост будет выглядеть в случае Driver и Observable:

Все, что нам здесь нужно, это Binder от RxSwift, который присваивает значения конкретному @Publishedvalue. Вот отрывок для функции binder, которая выполняет соединение:

Возвращаясь к нашей ViewModel, вы можете выполнить привязку прямо при инициализации Adapter:

Недостатком этого подхода является шаблонный код, который приходится повторять для каждой @Published переменной.

Привязка наблюдаемого к @State

Этот второй подход требует гораздо меньше кода настройки и основан на другом способе, которым представления SwiftUI могут использовать внешнее состояние: onReceive модификатор представления с присвоением значений локальным @State.

Прелесть здесь в том, что мы можем использовать исходную ViewModel непосредственно в представлении SwiftUI:

viewModel.isLoadingData - это Driver, поэтому нам нужно преобразовать его в Publisher из Combine.

Сообщество уже разработало библиотеку RxCombine, которая позволяет выполнять переход от Observable к Publisher, поэтому расширить ее для поддержки Driver очень просто:

Подключение UIKit к Combine

Если у вас есть возможность поддерживать iOS 13+, вы можете рассмотреть возможность использования Combine для создания сетевых и других модулей, не связанных с пользовательским интерфейсом, в приложении.

Несмотря на то, что связывание Combine с UIKit несколько неудобно, выбор Combine в качестве базовой структуры, управляющей данными в вашем приложении, должен окупиться в долгосрочной перспективе, когда ваш проект полностью перейдет на SwiftUI.

А пока вы можете обновить представление UIKit внутри функции sink:

Или вы можете использовать вышеупомянутую библиотеку RxCombine для преобразования Publisher в Observable, в полной мере используя привязки данных, доступные в RxCocoa:

Я должен отметить, что если мы выберем Combine в качестве основного реактивного фреймворка в приложении, использование RxSwift, RxCocoa и RxCombine должно быть ограничено только привязками данных к представлениям UIKit, чтобы мы могли легко избавиться от этих зависимостей вместе с последний просмотр UIKit в приложении.

ViewModel в этом случае должен быть построен только с помощью Combine (не import RxSwift!).

Возвращаясь к исходному примеру:

И когда придет время перестраивать этот экран в SwiftUI, все будет настроено для вас: ничего не нужно менять в ViewModel.

Мысли о маршрутизации

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

Со временем это наверняка будет исправлено, но на данный момент я бы не стал доверять SwiftUI маршрутизацию.

Отказавшись от маршрутизации SwiftUI, мы мало что теряем. Пока SwiftUI поддерживается UIKit, не будет положительной разницы в производительности по сравнению с тем, что мы можем достичь с помощью UIKit.

В образце проекта, который я создал для этой статьи, я использовал традиционный шаблон координатора (MVVM-R), который отлично работал для экранов, созданных с использованием UIHostingController из SwiftUI.

Заключение

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

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

Извлеките как можно больше бизнес-логики из слоя пользовательского интерфейса и сделайте свои экраны UIKit управляемыми данными. Таким образом, перейти на SwiftUI будет несложно.

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

Есть две цели: одна работает на UIKit, другая - на SwiftUI, и обе совместно используют существенную часть кодовой базы.