Если вы создаете свое приложение или работаете в компании, где ваша команда еще не перенесла дизайн компонентов пользовательского интерфейса в отдельную библиотеку, пришло время сделать этот шаг. В этой статье я расскажу вам о своем подходе к созданию многократно используемых компонентов с помощью SwiftUI. Я пропущу базовую настройку библиотеки в Xcode (этому посвящено множество руководств) и сосредоточусь на том, что действительно важно: на создании надежной системы компонентов.
Почему стоит использовать библиотеки компонентов?
Позвольте мне рассказать, почему я такой большой сторонник библиотек компонентов. Во-первых, они значительно снижают нагрузку на ваш основной проект — мы говорим о более быстром времени сборки и чистых кодовых базах. Когда вам понадобится изменить дизайн (а поверьте, вам придется это делать), наличие компонентов, изолированных в отдельном проекте, убережет вас от случайного нарушения бизнес-логики.
Вот основные принципы, которых я придерживался:
- Сохраняйте внешнюю логику для UI — состояния компонентов должны контролироваться внешними переменными. Например, состояние тумблера должно находиться вне компонента, а не внутри него.
- Максимизируйте настраиваемость — я сделал компоненты легко настраиваемыми с помощью переменных, а не сложной внутренней логики. Такой подход сэкономил мне бесчисленное количество часов при адаптации компонентов для различных случаев использования.
- Используйте атомарный дизайн — подумайте о кирпичиках LEGO. Я собираю сложные компоненты из более мелких, независимых частей. Это делает обслуживание и обновление гораздо более удобным.
Начало работы: основа
Прежде чем погрузиться в сложные компоненты, мне нужен прочный фундамент. Давайте начнем с управления цветом:
import SwiftUI import UIKit public extension Color { static var primaryGreen : Color { Color(uiColor: UIColor(red: 0.169, green: 0.38, blue: 0.451, alpha: 1.00)) } // #2b6173 static var darkGrey : Color { Color(uiColor: UIColor(red: 0.36, green: 0.38, blue: 0.4, alpha: 1)) } // #5D6166 static var border : Color { Color(uiColor: UIColor(red: 0.871, green: 0.878, blue: 0.89, alpha: 1)) } // #dee0e3 static var secondaryYellow : Color { Color(uiColor: UIColor(red: 0.91, green: 0.96, blue: 0.41, alpha: 1.00)) } // #E8F569 static var backgroundGrey : Color { Color(uiColor: UIColor(red: 0.98, green: 0.98, blue: 0.98, alpha: 1)) } // #fafafa static var alertRedOpacity: Color { Color(red: 0.996, green: 0.945, blue: 0.945) } // #FEF2F1 static var alertRed: Color { Color(red: 0.792, green: 0.11, blue: 0.2) } // #CA1C33 static var alertDarkRed: Color { Color(red: 0.345, green: 0, blue: 0.008) } // #580002 static var alertGreenOpacity: Color { Color(red: 0.882, green: 0.992, blue: 0.957) } // #e1fdf4 static var alertDarkGreen: Color { Color(red: 0, green: 0.314, blue: 0.145) } // #005025 static var alertGreen: Color { Color(red: 0.016, green: 0.69, blue: 0.435) } // #04b06f }
Для более быстрой генерации цветов я использовал этот HEX-конвертер.
В качестве базового элемента я создал компонент EXBase
. Это мой основной компонент, когда мне нужен постоянный фон и отступы для разных элементов:
public struct EXBase<Content: View>: View { let content: () -> Content public init(@ViewBuilder content: @escaping () -> Content) { self.content = content } public var body: some View { content() .padding(12) .background(Color.backgroundGrey) .cornerRadius(8) } }
Разбор реального примера: EXInfoCard
Давайте рассмотрим нечто практичное — компонент EXInfoCard
. Вы наверняка видели этот паттерн бесчисленное количество раз в мобильных приложениях: информационная карточка с иконкой, заголовком, текстом и иногда кнопкой. Целью здесь была максимальная гибкость при полной независимости от бизнес-логики.
Вот как я его структурировал:
public struct EXInfoCard: View { var title: String var text: String var icon: IconType? var isButton: Bool var buttonIcon: Image? var buttonText: String? = nil var buttonAction: (() -> Void)? public init( title: String, text: String, icon: IconType? = nil, isButton: Bool = false, buttonIcon: Image? = nil, buttonText: String? = nil, buttonAction: (() -> Void)? = nil ) { self.title = title self.text = text self.icon = icon self.isButton = isButton self.buttonIcon = buttonIcon self.buttonText = buttonText self.buttonAction = buttonAction } public var body: some View { EXBase { VStack { VStack(alignment: .leading, spacing: 5) { if let icon = icon { switch icon { case .imageName(let imageName) where !imageName.isEmpty: Text(imageName) case .image(let image): image .foregroundColor(.primaryGreen) default: EmptyView() } } Text(title) .font(.system(.headline, weight: .semibold)) Text(text) .font(.system(.subheadline, weight: .regular)) .foregroundColor(.darkGrey) } .frame(maxWidth: .infinity, alignment: .leading) if isButton, let buttonIcon = buttonIcon, let buttonText = buttonText, let buttonAction = buttonAction { Button(action: { buttonAction() }) { HStack { Text(buttonText) .font(.system(.subheadline, weight: .semibold)) buttonIcon } .frame(maxWidth: .infinity) } .buttonStyle(EXPrimaryButtonStyle(showLoader: .constant(false))) .padding(.top, 5) } } } } }
Обратите внимание, что компонент не делает никаких предположений о том, когда показывать или скрывать элементы. Вместо этого он реагирует на внешние переменные состояния. Например, видимость кнопки контролируется параметром isButton
, который вы можете привязать к любой @State
переменной в ваших представлениях:
@State private var condition: Bool = false EXInfoCard( title: "Earn Rewards", text: "Learn how our points system works", icon: .imageName("⭐"), isButton: condition, buttonIcon: Image(systemName: "arrow.right"), buttonText: "Learn More", buttonAction: {} ) // Condition logic definition
Основные выводы
Создание этой библиотеки преподало мне ценные уроки по созданию действительно многократно используемых компонентов пользовательского интерфейса:
- Думайте системами, а не экранами — Каждый компонент должен работать везде, а не только в вашем текущем сценарии использования.
- Делайте конфигурации общедоступными — Использование общедоступных перечислений и структур означает лучшую возможность повторного использования в разных проектах.
- Сохраняйте широкие, но значимые возможности настройки — Соблюдайте баланс между гибкостью и сложностью.
Я сделал эту библиотеку с открытым исходным кодом, так что не стесняйтесь использовать ее в своем следующем проекте или в качестве шаблона для своей собственной системы. Вы можете найти ее на GitHub и ознакомиться с более подробной информацией на сайте библиотеки.
Помните, что хорошая библиотека компонентов развивается вместе с вашими потребностями. Начните с малого, сосредоточьтесь на возможности повторного использования и продолжайте совершенствоваться по мере продвижения. Поверьте, в будущем вы будете благодарны себе за то, что уделили время созданию этой основы.