design systems
28 იანვარი, 2021 წელი
სანდო დიზაინ სისტემის კომპონენტები
დღეს მინდა გესაუბროთ დიზაინ სისტემაზე და მისი საბაზო კომპონენტების კონკრეტულ გადაწყვეტაზე. პირველ რიგში განვმარტოთ თუ რა არის დიზაინ სისტემა.
დიზაინ სისტემა არის პროექტის ყოვლისმომცველი სახელმძღვანელო, რომელიც აერთიანებს გარკვეულ წესებს, პრინციპებსა და საუკეთესო მიდგომებს. დიზაინ სისტემის საბაზო კომპონენტებს ძირითადად წარმოადგენს UI ელემენტები. დიზაინ სისტემას ხშირად უწოდებენ single source of truth, რომელიც მთელ გუნდს ეხმარება განავითაროს პროდუქტი და შეინარჩუნოს მაღალი ხარისხი. დიზაინ სისტემა შეიძლება გავაიგივოთ ლეგოსთან.
მეორე მნიშვნელოვანი კითხვა დიზაინ სისტემასთან მიმართებაში არის ის თუ რა სარგებლობა მოაქვს მას?
პირველ რიგში ეს არის თანმიმდევრულობა, სადაც თითოეული დიზაინის გვერდი აღქმულია როგორც გარკვეული საბაზო კომპონენტების ერთობლიობა. ასევე მარტივი ხდება ტესტირება და გარკვეული ანომალიების გამოვლენა. ასევე დიზაინ სისტემა აადვილებს კომუნიკაციას დიზაინისა და დეველოპმენტის გუნდებს შორის, და რაც ყველაზე მთავარია, საშუალება გვეძლევა სწრაფი იტერაციების.
დიზაინ სისტემის სრულყოფილ მაგალითად შეგიძლიათ წარმოიდგინოთ human interface guidelines. ჩვენი დღევანდელი მიზანი არის iOS-ის პროექტის მაგალითზე, დიზაინ სისტემის საბაზო ინფრასტრუქტურის მოწყობა და Unit ტესტებით განმტკიცება.
რომელ ფუნდამენტურ კომპონენტებზე გავამახვილებთ ყურადღებას?
- ფერები
- ტიპოგრაფია
- ღილაკები
ფერები
სანამ უშუალოდ გადაწყვეტაზე გადავალთ, ორი სიტყვა ვთქვათ ფერებზე. დიზაინ სისტემის ფარგლებში, ყველა ის ფერი, რომელსაც პროდუქტის ფარგლებში გამოვიყენებთ იქნება სასრული. თითოეულ ფერს ექნება კონკრეტული სახელი. ეს სახელი აუცილებლად არ გადმოსცემს ფერის მნიშვნელობას. მაგალითად თუ #FFFFFF სახელად დავარქმევთ white-ს და რამდენიმე თემის მხარდაჭერას ვგეგმავთ, ამ სახელის უკან #FFFFFF-გან განსხვავებული კოდის წარმოდგენა გამოიწვევს სემანტიკურ დაბნეულობას. ამიტომ ფერების სახელები სასურველია უფრო ზოგადი გვქონდეს. მაგალითად primary, secondary, error და ა.შ.
დავუშვათ ჩვენ წარმოსახვით დიზაინ სისტემაში ვიყენებთ მხოლოდ 3 ფერს. თითოეული სახელის უკან დგას კონკრეტული ფერის კოდი. მოდით აღნიშნული ფერები დავამატოთ Asset კატალოგში.
import Foundation
public enum DesignSystem {
}
შევქმნათ Color ენამი, რომელიც Asset კატალოგში აღწერილ ფერებთან სტატიკური ქეისებით მუშაობის საშუალებას მოგვცემს.
import UIKit
public extension DesignSystem {
enum Color: Equatable, Hashable {
case clear
case primary(alpha: CGFloat = 1)
case secondary(alpha: CGFloat = 1)
case error(alpha: CGFloat = 1)
public var value: UIColor {
switch self {
case .clear: return .clear
case .primary(let alpha): return UIColor(named: "primary")!.withAlphaComponent(alpha)
case .secondary(let alpha): return UIColor(named: "secondary")!.withAlphaComponent(alpha)
case .error(let alpha): return UIColor(named: "error")!.withAlphaComponent(alpha)
}
}
}
}
ენამები სვიფტში საკმაოდ დაიხვეწა, ამიტომ ელეგანტურად ერგება ფერებთან მუშაობის ამოცანის გადაჭრას. ძირითად მოქნილობას ქმნის ის ფაქტი რომ ენამის ქეისის პარამეტრს შეიძლება ჰქონდეს დიფოლტ მნიშვნელობა (apha: CGFloat = 1).
შემდეგში ფერების დასეტვა კონკრეტულ UI ელემენტებზე მოხდება მხოლოდ ზემოთ შექმინილი Color ენამის კონკრეტული ქეისებით. შეგვიძლია დავივიწყოთ UIColor-ის არსებობა. ამისთვის კი დაგვჭირდება დამხმარე ინფრასტრუქტურა. დავამატოთ extension მეთოდები UIView, UILabel და UIButton კლასებს, რომელიც საშუალებას მოგცემს Color-ის კონკრეტული ქეისი, მოქნილი API-ის საშუალებით დავსეტოთ.
import UIKit
public extension UIView {
func setBackgroundColor(to color: DesignSystem.Color) {
self.backgroundColor = color.value
}
func setTintColor(to color: DesignSystem.Color) {
self.tintColor = color.value
}
func setBorderColor(to color: DesignSystem.Color) {
self.layer.borderColor = color.value.cgColor
}
}
import UIKit
public extension UILabel {
func setTextColor(to color: DesignSystem.Color) {
self.textColor = color.value
}
}
import UIKit
public extension UIButton {
func setTitleColor(to color: DesignSystem.Color?, for state: UIControl.State) {
self.setTitleColor(color?.value, for: state)
}
}
UIView, UILabel და UIButton ვიღებთ როგორც საბაზო და ყველაზე ხშირად გამოყენებადი UI კლასებს. იგივე ლოგიკა შეგვიძლია გავავრცელოთ UIKit-ის სხვა კომპონენტებზეც.
მოდით ვნახოთ ზემოთ დამატებული თითოეული ექსთენშენ მეთოდის გამოყენების მაგალითი განმტკიცებული კონკრეტული Unit ტესტებით.
import XCTest
@testable import DesignSystem
class DesignSystemTests: XCTestCase {
// MARK: - UIView
func testViewSetBackgroundColor() {
let color = DesignSystem.Color.primary()
let view = UIView()
view.setBackgroundColor(to: color)
XCTAssertEqual(view.backgroundColor, color.value)
}
func testViewSetTintColor() {
let alpha = CGFloat.random(in: 0...1)
let color = DesignSystem.Color.secondary(alpha: alpha)
let view = UIView()
view.setTintColor(to: color)
XCTAssertEqual(view.tintColor, color.value)
}
func testViewSetBorderColor() {
let color = DesignSystem.Color.error()
let view = UIView()
view.setBorderColor(to: color)
XCTAssertEqual(view.layer.borderColor, color.value.cgColor)
}
// MARK: - UILabel
func testLabelSetTitleColor() {
let alpha = CGFloat.random(in: 0...1)
let color = DesignSystem.Color.primary(alpha: alpha)
let label = UILabel()
label.setTextColor(to: color)
XCTAssertEqual(label.textColor, color.value)
}
// MARK: - UIButton
func testButtonSetTitleColor() {
let alpha = CGFloat.random(in: 0...1)
let color = DesignSystem.Color.error(alpha: alpha)
let button = UIButton()
button.setTitleColor(to: color, for: .normal)
XCTAssertEqual(button.titleColor(for: .normal), color.value)
}
}
ტესტები დოკუმენტაციის კარგ წყაროს წარმოადგენს. ცხადად ჩანს ინფრასტრუქტურის თითოეული კომპონენტის გამოყენების მაგალითი და ვერიფიკაცია.
ტიპოგრაფია
მოდით გადავიდეთ ტიპოგრაფიის აღწერაზე. დავუშვათ დიზაინ სისტემის ფარგლებში მოგვეწოდა ფერების მსგავსი სია. Color-ის მსგავსად დავამტოთ Typography ენამი და აღვწეროთ შესაბამისი რეალობა.
import UIKit
public extension DesignSystem {
enum Typography: Equatable {
case title1
case title2
case title3
case body
public var description: Description {
switch self {
case .title1: return .init(font: UIFont(name: "Roboto-Medium", size: 24)!, lineSpacing: 3)
case .title2: return .init(font: UIFont(name: "Roboto-Medium", size: 20)!, lineSpacing: 2)
case .title3: return .init(font: UIFont(name: "Roboto-Bold", size: 16)!, lineSpacing: 1)
case .body: return .init(font: UIFont(name: "Roboto-Regular", size: 13)!, lineSpacing: 0)
}
}
public struct Description {
public var font: UIFont
public var lineSpacing: CGFloat
}
}
}
ფერების მსგავსად, ტიპოგრაფიაშიც გვაქვს ზოგადი სახელები, რომლის უკან ერთიანდება ფონტი, ზომა და ხაზებს შორის დაშორება. უნდა აღინიშნოს, რომ Typography.Description შეიძლება მოიცავდეს სხვა პარამეტრებსაც, როგორიც არის მაგალითად lineHeight და ა.შ. ეს ყველაფერი დამოკიდებულია დიზაინ სისტემის დეტალიზაციაზე.
ტიპოგრაფიის მოქნილი გამოყენებისთვის დავამატოთ ექსთენშენ მეთოდები UILabel და UIButton კლასებისთვის:
public extension UILabel {
func setFont(from typography: DesignSystem.Typography) {
self.font = typography.description.font
}
}
public extension UIButton {
func setFont(from typography: DesignSystem.Typography) {
self.titleLabel?.font = typography.description.font
}
}
მსგავსი ექსთენშენ მეთოდები შეგვიძლია დავამაოთ ყველა სხვა UI კომპონენტს რომელსაც ფონტის ცნება გააჩნია. მაგალითად UITextField, UITextView და ა.შ.
ამ შემთხვევაშიც ვნახოთ ზემოთ დამატებული თითოეული ექსთენშენ მეთოდის გამოყენების მაგალითი განმტკიცებული კონკრეტული Unit ტესტებით.
import XCTest
@testable import DesignSystem
class DesignSystemTests: XCTestCase {
// MARK: - UILabel
func testLabelSetFont() {
let typography = DesignSystem.Typography.title1
let label = UILabel()
label.setFont(from: typography)
XCTAssertEqual(label.font, typography.description.font)
}
// MARK: - UIButton
func testButtonSetFont() {
let typography = DesignSystem.Typography.title2
let button = UIButton()
button.setFont(from: typography)
XCTAssertEqual(button.titleLabel?.font, typography.description.font)
}
}
უკვე გვაქვს ორი მნიშვნელოვანი საშენი კომპონენტი, რომელიც კარგ ფუნდამენტს წარმოადგენს დიზაინ სისტემის მომავალი გაფართოებისთვის.
ღილაკის აღწერა
მოდით გადავიდეთ ღილაკის აღწერაზე, რომელიც ჩვენ მიერ შექმნილ Color და Typography კომპონენტებს დაეყრდრობა.
import UIKit
public extension DesignSystem {
enum Button: Equatable {
public enum Size {
case small
case medium
case large
public var description: Description {
switch self {
case .small: return .init(typography: .title2, contentEdgeInsets: .zero)
case .medium: return .init(typography: .title2, contentEdgeInsets: .init(top: 10, left: 20, bottom: 10, right: 20))
case .large: return .init(typography: .title2, contentEdgeInsets: .init(top: 15, left: 25, bottom: 15, right: 25))
}
}
public struct Description {
public let typography: Typography
public var contentEdgeInsets: UIEdgeInsets
}
}
public enum State: Equatable {
case normal
case disabled
}
public enum Style {
case primary(state: State, size: Size)
public var description: Description {
switch self {
case .primary(let state, let size):
switch state {
case .normal: return .init(size: size, textColor: .primary(), backgroundColor: .clear)
case .disabled: return .init(size: size, textColor: .secondary(), backgroundColor: .clear)
}
}
}
public struct Description {
public var size: Button.Size
public var textColor: Color
public var backgroundColor: Color = .clear
}
}
}
}
ღილაკი შედარების კომპლექსური კომპონენტია. ის ხასიათდება Style-ით, რომელიც თავის მხრივ შედგება Size-სა და State-სგან. თითოელი მათგანი ახდენს გავლენას ღილაკის საბოლოო პრეზენტაციაზე. როგორც ხედავთ აღნიშნული ტიპები აქტიურად იყენებენ Color და Typography კომპონენტებს.
ღილაკის სტილების გამოყენებისთვის დავამატოთ ახალი ექსთენშენ მეთოდი UIButton კლასს:
public extension UIButton {
func setStyle(to style: DesignSystem.Button.Style) {
let description = style.description
setBackgroundColor(to: description.backgroundColor)
setTitleColor(to: description.textColor, for: .normal)
setFont(from: description.size.description.typography)
contentEdgeInsets = description.size.description.contentEdgeInsets
}
}
როგორც ხედავთ setStyle მეთოდი ძირთადად აგებულია ჩვენ მიერ უკვე დამატებული ექსთენშენ მეთოდებისგან.
ვნახოთ გამოყენების მაგალითი კონკრეტული ტესტის მიხედვით.
import XCTest
@testable import DesignSystem
class DesignSystemTests: XCTestCase {
// MARK: - UIButton
func testButtonSetStyle() {
let style = DesignSystem.Button.Style.primary(state: .normal, size: .medium)
let button = UIButton()
button.setStyle(to: style)
XCTAssertEqual(button.titleLabel?.font, style.description.size.description.typography.description.font)
XCTAssertEqual(button.contentEdgeInsets, style.description.size.description.contentEdgeInsets)
XCTAssertEqual(button.backgroundColor, style.description.backgroundColor.value)
XCTAssertEqual(button.titleColor(for: .normal), style.description.textColor.value)
}
}
ამით დავასრულეთ წარმოსახვითი დიზაინ სისტემის სამი ძირითადი კომპონენტის შექმნა. ამ ეტაპზე ინფრასტრუქტურა მზად არის მომავალი განვრცობისთვის, სადაც შესაძლებელია შემოვიტანოთ Dimension-ები, სადაც მაგალითად თავმოყრილი იქნება Spacing, Insets და CornerRadius. ასევე ღილაკის მსგავსად შეიძლება აიგოს სხვა პატარა ან დიდი მრავალჯერ გამოყენებადი კომპონენტი, რომელიც უკვე არსებულ კომპონენტებს დაეყრდნობა.
აღნიშნული მიდგომით ძალიან მარტივი ხდება აპლიკაციაში მრავალი თემის მხარდაჭერა, ხშირი იტერაცია და კონკრეტული გვერდების სწრაფი აწყობა. ასევე რედიზაინიც არ იქნება მტკივნეული.
ზემოთ მოყვანილი მაგალითი ეყრდნობოდა ჩვენთვის მოწოდებულ დიზაინ სისტემას, რომელზეც პროფესიონალი დიზაინერები მუშაობენ. არის შემთხვევები, როდესაც ასე არ ხდება და დიზაინში ჩვენ თვითონ გვიწევს გარკვეული წესების გამოვლენა. ამ შემთხვევაშიც არის შესაძლებელი ზემოთ მოყვანილი იმპლემენტაციით დავიწყოთ, ოღონდ გზადაგზა დავამატოთ ამა თუ იმ კომპოენენტს შესაბამისის ქეისები.
მაგალითად Typography enum-ს შეგვიძლია გავუკეოთოთ custom case, რომელსაც გამოყენებისას კონკრეტულ მნიშვნელობებს გადავაწვდით. ხოლო რაღაც დროის შემდეგ თუ გამოიკვეთება ხშირად განმეორებადი პარამეტრები, შეგვიძლია რაიმე კონკრეტული case-ის სახით აღვწეროთ და ჩვენთვის მისაღები სახელი დავარქვათ. ჩავთვალოთ ამ შემთხვევაში, გზადაგზა გამოვავლენთ დიზაინ სისტემის ძირითად მნიშვნელობებს.
import UIKit
public extension DesignSystem {
enum Typography: Equatable {
case title1
case title2
case title3
case body
case custom(description: Description)
public var description: Description {
switch self {
case .title1: return .init(font: UIFont(name: "Roboto-Medium", size: 24)!, lineSpacing: 3)
case .title2: return .init(font: UIFont(name: "Roboto-Medium", size: 20)!, lineSpacing: 2)
case .title3: return .init(font: UIFont(name: "Roboto-Bold", size: 16)!, lineSpacing: 1)
case .body: return .init(font: UIFont(name: "Roboto-Regular", size: 13)!, lineSpacing: 0)
case .custom(let description): return description
}
}
public struct Description {
public var font: UIFont
public var lineSpacing: CGFloat = 0
}
}
}
custom case-ის იდეა შეგვიძლია სხვა ენამების შემთხვევაშიც განვავრცოთ.