Ніякого rocket science або декілька порад для старту нового проєкту

Усім привіт, мене звати Денис Румянцев і останні три роки я працюю над додатком Hily. На момент, коли я приєднався до проєкту, він був на етапі пост-MVP, та деякі рішення було дуже важко масштабувати. Але, будь ця стаття у моїх попередників, моя команда пережила б трохи менше болю 😀

Можливо, деякі речі могуть здатися over-engineering на ранньому етапі, а особливо, якщо мова йде про стартап, проте запевняю вас: усі ці рішення окупляться з лишком у найближчому часі, тож вйо до змісту.

Про що будемо розмовляти:

  • Використання зовнішніх залежностей (нащо воно тобі треба?)
  • Як зберегти собі трохи часу
  • Czy mówisz po angielsku?
  • Дизайн-система

Використання зовнішніх залежностей (нащо воно тобі треба?)

Але спершу історія: у свій перший робочий день, я відкрив Podfile.lock й зразу виникло бажання звільнитися, бо там було, бляха, 38 (!!!) зовнішніх залежностей різного виду та окрасу: придурошні спінери, 2 фреймворки для BDD-тестування (проте, жодного тесту), та дивний симбіоз кор-залежностей на кшталт IGListKit, Realm, PromiseKit.

Znevaga

А випилювали усе це добро ми майже два з гаком роки…

Якщо не занурюватись до сакрального сенсу “нащо воно треба?”, то навіть з таким hype driven development можна було б жити, якщо підходити до всього з розумом.

Розглянемо приклад, коли ми явно використовуємо якусь залежність, тим самим сприяємо розповсюдженню зарази:

import Alamofire // <-- тривожний дзвіночок

final class SomeViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let url = "https://your-cool-domain.com/feed"
        let parameters: [String: Any] = [
            "offset": 0,
            "type": "all"
        ]
        let headers = [
            "Content-type": "application/json",
            "Authroization": "Bearer ..."
        ]
        
        Alamofire.request(
            url,
            metod: .get,
            parameters: parameters,
            encoding: .default,
            headers: headers
        ).responseData { response in
            // Щось тута робимо
        }
    }
}

Поза тим, що у приведеному випадку мережа у контролері (сподіваюсь, що так уже жодна душа не робить), є інша дилема: якщо ви вирішите відмовитися від Alamofire на користь власного сервісу на 30 рядочків, а ваш проєкт буде більше одного екрану, доведеться гаяти час та проходитись по усім файлам, де воно використовується, та страждати.

Що у такому випадку пропонує кожна третя стаття в інтернеті? Усе вірно, винести роботу з Alamofire у відокремлений сервіс, закрити взаємодію протоколом, щоб догодити богам SOLID та мати можливість покрити автотестами сервіси/юніти, де він використовуюється.

То чому ж для роботи з іншими сторонніми фреймворками не зробити так само? Цей підхід не спрацює для супер-кор-технологій на кшталт RxSwift, проте для більшості інших – заєбумба.

Ось які переваги нам дає тактика маскування використання сторонніх бібліотек:

  • Можливість адаптувати API “під себе”.
  • Отримати повний контроль над кодом.
  • Не будуть розмазані import WowNewCocoapodSpinner по усьому проєкту
  • У разі прийняття рішення про видалення фреймворку чи заміни його на інший, треба буде щось робити лише у декількох файлах, решта коду, який використовує ваші врапери продовжать роботу as is.

Можливо, все це вам і не треба (але хто його зна що буде завтра), проте мінусом цього підходу є тільки надлишкове проксі.

Взагалі, кожен раз, перед тим як додавати нову залежність до свого проєкту, треба зважити: чи варте воно того? Слід бути впевненим тому, що ця бібліотека із вами до кінця, чи мати подай якийсь план, як це випилювати. Також, треба думати не лише про себе, але й команду – чи вміють/чи мають час навчитися? Може, варто витратити деякий час та зробити власний велосипед?

В усьому повинен зберігатися баланс та здоровий глузд, то ж choice wisely.

Як зберегти собі трохи часу

1. Автоматизувати усе!

Усі ми люди та й час від часу можемо помилятися, а також занудьгувати від монотонної праці. Але є й хороша новина – зараз кінець 2020 року (фух!) і кожному із нас доступний великий арсенал інструментів зі спрощення життя автоматизації.

Спершу, поговоримо про автоматизацію збірки: нащо нам оце робити на самому початку? Ось мої аргументи:

  • На самому старті проєкт компілиться дуже швидко, то ж в разі помилки, час на ретест зміни буде мінімальний.
  • Потім буде ліньки й життя потрохи буду перетворюватись на таке: Compiling
  • Це, банально, інвестиція у зекономлений час у майбутньому і це окупається у 100% кейсів.
  • При коректному формуванні пайплайнів збірки, майже повністю вилучається людський фактор. Роботи рідко помиляються ;)

Як на мене, найпоширеніший й простіший варіант це fastlane + Jenkins на власному маку міні (якщо опції виділення окремої машини немає, що ж, можна запускати пайплайни фастлейна на власному компі). Ось приклад як буде виглядати фастлейн-конфігурація найпростішого деплойменту проєкту із кокоа-подс до тестфлайту:

lane :release do
  ensure_git_status_clean
  cocoapods(try_repo_update_on_error: true)
  build_app(
    scheme: "MyApp",
    workspace: "Example.xcworkspace",
    include_bitcode: true
  )
  upload_to_testflight(skip_waiting_build_processing: true)
  clean_build_artefacts
end

Якщо немає ні компа для CI/CD, ні бажання деплоїти на своєму – тут на допомогу приходять безліч клауд-сервісів, наприклад: Bitrise, CircleCI, TravisCI, Buddybuild та інші.

2. Кодогенерація

Не будемо багато зупинятися на цьому, треба лише пам’ятати, що якщо є можливість чогось не робити, при цьому закривати потребу – варто приділити цьому трохи часу.

З чого можна почати:

  • Створення сніпетів / шаблонів нових модулів за допомогою Xcode-темплейтів.
  • Зробити свій код більш безпечним з використанням статичних посилань на зображення, локалізацію, шрифти, тощо.

Більшість речей вже зроблено за нас і можна знайти на репозиторіях типу R.swift, SwiftGen та sourcery. Впевнений, кожен знайде щось для себе.

Одна із найбільших переваг кодогенерації – це створення type-safe вказівників на ресурси, зокрема зображення. Ми позбавляємося одразу декількох потенційних проблем.

Не треба дивитись як називався ресурс і хардкодити за допомогою конструкцій типу:

imageView.image = UIImage(named: "veryImportantImage")

Якщо дизайнер вирішить змінити контент та дасть вам нове зображення, ви заміните старе, а конструкція залишиться той самою, в результаті може статися халепа, й користувачі так і не побачать зображення 😿. Проте, якщо заюзати одну із наведених вище бібліотек, використання може виглядати десь так (приклад із використанням бібліотеки R.swift):

imageView.image = R.image.veryImportantImage()

Якщо назва змінилась, компілятор не дасть збілдити це і ви миттєво підправите помилку. Бонусом отримаємо автокомпліт і відсутність літералів.

Czy mówisz po angielsku?

Найліпше, що можна зробити зі рядками на новому проєкті – це не додавати їх взагалі огорнути їх усі у NSLocalizedString. На жаль, ми це не робили спочатку, бо вважали що будемо працювати виключно на ринку США, але на другий рік проєкту плани змінилися та ось зараз в нас уже реалізовано підтримку десяти мов.

А ще краще – використати якусь тулзу для кодогенерації із попереднього пункту і на виході отримаємо щось типу:

enum L10n {
    struct Alert {
        static let title = NSLocalizedString("Alert.Title", comment: "")
        static let text = NSLocalizedString("Alert.Text", comment: "")
    }
}

І, в решті решт, еволюція виглядає наступним чином:

// 😿
alert.title = "Ну клас, а як це буде виглядати у Німеччині?"
alert.text = "Та ось так. Не зрозуміло"

// 😼
alert.title = NSLocalizedString("Alert.Title", comment: "")
alert.text = NSLocalizedString("Alert.Text", comment: "")

// 😻
alert.title = L10n.Alert.title
alert.text = L10n.Alert.text

Якщо додати до Build Phases виконання скрипту автогенерації, а по коду використовувати лише статичні конструкції, також виключається можливість зробити щось не так і показати користувачу не валідну інфу. Користуйтесь.

Дизайн-система

Як часто вам доводилось страждати при редизайні чи примсі дизайнера? Впевнений, у кожного в досвіді таке було принаймні один раз.

Нормальні дизайнери, при створенні мокапів завжди користуються символами у Figma чи Sketch – де вони роблять конструктор усіх компонентів та починають із маленького – типографіки, кольорів й переходять на більші речі типу лейблів, формочок та ін. (більше деталей можна почитати тут, або ось тут).

Проте деякі розробники нехтують цим інструментом та працюють відокремлено, хардкодячи шрифти чи кольори у сторібордах (не треба їх юзати взагалі, кажу вам), що потенційно впливає на конститентність та легкість внесення змін. Мій поінт у тому, щоб закласти декілька годинок на самому старті й зробити у коді інтерпретацію такої системи. Якщо виносити це в окремий проєкт/фреймворк, то це може стати першим кроком до модулярізації воркспейсу чи послужити шаблоном для наступних аппок та реюзатися на рівні компанії.

Тож, йдемо до дизайнера та домовляємося про співпрацю. Вам видадуть щось типу оцього: Design system fonts

Так виглядала наша перша ітерація шрифтів у додатку. А ми робимо щось на кшталт цього:

Styles

Та не зупиняємось, створюємо свою реалізацію дефолтних компонентів, типу:

final class MyAppLabel: UILabel {
    func configure(with style: TextStyle) {
        self.textColor = style.color
        self.textAlignment = style.alignment
        self.font = style.font
        // etc
    }
}

// Приклад використання
// let label = MyAppLabel()
// label.configure(with: .d4(.center))

final class MyAppButton: UIButton {
    func configure(with style: ButtonsStyle) {
        self.setTitleColor(style.textColor, for: .normal)
        self.backgroundColor = style.bgColor
        // etc
    }
}

І все, ви прекрасні. Код організовано, усе реюзабельно, та ніякі редизайни вам не страшні!

Сподіваюсь, що ці поради будуть для вас цінними та зроблять вашу працю над додатком трішки приємнішою або ж додадуть простір для власних експериментів. Усі приведенні речі забирають трохи часу на ранньому етапі, проте допомагають економити значно більше із ростом проєкту. Також, дозволять швидко та, що важливо, якісно реагувати на дуже мінливі вимоги зі сторони бізнесу. То ж, чи ви працюєте у стартапі, де усе змінюється по 100 разів на день, чи піклуєтесь про гіганта індустрії, раджу розглянути приведенні ідеї та втілити їх у життя. Створюйте круті продукти, завжди піклуйтесь о масштабуванні та мийте руки 😉

Якщо ви маєте цікавий досвід і прагнете ним поділитись – пишіть Славіку на пошту: killobatt@gmail.com. Якщо не прагнете ділитись досвідом – то прагніть. А якщо ви ще не маєте досвіду – то здобувайте його разом із нами. Адже ми тут говоримо про Swift. Українською.