Дізнайтеся кращі практики iOS, побудувавши просту програму рецептів

Джерело: ChefStep

Зміст

  • Починаємо
  • Версія Xcode та Swift
  • Мінімальна версія iOS для підтримки
  • Організація проекту Xcode
  • Структура програми Рецепти
  • Кодекси конвенцій
  • Документація
  • Позначення розділів коду
  • Контроль джерела
  • Залежності
  • Потрапляння в проект
  • API
  • Запуск екрана
  • Піктограма програми
  • Пов’язка коду з SwiftLint
  • Безпечний для типу ресурс
  • Покажіть мені код
  • Проектування моделі
  • Краща навігація за допомогою FlowController
  • Автоматичний макет
  • Архітектура
  • Контролер масового виду
  • Управління доступом
  • Ледачі властивості
  • Кодові фрагменти
  • Мережі
  • Як перевірити мережевий код
  • Реалізація кешу для автономної підтримки
  • Як перевірити кеш
  • Завантаження віддалених зображень
  • Зробити завантаження зображення більш зручним для UIImageView
  • Загальний джерело даних для UITableView та UICollectionView
  • Контролер і вид
  • Поводження обов'язків з контролером подання дитини
  • Склад та ін'єкційне залежність
  • Безпека транспорту додатків
  • Спеціальний вид прокрутки
  • Додавання функції пошуку
  • Розуміння контексту презентації
  • Дебютування пошукових дій
  • Тестування дебютації із перевернутим очікуванням
  • Тестування інтерфейсу користувача з UITests
  • Захист головного різьблення
  • Вимірювання виступів та питань
  • Прототипи з дитячим майданчиком
  • Куди піти звідси

Я почав розробку iOS, коли було оголошено про iOS 7. І я трохи навчився, працюючи, поради колег та спільноти iOS.

У цій статті я хотів би поділитися багатьма хорошими практиками, взявши на приклад простий додаток рецептів. Вихідний код знаходиться в GitHub Recipes.

Додаток - це традиційний додаток для детальної майстерності, який демонструє список рецептів разом із їх детальною інформацією.

Є тисячі способів вирішити проблему, а спосіб вирішення проблеми також залежить від особистого смаку. Сподіваємось, у цій статті ви дізнаєтесь щось корисне - я багато чого навчився, коли робив цей проект.

Я додав посилання на деякі ключові слова, де я вважав, що подальше читання буде корисним. Тож обов'язково перевіряйте їх. Будь-який відгук вітається.

Тож почнемо ...

Ось огляд високого рівня того, що ви будуєте.

Починаємо

Давайте визначимося з інструментом та налаштуваннями проекту, які ми використовуємо.

Версія Xcode та Swift

На WWDC 2018 Apple представила Xcode 10 із Swift 4.2. Однак на момент написання запису Xcode 10 все ще знаходиться у бета-версії 5. Тож дотримуйтесь стабільних Xcode 9 та Swift 4.1. Xcode 4.2 має кілька цікавих функцій - ви можете грати з ним на цій чудовій ігровій майданчику. Він не вносить величезних змін із Swift 4.1, тому ми можемо легко оновити наш додаток найближчим часом, якщо буде потрібно.

Ви повинні встановити версію Swift у налаштуваннях проекту замість цільових налаштувань. Це означає, що всі цілі в проекті мають однакову версію Swift (4.1).

Мінімальна версія iOS для підтримки

Станом на літо 2018 року iOS 12 є загальнодоступною бета-версією 5, і ми не можемо націлити на iOS 12 без Xcode 10. У цій публікації ми використовуємо Xcode 9, а базовим SDK є iOS 11. Залежно від вимог та баз користувачів, деякі програми потрібно підтримувати старі версії iOS. Хоча користувачі iOS схильні приймати нові версії iOS швидше, ніж ті, хто використовує Android, є деякі, які залишаються зі старими версіями. Відповідно до порад Apple, ми повинні підтримувати дві останні версії - iOS 10 та iOS 11. За даними App Store 31 травня 2018 року, лише 5% користувачів використовують iOS 9 та попередню версію.

Націлювання на нові версії iOS означає, що ми можемо скористатися новими SDK, які інженери Apple щороку вдосконалюють. Веб-сайт розробника Apple має покращений вигляд журналу змін. Тепер легше бачити, що було додано чи змінено.

В ідеалі, щоб визначити, коли потрібно припинити підтримку старих версій iOS, нам потрібна аналітика щодо того, як користувачі використовують наш додаток.

Організація проекту Xcode

Коли ми створюємо новий проект, вибираємо як «Включити тести одиниць», так і «Включити тести інтерфейсу», оскільки це рекомендована практика рано писати тести. Останні зміни в рамках XCTest, особливо в тестах інтерфейсу користувача, роблять тестування легким вітром і досить стабільними.

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

За роллю / типом:

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

За функцією / модулем

  • Плюси: це робить все модульним і заохочує композицію.
  • Мінуси: Це може стати безладним, коли багато файлів різних типів поєднано разом.

Бути модульним

Особисто я намагаюся якомога більше організувати свій код за особливостями / компонентами. Це полегшує ідентифікацію відповідного коду для виправлення та простіше додавати нові функції в майбутньому. Він відповідає на питання "Що робить ця програма?" Замість "Що це за файл?" Ось хороша стаття щодо цього.

Добре правило - залишатися послідовними, незалежно від того, яку структуру ви вибрали.

Структура програми Рецепти

Далі йде структура додатків, яку використовує наш додаток рецептів:

Джерело

Містить файли вихідного коду, розділені на компоненти:

  • Особливості: основні функції в додатку
  • Домашня сторінка: головний екран із переліком рецептів та відкритим пошуком
  • Список: показує перелік рецептів, включаючи перезавантаження рецепта та показ порожнього перегляду, коли рецепта не існує
  • Пошук: обробляти пошук і розмову
  • Детальніше: показує детальну інформацію

Бібліотека

Містить основні компоненти нашої програми:

  • Потік: містить FlowController для управління потоками
  • Адаптер: загальне джерело даних для UICollectionView
  • Розширення: зручні розширення для звичайних операцій
  • Модель: модель в додатку, проаналізована від JSON

Ресурс

Містить файли плістів, ресурсів та розгортки.

Кодекси конвенцій

Я погоджуюся з більшістю посібників зі стилів у raywenderlich / swift-style-guide та github / swift-guide-guide. Це просто та розумно використовувати у проекті Swift. Також ознайомтесь з офіційними рекомендаціями щодо дизайну API, зробленими командою Swift в Apple, про те, як написати кращий код Swift.

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

Відступ та війна-простір - це чутлива тема, але знову ж таки, це залежить від смаку. Я використовую чотири пробіли в просторах проектів Android та два пробіли в iOS та React. У цій програмі "Рецепти" я дотримуюсь послідовного та простого в обґрунтуванні відступу, про яке я писав тут і тут.

Документація

Хороший код повинен чітко пояснювати себе, щоб не потрібно писати коментарі. Якщо фрагмент коду важко зрозуміти, добре взяти паузу і переробити її на деякі методи з описовими іменами, щоб чіткий зрозумілий фрагмент коду. Однак я вважаю, що заняття з документування та методи гарні також для ваших колег та майбутніх людей. Відповідно до інструкцій щодо дизайну API Swift,

Напишіть коментар до документації для кожної декларації. Статистика, отримана написанням документації, може мати глибокий вплив на ваш дизайн, тому не відкладайте його.

Згенерувати шаблон коментаря /// у Xcode за допомогою Cmd + Alt + / дуже просто. Якщо ви плануєте переробляти свій код на рамки, щоб надалі ділитися з іншими, такі інструменти, як jazzy, можуть створювати документацію, щоб інші люди могли слідувати далі.

Позначення розділів коду

Використання MARK може бути корисним для відокремлення розділів коду. Він також групує функції добре в навігаційній панелі. Ви також можете використовувати групи розширень, пов’язані з ними властивості та методи.

Для простого UIViewController ми можемо визначити наступні позначки:

// МАРКА: - Ініт
// ОЗНАКА: - Перегляд життєвого циклу
// МАРКА: - Налаштування
// МАРКА: - Дія
// МАРКА: - Дані

Контроль джерела

Git - популярна зараз система управління джерелами. Ми можемо використовувати файл шаблону .gitignore від gitignore.io/api/swift. Перевірка файлів залежностей (CocoaPods і Carthage) є і плюсами, і мінусами. Це залежить від вашого проекту, але я схильний не здійснювати залежності (node_modules, Carthage, Pods) у керуванні джерелами, щоб не захаращувати базу коду. Це також полегшує розгляд запитів на Pull.

Незалежно від того, чи переходите ви в каталог Pods, Podfile та Podfile.lock завжди повинні перебувати під контролем версій.

Я використовую як iTerm2 для виконання команд, так і джерельного дерева для перегляду гілок та постановок.

Залежності

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

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

У корені проекту є файл під назвою .swift-версія значення 4.1, який повідомляє CocoaPods, що цей проект використовує Swift 4.1. Це виглядає просто, але мені знадобилося досить багато часу, щоб розібратися.

Потрапляння в проект

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

API

Найпростіший спосіб навчитися мережам iOS - через безкоштовні публічні служби API. Тут я використовую food2fork. Ви можете зареєструватися для облікового запису на веб-сайті http://food2fork.com/about/api. У цьому сховищі public-api є багато інших дивовижних API.

Добре зберігати свої дані в безпечному місці. Я використовую 1Password для створення та зберігання своїх паролів.

Перш ніж розпочати кодування, давайте пограємо з API, щоб побачити, які типи запитів і відповіді вони повертають. Я використовую інструмент Insomnia для тестування та аналізу відповідей API. Це відкритий код, безкоштовний і чудово працює.

Запуск екрана

Перше враження важливе, так це і екран запуску. Кращим способом є використання LaunchScreen.storyboard замість статичного зображення Launch.

Щоб додати стартове зображення до каталогу активів, відкрийте LaunchScreen.storyboard, додайте UIImageView і закріпіть його на краях UIView. Ми не повинні закріплювати зображення в безпечній зоні, оскільки ми хочемо, щоб зображення було повноекранним. Також зніміть прапорці в обмеженнях автоматичного макета. Встановіть contentMode UIImageView як Aspect Fill, щоб він розтягувався з правильним співвідношенням сторін.

Налаштування макета в LaunchScreen.

Піктограма програми

Доброю практикою є надання всіх необхідних значків додатків для кожного підтримуваного пристрою, а також для таких місць, як сповіщення, налаштування та трамплін. Переконайтесь, що на кожному зображенні немає прозорих пікселів, інакше це призведе до чорного фону. Ця порада - з Інструкції щодо людського інтерфейсу - Ікона додатка.

Зберігайте фон простим і уникайте прозорості. Переконайтеся, що ваш значок непрозорий, і не захаращуйте фон. Надайте йому простий фон, щоб він не переповнював інші значки додатків поблизу. Вам не потрібно заповнювати всю іконку вмістом.

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

Додаток IconGenerator може генерувати піктограми для iOS в додатках iPhone, iPad, macOS та watchOS. Результат - AppIcon.appiconset, який ми можемо перетягнути прямо в каталог активів. Каталог активів - це шлях для сучасних проектів Xcode.

Пов’язка коду з SwiftLint

Незалежно від того, на якій платформі ми розробляємо, добре мати лінійку для виконання послідовних конвенцій. Найпопулярнішим інструментом для проектів Swift є SwiftLint, виготовлений чудовими людьми в Realm.

Щоб встановити його, додайте podfile 'SwiftLint', '~> 0.25' до Podfile. Також є хорошою практикою вказати версію залежностей, щоб встановити pod випадково не буде оновлено до основної версії, яка може зламати ваш додаток. Потім додайте .swiftlint.yml з бажаною конфігурацією. Зразок конфігурації можна знайти тут.

Нарешті, додайте нову фразу Run Script, щоб виконати швидку розкладку після компіляції.

Безпечний для типу ресурс

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

imageView.image = R.image.notFound ()

Покажіть мені код

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

Проектування моделі

Це може здатися нудним, але клієнти - це лише гарний спосіб представити відповідь API. Модель - це, мабуть, найосновніше, і ми її багато використовуємо в додатку. Він відіграє настільки важливу роль, але можуть бути деякі очевидні помилки, пов’язані з неправильними моделями, і припущення про те, як слід розбирати модель, яку потрібно враховувати.

Ми повинні перевірити кожну модель програми. В ідеалі нам потрібно автоматизоване тестування моделей за допомогою відповідей API на випадок, якщо модель змінилася з бекенда.

Починаючи з Swift 4.0, ми можемо відповідати нашій моделі Codable, щоб легко серіалізувати до та від JSON. Наша модель повинна бути незмінною:

Структура Рецепт: Codable {
  нехай видавець: String
  нехай URL: URL
  нехай sourceUrl: String
  нехай ідентифікатор: Рядок
  нехай назва: Рядок
  нехай imageUrl: String
  нехай socialRank: подвійний
  нехай publisherUrl: URL
enum CodingKeys: String, CodingKey {
    справа видавець
    case url = "f2f_url"
    case sourceUrl = "source_url"
    case id = "recept_id"
    назва справи
    case imageUrl = "image_url"
    case socialRank = "social_rank"
    case publisherUrl = "видавець_url"
  }
}

Ми можемо використовувати деякі тестові рамки, якщо вам подобається фантазійний синтаксис або стиль RSpec. У деяких сторонніх тестових рамках можуть виникнути проблеми. Я вважаю XCTest досить хорошим.

імпорт XCTest
@testable Рецепти імпорту
Рецепти класу: XCTestCase {
  func testParsing () кидає {
    нехай json: [Рядок: Будь-який] = [
      "видавець": "Два горошини та їх стручок",
      "f2f_url": "http://food2fork.com/view/975e33",
      "title": "Печиво з шоколадом з арахісовим маслом без печенья"
      "source_url": "http://www.twopeasandtheirpod.com/no-bake-chocolate-peanut-butter-pretzel-cookies/",
      "recept_id": "975e33",
      "image_url": "http://static.food2fork.com/NoBakeChocolatePeanutButterPretzelCookies44147.jpg",
      "social_rank": 99.99999999999974,
      "publisher_url": "http://www.twopeasandtheirpod.com"
    ]
нехай дані = спробуйте JSONSerialization.data (withJSONObject: json, options: [])
    нехай декодер = JSONDecoder ()
    нехай рецепт = спробуйте decoder.decode (Recipe.self, з: даних)
XCTAssertEqual (recept.title, "Печиво з шоколадного арахісового масла без печива" Крендель "
    XCTAssertEqual (recept.id, "975e33")
    XCTAssertEqual (recept.url, URL (рядок: "http://food2fork.com/view/975e33")!)
  }
}

Краща навігація за допомогою FlowController

Раніше я використовував компас як двигун маршрутизації у своїх проектах, але з часом я виявив, що написання простого коду маршрутизації також працює.

FlowController використовується для управління багатьма компонентами, пов'язаними з UIViewController, для спільної функції. Ви можете прочитати FlowController та Координатор для інших випадків використання та отримати краще розуміння.

Існує AppFlowController, який управляє зміною rootViewController. Поки що він запускає RecipeFlowController.

window = UIWindow (кадр: UIScreen.main.bounds)
вікно? .rootViewController = appFlowController
вікно? .makeKeyAndVisible ()
appFlowController.start ()

RecipeFlowController управляє (насправді це) UINavigationController, який обробляє натискання HomeViewController, RecipesDetailViewController, SafariViewController.

фінальний клас RecipeFlowController: UINavigationController {
  /// Запустіть потік
  func start () {
    нехай послуга = RecipesService (мережа: NetworkService ())
    нехай контролер = HomeViewController (рецептиСервіс: сервіс)
    viewControllers = [контролер]
    controller.select = {[слабкий самодослідник] рецепт в
      самостійно? .rtrtDetail (рецепт: рецепт)
    }
  }
private func startDetail (рецепт: Рецепт) {}
  private func startWeb (url: URL) {}
}

UIViewController може використовувати делегат або закриття, щоб повідомити FlowController про зміни або наступні екрани в потоці. Для делегата може знадобитися перевірка наявності двох екземплярів одного класу. Тут ми використовуємо закриття для простоти.

Автоматичний макет

Автоматичний макет існує з iOS 5, з кожним роком він стає кращим. Хоча деякі люди все ще мають проблеми з цим, здебільшого через заплутані обмеження та продуктивність, але особисто я вважаю, що автоматичний макет є досить хорошим.

Я намагаюся максимально використовувати автоматичний макет, щоб зробити адаптивний інтерфейс користувача. Ми можемо використовувати бібліотеки на зразок якорів для декларативної та швидкої автоматичної розстановки. Однак у цьому додатку ми просто використовуватимемо NSLayoutAnchor, оскільки він перебуває з iOS 9. Код нижче натхненний обмеженням. Пам’ятайте, що автоматичний макет у найпростішій формі включає перемикання перекладуAutoresizingMaskIntoConstraints та активацію isActive обмежень.

розширення NSLayoutConstraint {
  статична функція активації (_ обмеження: [NSLayoutConstraint]) {
    constraints.forEach {
      ($ 0.firstItem як? UIView) ?. перекладаєAutoresizingMaskIntoConstraints = false
      $ 0.isActive = вірно
    }
  }
}

На GitHub насправді доступно багато інших механізмів компонування. Щоб зрозуміти, який із них можна було б використовувати, перегляньте позначку LayoutFrameworkBenchmark.

Архітектура

Архітектура - це, мабуть, найбільш розгублена тема. Я любитель досліджувати архітектури, ви можете переглянути більше публікацій та рамок про різні архітектури тут.

Для мене всі архітектури та шаблони визначають ролі для кожного об’єкта та способи їх з'єднання. Запам’ятайте ці керівні принципи для свого вибору архітектури:

  • інкапсулюйте те, що змінюється
  • прихильність складу над спадщиною
  • програма для інтерфейсу, а не для реалізації

Погравши з багатьма різними архітектурами, з і без Rx, я виявив, що простий MVC досить хороший. У цьому простому проекті є просто UIViewController з логікою, інкапсульованою в допоміжні сервісні класи,

Контролер масового виду

Можливо, ви чули, як люди жартують про те, наскільки масивний UIViewController, але насправді немає масового контролера перегляду. Ми просто пишемо поганий код. Однак є способи зменшити її.

У застосуванні рецептів, які я використовую,

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

Ось дуже гарна стаття з 8 підказок для зменшення великих контролерів.

Управління доступом

У документації SWIFT зазначається, що «контроль доступу обмежує доступ до частин вашого коду з коду в інших вихідних файлах та модулях. Ця функція дозволяє приховати деталі реалізації коду та вказати бажаний інтерфейс, через який можна отримати доступ та використовувати цей код. "

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

Декларуйте властивості як остаточні, де це можливо.

заключний клас HomeViewController: UIViewController {}

Заявіть про властивості як приватні або принаймні приватні (набір).

заключний клас RecipeDetailView: UIView {
  приватне нехай scrollableView = ScrollableView ()
  приватний (набір) ледачий var imageView: UIImageView = self.makeImageView ()
}

Ледачі властивості

Для властивостей, до яких можна отримати доступ пізніше, ми можемо оголосити їх як "lazyand" може використовувати закриття для швидкої побудови.

фінальний клас RecipeCell: UICollectionViewCell {
  приватний (набір) ледачий var containerView: UIView = {
    нехай перегляд = UIView ()
    view.clipsToBounds = вірно
    view.layer.cornerRadius = 5
    view.backgroundColor = Color.main.withAlphaComponent (0,4)
вид назад
  } ()
}

Ми також можемо використовувати функції make, якщо плануємо повторно використовувати ту саму функцію для кількох властивостей.

заключний клас RecipeDetailView: UIView {
  приватний (набір) ледачий var imageView: UIImageView = self.makeImageView ()
private func makeImageView () -> UIImageView {
    нехай imageView = UIImageView ()
    imageView.contentMode = .scaleAspectFill
    imageView.clipsToBounds = вірно
    повернути imageView
  }
}

Це також відповідає рекомендаціям прагнення до безоплатного використання.

Почніть назви заводських методів з "make", наприклад, x.makeIterator ().

Кодові фрагменти

Синтаксис коду важко запам'ятати. Подумайте про використання фрагментів коду для автоматичного генерування коду. Це підтримується Xcode і є кращим способом інженерів Apple під час демонстрації.

якщо #available (iOS 11, *) {
  viewController.navigationItem.searchController = пошукController
  viewController.navigationItem.hidesSearchBarWhenScrolling = false
} else {
  viewController.navigationItem.titleView = пошукController.searchBar
}

Я зробив репо з кількома корисними фрагментами Swift, якими користуються багато хто.

Мережі

Налаштування мережі в Swift - це свого роду вирішена проблема. Є стомлюючі та схильні до помилок завдання, такі як аналіз HTTP-відповідей, обробка черг запитів, обробка запитів параметрів. Я бачив помилки щодо запитів PATCH, методів HTTP із нижнього розміру,… Ми можемо просто використовувати Alamofire. Тут не потрібно витрачати час.

Для цього додатка, оскільки це просто і уникати зайвих залежностей. Ми можемо просто використовувати URLSession безпосередньо. Ресурс зазвичай містить URL, шлях, параметри та метод HTTP.

Stru Resource {
  нехай URL: URL
  нехай шлях: Рядок?
  нехай httpMethod: String
  нехай параметри: [String: String]
}

Простий мережевий сервіс може просто проаналізувати ресурс для URLRequest і повідомити URLSession виконати

заключний клас NetworkService: Мережі {
  @discardableResult func fetch (ресурс: ресурс, завершення: @escaping (дані?) -> недійсність) -> URLSessionTask? {
    guard Let request = makeRequest (ресурс: ресурс) else {
      завершення (нульове)
      повернути нуль
    }
нехай task = session.dataTask (з: запит, завершенняHandler: {дані, _, помилка в
      захистити нехай дані = дані, помилка == нуль ще {
        завершення (нульове)
        повернення
      }
завершення (дані)
    })
task.resume ()
    завдання повернення
  }
}

Використовуйте ін'єкційну залежність. Дозволити абоненту вказати URLSessionConfiguration. Тут ми використовуємо параметр за замовчуванням Swift, щоб забезпечити найпоширеніший варіант.

init (конфігурація: URLSessionConfiguration = URLSessionConfiguration.default) {
  self.session = URL-сесія (конфігурація: конфігурація)
}

Я також використовую URLQueryItem, який був із iOS 8. Це робить параметри аналізу для елементів запиту приємними та менш стомлюючими.

Як перевірити мережевий код

Ми можемо використовувати URLProtocol та URLCache, щоб додати заглушку для мережевих відповідей, або ми можемо використовувати рамки, такі як Mockingjay, яка шипить URLSessionConfiguration.

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

мережа протоколу {
  @discardableResult func fetch (ресурс: ресурс, завершення: @escaping (дані?) -> недійсність) -> URLSessionTask?
}
заключний клас MockNetworkService: Мережі {
  нехай дані: Дані
  init (fileName: String) {
    нехай розшарування = розшарування (для: MockNetworkService.self)
    нехай url = bundle.url (forResource: fileName, withExtension: "json")!
    self.data = спробуйте! Дані (contentOf: url)
  }
func fetch (ресурс: Resource, завершення: @escaping (Data?) -> void) -> URLSessionTask? {
    завершення (дані)
    повернути нуль
  }
}

Реалізація кешу для автономної підтримки

Раніше я багато працював і використовував бібліотеку під назвою Кеш. Нам потрібна хороша бібліотека кешу - це кеш пам'яті та диска, пам'ять для швидкого доступу, диск для стійкості. Коли ми економимо, ми економимо як на пам'яті, так і на диску. Коли ми завантажуємо, якщо кеш пам'яті виходить з ладу, ми завантажуємо з диска, а потім знову оновлюємо пам'ять. Існує багато розширених тем про кеш, такі як очищення, термін дії, частота доступу. Почитайте про них тут.

У цьому простому додатку достатньо класу обслуговування кеш-домівок і гарного способу дізнатися, як працює кешування. Все в Swift може бути перетворено в Data, тому ми можемо просто зберегти дані в кеш. Swift 4 Codable може серіалізувати об'єкт до даних.

Нижче наведений код показує нам, як використовувати FileManager для кешу диска.

/// Збереження та завантаження даних у пам'ять та дисковий кеш
заключний клас CacheService {
/// Для отримання або завантаження даних у пам'ять
  приватна нехай пам'ять = NSCache  ()
/// URL-адреса шляху, що містить кешовані файли (mp3-файли та файли зображень)
  приватний нехай diskPath: URL
/// Для перевірки існування файлу чи каталогу у визначеному шляху
  приватний нехай fileManager: FileManager
/// Переконайтесь, що всі операції виконуються послідовно
  private let serialQueue = DispatchQueue (мітка: "Рецепти")
init (fileManager: FileManager = FileManager.default) {
    self.fileManager = fileManager
    робити {
      нехай documentDirectory = спробуйте fileManager.url (
        для: .documentDirectory,
        у: .userDomainMask,
        підходитьДля нуля,
        творити: правда
      )
      diskPath = documentDirectory.appendingPathComponent ("Рецепти")
      спробуйте createDirectoryIfNeeded ()
    } виловити {
      критична помилка()
    }
  }
func save (дані: дані, ключ: рядок, завершення: (() -> недійсний)? = нуль) {
    дозволити клавішу = MD5 (клавіша)
serialQueue.async {
      self.memory.setObject (дані як NSData, forKey: ключ як NSString)
      робити {
        спробуйте data.write (до: self.filePath (ключ: ключ))
        завершення? ()
      } виловити {
        друк (помилка)
      }
    }
  }
}

Щоб уникнути неправильних і дуже довгих імен файлів, ми можемо їх хеш. Я використовую MD5 від SwiftHash, який дає мертве просте використання дозволений ключ = MD5 (ключ).

Як перевірити кеш

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

клас CacheServiceTests: XCTestCase {
  нехай служба = CacheService ()
переопределити функцію setUp () {
    super.setUp ()
спробувати? service.clear ()
  }
func testClear () {
    нехай очікування = self.expectation (опис: # функція)
    let string = "Привіт, світ"
    нехай дані = string.data (використовуючи: .utf8)!
service.save (дані: дані, ключ: "ключ", завершення: {
      спробувати? self.service.clear ()
      self.service.load (ключ: "ключ", завершення: {
        XCTAssertNil ($ 0)
        expectation.fulfill ()
      })
    })
зачекати (для: [очікування], час очікування: 1)
  }
}

Завантаження віддалених зображень

Я також беру участь у програмі Imaginary, щоб я трохи знав, як це працює. Для віддалених зображень нам потрібно завантажити та кешувати, а кеш-клавіш - це зазвичай URL-адреса віддаленого зображення.

У нашому додатку рецептів створимо просту програму ImageService на основі наших NetworkService та CacheService. В основному зображення - це лише мережевий ресурс, який ми завантажуємо та кешуємо. Ми віддаємо перевагу складу, тому включимо NetworkService і CacheService в ImageService.

/// Перевірка локального кешу та отримання віддаленого зображення
Випускний клас ImageService {
приватне дозволене мережеве обслуговування: мережа
  приватне нехай cacheService: CacheService
  приватне завдання var: URLSessionTask?
init (networkService: Мережа, cacheService: CacheService) {
    self.networkService = networkService
    self.cacheService = cacheService
  }
}

Зазвичай у нас є комірки UICollectionViewі UITableView з UIImageView. Оскільки клітини повторно використовуються, нам потрібно скасувати будь-яке існуюче завдання запиту перед тим, як зробити новий запит.

func fetch (URL: URL, завершення: @escaping (UIImage?) -> void) {
  // Скасувати існуюче завдання, якщо воно є
  завдання? .cancel ()
// Спробуйте завантажити з кеша
  cacheService.load (ключ: url.absoluteString, завершення: {[слабкий я] cachedData в
    якщо нехай data = cachedData, нехай image = UIImage (data: data) {
      DispatchQueue.main.async {
        завершення (зображення)
      }
    } else {
      // Спробуйте подати запит від мережі
      нехай ресурс = Resource (url: url)
      self? .task = self? .networkService.fetch (ресурс: ресурс, завершення: {networkData в
        якщо нехай data = networkData, нехай image = UIImage (data: data) {
          // Зберегти в кеш
          self? .cacheService.save (дані: дані, ключ: url.absoluteString)
          DispatchQueue.main.async {
            завершення (зображення)
          }
        } else {
          print ("Помилка завантаження зображення в \ (url)")
        }
      })
себе? .задача? .результат ()
    }
  })
}

Зробити завантаження зображення більш зручним для UIImageView

Додамо розширення до UIImageView, щоб встановити віддалене зображення з URL-адреси. Я використовую пов'язаний об'єкт, щоб зберегти цю ImageService і скасувати старі запити. Ми добре використовуємо пов'язаний об’єкт, щоб приєднати ImageService до UIImageView. Справа в тому, щоб скасувати поточний запит, коли запит буде запущений знову. Це зручно, коли представлення зображень відображаються у списку прокрутки.

розширення UIImageView {
  func setImage (URL: URL, заповнювач: UIImage? = нуль) {
    якщо imageService == нуль {
      imageService = ImageService (networkService: NetworkService (), cacheService: CacheService ())
    }
self.image = заповнювач
    self.imageService? .fetch (url: url, завершення: {[слабке "я") в
      себе? .образа = образ
    })
  }
приватний var imageService: ImageService? {
    отримати {
      повернути objc_getAssociatedObject (self, & AssociateKey.imageService) як? ImageService
    }
    встановити {
      objc_setAssociatedObject (
        Я,
        & AssociateKey.imageService,
        newValue,
        objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
      )
    }
  }
}

Загальний джерело даних для UITableView та UICollectionView

Ми використовуємо UITableView і UICollectionView майже в кожному додатку і майже повторюємо те саме.

  • показати керування оновленням під час завантаження
  • перезавантажити список у випадку даних
  • показати помилку в разі відмови.

Навколо UITableView та UICollection є багато обгортки. Кожен додає ще один шар абстракції, що дає нам більше сили, але одночасно застосовує обмеження.

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

Я також використовую Upstream на основі цієї ідеї. Важко обернути UITableView і UICollectionView, як багато разів, це специфічно для додатків, тому достатньо тонкої обгортки, як адаптер.

адаптер остаточного класу : NSObject,
UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
  var елементів: [T] = []
  var configure: ((T, Cell) -> void)?
  var select: ((T) -> void)?
  var cellHeight: CGFloat = 60
}

Контролер і вид

Я скинув Дошку розкатувань через багато обмежень та багатьох питань. Натомість я використовую код для перегляду та визначення обмежень. Це не так важко дотримуватися. Більшість кодів котла у UIViewController призначені для створення представлень та налаштування макета. Давайте перенесемо їх на вигляд. Більше про це можна прочитати тут.

/// Використовується для розділення між контролером та переглядом
клас BaseController : UIViewController {
  нехай корінь = T ()
замінити функцію loadView () {
    view = корінь
  }
}
фінальний клас RecipeDetailViewController: BaseController  {}

Поводження обов'язків з контролером подання дитини

Контейнер контролера View - це потужна концепція. Кожен контролер подання має окремий інтерес і може бути складений разом для створення розширених функцій. Я використовував RecipeListViewController для управління UICollectionView і показував список рецептів.

фінальний клас RecipeListViewController: UIViewController {
  приватний (набір) var collectionView: UICollectionView!
  нехай адаптер = адаптер <рецепт, рецепт> ()
  private let emptyView = EmptyView (текст: "Не знайдено рецептів!")
}

Є HomeViewController, який вбудовує цей RecipeListViewController

/// Показати список рецептів
заключний клас HomeViewController: UIViewController {
/// Коли вибрати рецепт
  var select: ((Рецепт) -> void)?
приватний var refreshControl = UIRefreshControl ()
  приватні нехай рецептиСервіс: РецептиСервіс
  приватний нехай searchComponent: SearchComponent
  приватний нехай рецептListViewController = RecipeListViewController ()
}

Склад та ін'єкційне залежність

Я намагаюся створювати компоненти та створювати код, коли можу. Ми бачимо, що ImageService використовує NetworkService і CacheService, а RecipeDetailViewController використовує Recipe і RecipesService.

В ідеалі об'єкти не повинні створювати залежності самостійно. Залежності повинні створюватися назовні та передаватися з кореня. У нашому додатку коренем є AppDelegate та AppFlowController, тому залежності повинні починатися звідси.

Безпека транспорту додатків

Починаючи з iOS 9, усі додатки повинні підтримувати безпеку транспорту додатків

Безпека транспортного забезпечення додатків (ATS) застосовує кращі практики щодо безпечних з'єднань між додатком та його задньою частиною. ОВС запобігає випадковому розкриттю, забезпечує безпечну поведінку за замовчуванням і його легко прийняти; він також за замовчуванням увімкнено в iOS 9 та OS X v10.11. Вам слід якомога швидше прийняти АТС, незалежно від того, створюєте ви новий додаток чи оновлюєте існуючий.

У нашому додатку деякі зображення отримуються через HTTP-з'єднання. Нам потрібно виключити його з правила безпеки, але лише для цього домену.

 NSAppTransportSecurity 
<вирок>
   NSExceptionDomains 
  <вирок>
     food2fork.com 
    <вирок>
       NSIncludesSubdomains 
      <правда />
       NSExceptionAllowsInsecureHTTPLoads 
      <правда />
    
  

Спеціальний вид прокрутки

Для екрана деталей ми можемо використовувати UITableView та UICollectionView з різними типами комірок. Тут погляди повинні бути статичними. Ми можемо стекувати за допомогою UIStackView. Для більшої гнучкості ми можемо просто використовувати UIScrollView.

/// Перегляд вертикального макета за допомогою автоматичного макета в UIScrollView
остаточний клас ScrollableView: UIView {
  приватний нехай scrollView = UIScrollView ()
  приватне нехай contentView = UIView ()
переосмислити init (кадр: CGRect) {
    super.init (кадр: кадр)
scrollView.showsHorizontalScrollIndicator = false
    scrollView.alwaysBounceHorizontal = false
    addSubview (scrollView)
scrollView.addSubview (contentView)
NSLayoutConstraint.activate ([
      scrollView.topAnchor.constraint (рівнийTo: topAnchor),
      scrollView.bottomAnchor.constraint (рівнийTo: bottomAnchor),
      scrollView.leftAnchor.constraint (рівнийTo: leftAnchor),
      scrollView.rightAnchor.constraint (рівнийTo: rightAnchor),
contentView.topAnchor.constraint (рівнийTo: scrollView.topAnchor),
      contentView.bottomAnchor.constraint (рівнийTo: scrollView.bottomAnchor),
      contentView.leftAnchor.constraint (рівнийTo: leftAnchor),
      contentView.rightAnchor.constraint (рівнийTo: rightAnchor)
    ])
  }
}

Закріплюємо UIScrollView до країв. Ми закріплюємо contentView лівим і правим якорем для самозабезпечення, при цьому закріплюючи contentView верхній і нижній якір до UIScrollView.

Погляди всередині contentView мають обмеження вгорі та внизу, тому, коли вони розширюються, вони також розширюють contentView. UIScrollView використовує інформацію про автоматичний макет цього вмісту для визначення його розміру contentSiew. Ось як ScrollableView використовується в RecipeDetailView.

scrollableView.setup (пари: [
  ScrollableView.Pair (перегляд: imageView, вставка: UIEdgeInsets (вгорі: 8, зліва: 0, внизу: 0, праворуч: 0)),
  ScrollableView.Pair (перегляд: інгредієнтHeaderView, вставка: UIEdgeInsets (верх: 8, зліва: 0, низ: 0, праворуч: 0)),
  ScrollableView.Pair (перегляд: інгредієнтLabel, вставка: UIEdgeInsets (верх: 4, зліва: 8, низ: 0, праворуч: 0)),
  ScrollableView.Pair (перегляд: infoHeaderView, вставка: UIEdgeInsets (верх: 4, ліворуч: 0, низ: 0, праворуч: 0)),
  ScrollableView.Pair (перегляд: інструкціяButton, вставка: UIEdgeInsets (верх: 8, ліворуч: 20, низ: 0, праворуч: 20)),
  ScrollableView.Pair (перегляд: originalButton, вставка: UIEdgeInsets (верх: 8, ліворуч: 20, низ: 0, праворуч: 20)),
  ScrollableView.Pair (перегляд: infoView, вставка: UIEdgeInsets (верх: 16, зліва: 0, низ: 20, праворуч: 0))
])

Додавання функції пошуку

З iOS 8 ми можемо використовувати UISearchController, щоб отримати досвід пошуку за замовчуванням за допомогою панелі пошуку та контролера результатів. Ми вкладемо функцію пошуку в SearchComponent, щоб її можна було підключити.

SearchComponent final class: NSObject, UISearchResultsUpdating, UISearchBarDelegate {
  нехай рецептиСервіс: РецептиСервіс
  нехай пошукController: UISearchController
  нехай receptListViewController = RecipeListViewController ()
}

Починаючи з iOS 11, на UINavigationItem є властивість під назвою searchController, що дозволяє легко показувати панель пошуку на панелі навігації.

func add (для переглядуController: UIViewController) {
  якщо #available (iOS 11, *) {
    viewController.navigationItem.searchController = пошукController
    viewController.navigationItem.hidesSearchBarWhenScrolling = false
  } else {
    viewController.navigationItem.titleView = пошукController.searchBar
  }
viewController.definesPresentationContext = вірно
}

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

Розуміння контексту презентації

Розуміння контексту презентації має вирішальне значення для представлення контролера представлення. У пошуку ми використовуємо searchResultsController.

self.searchController = UISearchController (пошукРезультатиКонтролер: рецептListViewController)

Нам потрібно використовувати definesPresentationContext на контролері подання джерела (контролер перегляду, куди ми додаємо рядок пошуку). Без цього ми отримуємо searchResultsController, який буде представлений на весь екран !!!

Використовуючи стиль currentContext або overCurrentContext для представлення контролера перегляду, це властивість керує, який існуючий контролер перегляду в ієрархії контролера перегляду фактично охоплюється новим вмістом. Коли відбувається презентація, що базується на контексті, UIKit запускається з подаючого контролера подання та здійснює ієрархію контролера перегляду. Якщо він знайде контролер перегляду, значення якого для цього властивості є істинним, він просить цей контролер подання представити новий контролер перегляду. Якщо жоден контролер представлення не визначає контекст презентації, UIKit просить контролер кореневого вікна вікна обробляти презентацію.
Значення за замовчуванням для цього властивості хибне. Деякі контролери подання системи, такі як UINavigationController, змінюють значення за замовчуванням на істинне.

Дебютування пошукових дій

Ми не повинні виконувати пошукові запити для кожного ключового обведення типів користувачів на панелі пошуку. Тому потрібне якесь дроселювання. Ми можемо використовувати DispatchWorkItem, щоб інкапсулювати дію та надіслати її до черги. Пізніше ми можемо скасувати це.

випускник випускного класу {
  приватна відстрочка відпуску: TimeInterval
  приватний var workItem: DispatchWorkItem?
init (затримка: TimeInterval) {
    self.delay = затримка
  }
/// Запустіть дію після деякої затримки
  графік функцій (дія: @escaping () -> недійсний) {
    workItem? .cancel ()
    workItem = DispatchWorkItem (блок: дія)
    DispatchQueue.main.asyncAfter (термін: .now () + затримка, виконання: workItem!)
  }
}

Тестування дебютації із перевернутим очікуванням

Для тестування Debouncer ми можемо використовувати очікування XCTest у перевернутому режимі. Детальніше про це читайте в модульному тестуванні асинхронного коду Swift.

Щоб перевірити, чи не виникає ситуація під час тестування, створіть очікування, яке виконується, коли станеться несподівана ситуація, і встановіть його властивість isInverted на істинне. Ваш тест буде негайно завершений, якщо перевернуте очікування виконано.
клас DebouncerTests: XCTestCase {
  func testDebouncing () {
    нехай cancelExpectation = self.expectation (опис: "скасувати")
    cancelExpectation.isInverted = вірно
нехай completeExpectation = self.expectation (опис: "завершено")
    нехай debuncer = Debouncer (затримка: 0,3)
debouncer.schedule {
      cancelExpectation.fulfill ()
    }
debouncer.schedule {
      completeExpectation.fulfill ()
    }
зачекайте (для: [скасуватиЕкспедиція, завершенеочікування], час очікування: 1)
  }
}

Тестування інтерфейсу користувача з UITests

Іноді невеликий рефакторинг може мати великий ефект. Відключена кнопка може призвести до появи непридатних екранів. UITest допомагає забезпечити цілісність та функціональні аспекти програми. Тест повинен бути декларативним. Ми можемо використовувати шаблон роботи.

Рецепти класуUITEST: XCTestCase {
  var додаток: застосування XCUIA!
  переопределити функцію setUp () {
    super.setUp ()
    ContinuAfterFailure = false
    app = XCUIAзастосування ()
  }
  func testScrolling () {
    app.launch ()
    нехай collectionView = app.collectionViews.element (linkedBy: 0)
    collectionView.swipeUp ()
    collectionView.swipeUp ()
  }
  func testGoToDetail () {
    app.launch ()
    нехай collectionView = app.collectionViews.element (linkedBy: 0)
    нехай firstCell = collectionView.cells.element (linkedBy: 0)
    firstCell.tap ()
  }
}

Ось кілька моїх статей щодо тестування.

  • Запуск UITests з входом у Facebook в iOS
  • Тестування у Swift із заданим малюнком Коли тоді

Головний захисний конец

Доступ до інтерфейсу користувача із чергової фону може призвести до потенційних проблем. Раніше мені потрібно було використовувати MainThreadGuard, тепер, коли Xcode 9 має перевірку Main Thread, я просто включив це в Xcode.

Перевірка головної нитки - це окремий інструмент для мов Swift та C, який виявляє недійсне використання AppKit, UIKit та інших API на фоновому потоці. Оновлення інтерфейсу користувача на потоці, відмінному від основного, є поширеною помилкою, яка може призвести до пропущених оновлень інтерфейсу, пошкоджень візуальної системи, пошкодження даних та збоїв.

Вимірювання виступів та питань

Ми можемо використовувати інструменти для ретельного профілювання програми. Для швидкого вимірювання ми можемо перейти на вкладку Навігатор налагодження і побачити використання процесора, пам'яті та мережі. Перегляньте цю класну статтю, щоб дізнатися більше про інструменти.

Прототипи з дитячим майданчиком

Ігровий майданчик - рекомендований спосіб прототипу та створення додатків. На WWDC 2018, Apple представила Create ML, який підтримує Playground для тренування моделі. Перегляньте цю класну статтю, щоб дізнатися більше про розвиток спортивних майданчиків у Swift.

Куди піти звідси

Дякуємо, що зробили це поки що. Сподіваюся, ви дізналися щось корисне. Найкращий спосіб чогось навчитися - це просто зробити. Якщо вам трапляється писати один і той же код знову і знову, зробіть його як компонент. Якщо проблема несе важкий час, напишіть про це. Поділіться своїм досвідом зі світом, ви багато чого навчитесь.

Рекомендую переглянути статтю Кращі місця для вивчення iOS, щоб дізнатися більше про розробку iOS.

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

Якщо вам подобається ця публікація, подумайте про відвідування інших моїх статей та програм