Лучшие практики в композиции SwiftUI
Некоторые мысли о составе представления SwiftUI, удобочитаемости кода и производительности приложения
SwiftUI изменит правила игры в том, как мы создаем будущие приложения для iOS, iPadOS, macOS, tvOS и watchOS.
Но полное влияние SwiftUI заключается не только в устранении UIKit и замене представлений на UIViews или списков на UITableViews, или даже в полном устранении необходимости в тысячах привязок UIConstraints и UIView.
И не в резком упрощении, которого наши приложения смогут достичь, исключив Раскадровки, IBOutlets, IBActions, Segues и все другие связанные шаблоны, которые так долго сковывали наши приложения. Не говоря уже об устранении ошибок, которые часто прячутся глубоко внутри вашего кода из-за случайно отключенных розеток и действий.
Не поймите меня неправильно. SwiftUI предоставит нам все из этих преимуществ. И более. Но с моей точки зрения, основное влияние SwiftUI будет заключаться не в том, как мы создаем интерфейсы нашего приложения ...
Но в том, как мы проектируем наши приложения.
SwiftUI View Best Practices
Мне есть что сказать о моделях просмотра и управлении состоянием просмотра. На самом деле более чем достаточно для нескольких статей.
Но сегодня я собираюсь сосредоточиться на том, что я считаю передовыми методами кодирования наших пользовательских интерфейсов, а также наших представлений и иерархий представлений.
- Просмотреть композицию
- Сосредоточьтесь на функциональности, а не на внешнем виде
- Использовать семантический цвет
- Архитектор, думая о других платформах
- Позвольте системе сделать это
- Свяжите состояние как можно ниже в иерархии
Готовый? Давайте копаться ...
Посмотреть композицию
Если что-то одно и повторялось снова и снова во время сеансов SwiftUI на WWDC, так это то, что представления SwiftUI чрезвычайно легкие и их создание практически не снижает производительности.
В отличие от UIViews в UIKit, большинство представлений SwiftUI существуют как структуры Swift и создаются, передаются и упоминаются как параметры значения. Хотя это может иметь некоторые нежелательные разветвления на данный момент, использование структур позволяет избежать множества выделений памяти и создания множества сильно подклассифицированных и динамических UIViews на основе UIKit с передачей сообщений.
Кроме того, и опять же, в отличие от UIView, параметры и модификаторы во вложенном представлении SwiftUI объединяются в единый объект во время циклов макета и отображения. Более того, узлы в дереве представления отслеживаются на предмет изменений состояния и, если они не изменились, обычно не нуждаются в повторной визуализации.
С другой стороны, каждый UIView выделяется и существует как связанный подпредставление где-то в дереве рендеринга макета и является активной частью процесса макета.
Все это означает, что в SwiftUI ваше явное преимущество - создавать столько различных и специализированных представлений, сколько может потребоваться вашему приложению.
struct FootnoteText : View { let text: String var body: some View { MultiLineText(text: text, alignment: .center) .font(.footnote) } } struct MultiLineText: View { var text: String = "" var alignment: HAlignment = .leading var body: some View { Text(text) .lineLimit(nil) .multilineTextAlignment(alignment) } }
В приведенном выше примере представление MultiLineText, вероятно, будет довольно часто использоваться во всем приложении. Представление сноски - это просто специализированный текст MultiLineText с определенным модификатором шрифта.
Таким образом, в приложении вы можете просто ссылаться на…
FootnoteText(text: $model.disclamer)
… По мере необходимости, а не разбрасывать по вашему коду следующее:
Text($model.disclamer) .lineLimit(nil) .multilineTextAlignment(.center) .font(.footnote)
Правильно названное представление, такое как FootnoteText more , официально объявляет о ваших намерениях всем, кто позже придет и прочитает ваш код.
Маленькие, содержательные представления также легче рассуждать, и они с гораздо меньшей вероятностью содержат ошибки и непреднамеренные побочные эффекты.
Возьмите страницу из учебника Smalltalk, где многие функции, по-видимому, состоят из одной или двух строк кода, прежде чем делегировать функциональность другой функции… которая, в свою очередь, делает то же самое.
Основная философия дизайна UIKit - это наследование.
SwiftUI - это композиция.
Сосредоточьтесь на функциональности, а не на внешнем виде
Всплывающая викторина. Какого цвета текст в следующем коде?
Text($model.disclamer) .foregroundColor(.red) .foregroundColor(.green)
(На заднем фоне звучит музыка Jeopardy…)
Отвечать? Текст красный. В этом случае к просмотру привязано ближайшее значение.
Так что это значит? Что ж, у вас может возникнуть соблазн указать текст сноски в первом примере как:
struct FootnoteText : View { let text: String var body: some View { MultiLineText(text: text, alignment: .center) .foregroundColor(.gray) .font(.footnote) } }
Если вы указали серый цвет, потому что это тот цвет, который вам нужен до сих пор. Но проблема в том, что теперь вы застряли в этом и пытаетесь…
FootnoteText(text: $model.disclamer) .foregroundColor(.red)
Текст по-прежнему отображается серым. В нашем примере сейчас много других проблем, например, мы, вероятно, испортили автоматическую адаптацию темного режима, указав конкретное значение цвета одного текста, и мы также, вероятно, испортили возможность использования нашего представления FootnoteText на других платформах с другим внешним видом. и цветовые схемы.
Так что воздержитесь от чисто визуальных модификаторов внешнего вида в ваших компонентах представления. Особенно цвет.
Использовать семантический цвет
Но если вам необходимо установить цвета, настоятельно рекомендуется использовать семантические цвета.
Color.primary
, Color.secondary
и Color.accentColor
- все это просто примеры цветов, предоставляемых системой и средой. Даже такой цвет, как .orange
, может и будет правильно адаптироваться к светлому и темному режимам, немного изменяясь в процессе.
Вы также можете определить свои собственные семантические цвета в Xcode. И, как и Apple, вы даже можете настроить их для светлого и темного режима.
Хорошая вещь в этом подходе заключается в том, что вы можете легко изменить свои цветовые схемы и брендинг для всего приложения в одном месте. Вы даже можете изменить свои схемы для каждой платформы, не меняя код, просто указав другой набор цветов в каталоге xcassets этой платформы.
Говоря о которых…
Архитектор, думая о других платформах
С SwiftUI проще, чем когда-либо, взять один и тот же код и использовать его на разных платформах. В некоторой степени вы могли бы сделать это до того, как перенести свои модели, код API и бизнес-логику с платформы на платформу, но теперь вполне возможно использовать многие элементы пользовательского интерфейса и на разных платформах.
Это было ясно видно из презентации Apple SwiftUI on All Devices во время WWDC.
Я не буду вдаваться в подробности, так как презентация так хорошо освещает это, но имейте в виду, что большая часть межплатформенного обмена пользовательским интерфейсом между приложениями iOS и приложениями iPadOS, а также приложениями macOS и приложениями tvOS происходит из-за подключения одного и того же просмотры контента, созданные вами на одной платформе, в другую структуру навигации на другой платформе.
Итак, снова подумайте, где вы можете указать такие вещи, как шрифты, цвета и тому подобное. SwiftUI имеет несколько механизмов для предоставления или задержки этих спецификаций.
Мы обсуждали семантические цвета выше, но вот еще один пример.
Group { MyCustomTextField($model.username) MyCustomTextField($model.password) } .font(.headline) .background(Color.white.opacity(0.5)) .relativeWidth(1)
Группировка - это мощный инструмент, который позволяет вам хорошо группировать вещи, которыми нужно управлять, вместе или, в данном случае, имеющие общие атрибуты.
В этом примере модификаторы группы применяются к каждому MyCustomTextField
,, и поэтому каждый из них выбирает шрифт заголовка, цвет фона и его размер соответствует полной ширине родительского контейнера.
MyCustomTextField
занимается функциональностью. Пусть контекст имеет дело со стилем.
Позвольте системе делать то, что нужно
Также помните, что SwiftUI автоматически переводит представления в элементы визуального интерфейса, подходящие для данной платформы. Например, представление Toogle отображается по-разному - и правильно - на iOS, macOS, tvOS и watchOS.
Кроме того, SwiftUI будет правильно настраивать цвета, интервалы, отступы и тому подобное в зависимости от платформы, на текущем экране и / или размере контейнера, в состоянии управления. Он также будет принимать во внимание любые изменения, необходимые для любых функций доступности, которые могут быть включены, делать правильные вещи для светлого / темного режима и многое другое.
Чтобы выбрать один случай наугад, заполнение абзаца текстом в iOS представляет собой один способ, но, как правило, имеет больше границ на экране iPad ... если только ваш контент теперь не сжимается, отображаясь в режиме слайд-контроля. Множество соображений, которые, честно говоря, мы не всегда принимаем во внимание.
В UIKit мы, вероятно, просто вставили значение 15 в поле ограничения на раскадровке и двинулись дальше.
Для нас здесь и сейчас это означает, что нам нужно расслабиться и позволить системе делать свое дело. Избегайте одержимых попыток сопоставить макеты дизайна с идеальным пикселем, которые часто создаются для одного макета «наилучшего случая» на экране определенного размера для режима специальных возможностей по умолчанию.
Возьмите пример с веб-разработчиков, которые перешли от жестких макетов дизайна к очень гибким макетам, хорошо подходящим для каждой платформы и потребностям отдельного пользователя.
Это может означать несколько разговоров с вашей командой дизайнеров UI / UX.
Apple приложила много усилий, чтобы убедиться, что SwiftUI делает правильные вещи в нужное время, при условии, что мы не толкаем его локтем. Так что не надо.
И я хочу здесь внести ясность. Я не говорю, что ты тоже не можешь. Apple предоставила нам полный контроль над презентацией. Нам просто нужно знать, когда отпустить, и сохранить особые случаи для очень особых случаев.
Позвольте системе сделать это, и вы не только напишете меньше кода, но я готов поспорить, что у вас также будет меньше ошибок.
Свяжите состояние как можно ниже в иерархии
Как упоминалось ранее, я собираюсь сохранить большую часть своих мыслей о моделях представлений и управлении состоянием для последующих статей, но эта концепция согласуется с примерами, показанными до сих пор, поэтому я завершу этим.
Обратите внимание на наш верный FootnoteText
view в следующем коде.
struct MyMainView : View { @ObservedObject var model: MainViewModel @EnvironmentObject var settings: UserSettings var body: some View { VStack { MainContentView(model) MainContentButtons(model) FootnoteText(text: settings.fullVersionString) } } }
Обратите внимание, что MyMainView
автоматически импортирует объект среды с именем settings и передает settings.fullVersionString
нашему FootnoteText
view.
Все хорошо ... но почему MyMainView
знает о UserSettings
вообще? Что, если бы мы сделали следующее?
struct MyMainView : View { @ObservedObject var model: MainViewModel var body: some View { VStack { MainContentView(model) MainContentButtons(model) ApplicationVersionFootnote() } } }
И определил ApplicationVersionFootnote
elsewhere как…
struct ApplicationVersionFootnote : View { @EnvironmentObject var settings: UserSettings var body: some View { FootnoteText(text: settings.fullVersionString) } }
Здесь наша переменная среды приобретается и используется ниже в иерархии представлений. MyMainView
ничего не знает о UserSettings
, да это и не должно его волновать. Да и MainViewModel
, если на то пошло.
Последний момент является ключевым. В течение некоторого времени мы пытались избежать синдрома массивного представления-контроллера, используя некоторую форму структуры модели представления, будь то MVVM, VIPER или что-то еще.
Однако, если мы не проявим особой осторожности, все, что нам удалось сделать, - это заменить наши контроллеры массивного представления на модели с массивным представлением.
В более традиционной реализации MVVM наш UserSettings
object, вероятно, будет внедрен в наш MainViewModel
, а некоторая функция, переменная или привязка будут созданы для предоставления fullVersionString
в нашей модели представления. Это усложняет нашу модель представления, усложняет наши стратегии внедрения и код инициализации, а также способствует тому, что компоненты нашего приложения становятся более тесно связанными и жесткими.
Но в нашем последнем примере наше ApplicationVersionFootnote
view фактически представляет собой небольшое, узкоспециализированное, специализированное представление модель, которое связывает нашу среду UserSettings
data с FootnoteText
view.
Я вижу большой потенциал для подобных вещей в будущем, и это очень хорошо согласуется с принципом единой ответственности SOLID.
ОБНОВЛЕНИЕ. Подробнее об этой концепции см. В моей статье Микросервисы SwiftUI.
Завершение блока
Так что ты думаешь? В тему? Не согласны? Я что-то упускаю?
Как всегда, дайте мне знать в комментариях ниже.