design systems

11001100

28 იანვარი, 2021 წელი

სანდო დიზაინ სისტემის კომპონენტები

დღეს მინდა გესაუბროთ დიზაინ სისტემაზე და მისი საბაზო კომპონენტების კონკრეტულ გადაწყვეტაზე. პირველ რიგში განვმარტოთ თუ რა არის დიზაინ სისტემა.

დიზაინ სისტემა არის პროექტის ყოვლისმომცველი სახელმძღვანელო, რომელიც აერთიანებს გარკვეულ წესებს, პრინციპებსა და საუკეთესო მიდგომებს. დიზაინ სისტემის საბაზო კომპონენტებს ძირითადად წარმოადგენს UI ელემენტები. დიზაინ სისტემას ხშირად უწოდებენ single source of truth, რომელიც მთელ გუნდს ეხმარება განავითაროს პროდუქტი და შეინარჩუნოს მაღალი ხარისხი. დიზაინ სისტემა შეიძლება გავაიგივოთ ლეგოსთან.

მეორე მნიშვნელოვანი კითხვა დიზაინ სისტემასთან მიმართებაში არის ის თუ რა სარგებლობა მოაქვს მას?

პირველ რიგში ეს არის თანმიმდევრულობა, სადაც თითოეული დიზაინის გვერდი აღქმულია როგორც გარკვეული საბაზო კომპონენტების ერთობლიობა. ასევე მარტივი ხდება ტესტირება და გარკვეული ანომალიების გამოვლენა. ასევე დიზაინ სისტემა აადვილებს კომუნიკაციას დიზაინისა და დეველოპმენტის გუნდებს შორის, და რაც ყველაზე მთავარია, საშუალება გვეძლევა სწრაფი იტერაციების.

დიზაინ სისტემის სრულყოფილ მაგალითად შეგიძლიათ წარმოიდგინოთ human interface guidelines. ჩვენი დღევანდელი მიზანი არის iOS-ის პროექტის მაგალითზე, დიზაინ სისტემის საბაზო ინფრასტრუქტურის მოწყობა და Unit ტესტებით განმტკიცება.
რომელ ფუნდამენტურ კომპონენტებზე გავამახვილებთ ყურადღებას?

  • ფერები
  • ტიპოგრაფია
  • ღილაკები

ფერები

სანამ უშუალოდ გადაწყვეტაზე გადავალთ, ორი სიტყვა ვთქვათ ფერებზე. დიზაინ სისტემის ფარგლებში, ყველა ის ფერი, რომელსაც პროდუქტის ფარგლებში გამოვიყენებთ იქნება სასრული. თითოეულ ფერს ექნება კონკრეტული სახელი. ეს სახელი აუცილებლად არ გადმოსცემს ფერის მნიშვნელობას. მაგალითად თუ #FFFFFF სახელად დავარქმევთ white-ს და რამდენიმე თემის მხარდაჭერას ვგეგმავთ, ამ სახელის უკან #FFFFFF-გან განსხვავებული კოდის წარმოდგენა გამოიწვევს სემანტიკურ დაბნეულობას. ამიტომ ფერების სახელები სასურველია უფრო ზოგადი გვქონდეს. მაგალითად primary, secondary, error და ა.შ.
დავუშვათ ჩვენ წარმოსახვით დიზაინ სისტემაში ვიყენებთ მხოლოდ 3 ფერს. თითოეული სახელის უკან დგას კონკრეტული ფერის კოდი. მოდით აღნიშნული ფერები დავამატოთ Asset კატალოგში.

10501050

Aasset კატალოგი

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)
    }
}
660660

ტესტები დოკუმენტაციის კარგ წყაროს წარმოადგენს. ცხადად ჩანს ინფრასტრუქტურის თითოეული კომპონენტის გამოყენების მაგალითი და ვერიფიკაცია.

ტიპოგრაფია

მოდით გადავიდეთ ტიპოგრაფიის აღწერაზე. დავუშვათ დიზაინ სისტემის ფარგლებში მოგვეწოდა ფერების მსგავსი სია. 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)
    }
}
660660

უკვე გვაქვს ორი მნიშვნელოვანი საშენი კომპონენტი, რომელიც კარგ ფუნდამენტს წარმოადგენს დიზაინ სისტემის მომავალი გაფართოებისთვის.

ღილაკის აღწერა

მოდით გადავიდეთ ღილაკის აღწერაზე, რომელიც ჩვენ მიერ შექმნილ 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-ის იდეა შეგვიძლია სხვა ენამების შემთხვევაშიც განვავრცოთ.