Site icon AppTractor

Как я создал собственную дизайн-систему для компонентов iOS-приложения

Если вы создаете свое приложение или работаете в компании, где ваша команда еще не перенесла дизайн компонентов пользовательского интерфейса в отдельную библиотеку, пришло время сделать этот шаг. В этой статье я расскажу вам о своем подходе к созданию многократно используемых компонентов с помощью SwiftUI. Я пропущу базовую настройку библиотеки в Xcode (этому посвящено множество руководств) и сосредоточусь на том, что действительно важно: на создании надежной системы компонентов.

Почему стоит использовать библиотеки компонентов?

Позвольте мне рассказать, почему я такой большой сторонник библиотек компонентов. Во-первых, они значительно снижают нагрузку на ваш основной проект — мы говорим о более быстром времени сборки и чистых кодовых базах. Когда вам понадобится изменить дизайн (а поверьте, вам придется это делать), наличие компонентов, изолированных в отдельном проекте, убережет вас от случайного нарушения бизнес-логики.

Вот основные принципы, которых я придерживался:

Начало работы: основа

Прежде чем погрузиться в сложные компоненты, мне нужен прочный фундамент. Давайте начнем с управления цветом:

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 и ознакомиться с более подробной информацией на сайте библиотеки.

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

Источник

Exit mobile version