Лучшие практики в композиции 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.

Завершение блока

Так что ты думаешь? В тему? Не согласны? Я что-то упускаю?

Как всегда, дайте мне знать в комментариях ниже.