Научете най-добрите практики на 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 в настройката Project вместо целевите настройки. Това означава, че всички цели в проекта споделят една и съща версия Swift (4.1).

Минимална версия на iOS за поддръжка

От лято 2018 г. iOS 12 е в обществена бета 5 и не можем да насочваме iOS 12 без Xcode 10. В тази публикация използваме Xcode 9, а основният SDK е iOS 11. В зависимост от изискването и потребителските бази, някои приложения трябва да поддържат стари версии на iOS. Въпреки че потребителите на iOS са склонни да приемат нови версии на iOS по-бързо от тези, които използват Android, има някои, които остават със старите версии. Според съветите на Apples трябва да поддържаме двете най-нови версии, които са iOS 10 и iOS 11. Както е измерено от App Store на 31 май 2018 г., само 5% от потребителите използват iOS 9 и преди.

Насочването на нови версии на iOS означава, че можем да се възползваме от нови SDK, които инженерите на Apple подобряват всяка година. Уебсайтът на разработчиците на Apple има подобрен изглед на дневника за промени. Сега е по-лесно да видите какво е добавено или променено.

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

Организиране на проекта Xcode

Когато създаваме новия проект, изберете както „Включване на тестове на единици“, така и „Включване на тестове за потребителски интерфейс“, тъй като е препоръчителна практика да се пишат тестове рано. Последните промени в рамката на XCTest, особено в тестовете на потребителския интерфейс, правят теста полъх и са доста стабилни.

Преди да добавите нови файлове към проекта, направете пауза и помислете за структурата на приложението си. Как искаме да организираме файловете? Имаме няколко възможности. Можем да организираме файлове по функция / модул или роля / видове. Всеки има своите плюсове и минуси и ще ги обсъдя по-долу.

По роля / тип:

  • Плюсове: Има по-малко мислене за това къде да поставите файлове. Също така е по-лесно да прилагате скриптове или филтри.
  • Минуси: Трудно е да се свърже, ако искаме да намерим няколко файла, свързани с една и съща функция. Ще отнеме време и за реорганизиране на файлове, ако искаме да ги превърнем в компоненти за многократна употреба в бъдеще.

По функция / модул

  • Плюсове: Това прави всичко модулно и насърчава състава.
  • Минуси: Може да се обърка, когато много файлове от различни видове са групирани заедно.

Да останеш модулен

Лично аз се опитвам да организирам кода си по функции / компоненти, доколкото е възможно. Това улеснява идентифицирането на свързания код за поправяне и добавянето на нови функции по-лесно в бъдеще. Той отговаря на въпроса „Какво прави това приложение?“ Вместо „Какъв е този файл?“ Ето една добра статия относно това.

Добро правило е да бъдете последователни, независимо коя структура изберете.

Структура на приложението за рецепти

По-долу е структурата на приложението, което използва нашето рецептно приложение:

източник

Съдържа файлове с изходен код, разделени на компоненти:

  • Характеристики: основните функции в приложението
  • Начало: началният екран, показващ списък с рецепти и открито търсене
  • Списък: показва списък с рецепти, включително презареждане на рецепта и показване на празен изглед, когато рецепта не съществува
  • Търсене: обработка на търсене и разглобяване
  • Детайл: показва подробна информация

Библиотека

Съдържа основните компоненти на нашето приложение:

  • Поток: съдържа FlowController за управление на потоци
  • Адаптер: общ източник на данни за UICollectionView
  • Разширение: удобни разширения за обикновени операции
  • Модел: Моделът в приложението, анализиран от JSON

средство

Съдържа файлове с плисти, ресурси и Storyboard.

Кодекси за конвенции

Съгласен съм с повечето ръководства за стил в raywenderlich / swift-style-guide и github / swift-style-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) в контрола на източника, за да не претрупвам кодовата база. Освен това прави по-лесно преглеждането на заявките за изтегляне.

Независимо дали проверявате или не в директорията Pods, Podfile и Podfile.lock винаги трябва да се държат под контрол на версиите.

Използвам и iTerm2 за изпълнение на команди и Source Tree за преглед на клонове и стъпване.

Зависимостите

Използвал съм рамки на трети страни, а също така направих и допринесох за отворен код много. Използването на рамка ви дава тласък в началото, но също така може да ви ограничи много в бъдеще. Възможно е да има някои тривиални промени, с които е много трудно да се работи. Същото се случва и при използване на SDK. Предпочитанието ми е да подбирам активни рамки с отворен код. Прочетете изходния код и проверете внимателно рамките и се консултирайте с вашия екип, ако смятате да ги използвате. Малко допълнителна предпазливост не вреди.

В това приложение се опитвам да използвам възможно най-малко зависимости. Достатъчно, за да демонстрира как да управляваш зависимостите. Някои опитни разработчици могат да предпочетат Carthage, мениджър на зависимости, тъй като ви дава пълен контрол. Тук избирам CocoaPods, тъй като лесният му за употреба, и той работи чудесно досега.

В корена на проекта има файл, наречен .swift-версия на стойност 4.1, който казва на CocoaPods, че този проект използва Swift 4.1. Това изглежда просто, но ми отне доста време да разбера.

Влизане в проекта

Нека изработим няколко стартови изображения и икони, за да дадем на проекта хубав външен вид.

API

Лесният начин да научите мрежите на iOS е чрез публични безплатни API услуги. Тук използвам food2fork. Можете да се регистрирате за акаунт на http://food2fork.com/about/api. Има много други страхотни API в този публичен api хранилище.

Добре е да запазите идентификационните си данни на сигурно място. Използвам 1Password за генериране и съхраняване на паролите си.

Преди да започнем да кодираме, нека да си поиграем с API-тата, за да видим какви искания изискват и отговорите, които връщат. Използвам инструмента Insomnia за тестване и анализ на отговорите на API. Той е с отворен код, безплатен и работи чудесно.

Стартиране на екрана

Първото впечатление е важно, така е и стартовият екран. Предпочитаният начин е използването на LaunchScreen.storyboard вместо статично изображение на стартиране.

За да добавите стартово изображение към каталог на активи, отворете 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.

За да го инсталирате, добавете pod 'SwiftLint', '~> 0.25' към Podfile. Също така е добра практика да определите версията на зависимостите, така че инсталацията на pod случайно няма да се актуализира до основна версия, която може да счупи приложението ви. След това добавете .swiftlint.yml с предпочитаната от вас конфигурация. Примерна конфигурация можете да намерите тук.

И накрая, добавете нова фраза за изпълнение на сценарии, за да изпълните swiftlint след компилиране.

Тип-безопасен ресурс

Използвам R.swift за безопасно управление на ресурсите. Той може да генерира класове, безопасни за достъп, за достъп до шрифт, локализиращи низове и цветове. Всеки път, когато променим имената на ресурсите, получаваме грешки при компилиране вместо неявен срив. Това ни пречи да извеждаме ресурси, които се използват активно.

imageView.image = R.image.notFound ()

Покажете ми кода

Нека се потопим в кода, като започнем от модела, контролерите на потока и сервизните класове.

Проектиране на модела

Може да звучи скучно, но клиентите са просто по-хубав начин да представят отговора на API. Моделът е може би най-основното нещо и го използваме много в приложението. Той играе толкова важна роля, но може да има някои очевидни грешки, свързани с неправилни модели и предположения за това как трябва да се анализира модел, които трябва да бъдат разгледани.

Трябва да тестваме за всеки модел на приложението. В идеалния случай се нуждаем от автоматизирано тестване на модели от API отговори, в случай че моделът се е променил от задния ред.

Започвайки от Swift 4.0, можем да съобразим модела си с Codable за лесно сериализиране на и от JSON. Нашият модел трябва да бъде неизменен:

структура Рецепта: Codable {
  нека издател: String
  нека URL: URL
  нека sourceUrl: String
  нека id: String
  нека заглавие: String
  нека imageUrl: String
  нека socialRank: Double
  нека 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: [String: Any] = [
      "издател": "Два граха и тяхната шушулка",
      "f2f_url": "http://food2fork.com/view/975e33",
      "заглавие": "Шоколадово фъстъчено масло без печене Брецъл бисквитки",
      "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, от: data)
XCTAssertEqual (recept.title, "Шоколадово фъстъчено масло без печене Кретчеви бисквитки")
    XCTAssertEqual (recept.id, "975e33")
    XCTAssertEqual (recept.url, URL (низ: "http://food2fork.com/view/975e33")!)
  }
}

По-добра навигация с FlowController

Преди използвах Compass като двигател за маршрутизиране в проектите си, но с течение на времето установих, че писането на прост код за маршрутизация също работи.

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 = {[слабо себе си] рецепта в
      самостоятелно? .portDetail (рецепта: рецепта)
    }
  }
private func startDetail (рецепта: Рецепта) {}
  private func startWeb (url: URL) {}
}

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

Автоматично оформление

Автоматичното оформление съществува от iOS 5, с всяка година става по-добро. Въпреки че някои хора все още имат проблем с него, най-вече заради объркване на нарушаването на ограниченията и производителността, но лично аз смятам, че Auto Layout е достатъчно добър.

Опитвам се да използвам автоматичното оформление възможно най-много, за да направя адаптивен потребителски интерфейс. Можем да използваме библиотеки като Котви, за да правим декларативни и бързи автоматични оформления. В това приложение обаче просто ще използваме NSLayoutAnchor, тъй като е от iOS 9. Кодът по-долу е вдъхновен от ограничението. Не забравяйте, че автоматичното оформление в най-простата си форма включва превключване на преводиAutoresizingMaskIntoConstraints и активиране на isActive ограничения.

разширение NSLayoutConstraint {
  static func activate (_ ограничения: [NSLayoutConstraint]) {
    constraints.forEach {
      ($ 0.firstItem като? UIView) ?. превеждаAutoresizingMaskIntoConstraints = false
      $ 0.isActive = true
    }
  }
}

Всъщност в 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 = true
    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 = true
    връщане на изображениеView
  }
}

Това също съответства на съветите на стремеж за свободно използване.

Започнете имената на фабричните методи с „make“, например, x.makeIterator ().

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

Синтаксис на код е трудно запомнящ се. Помислете дали да използвате кодови фрагменти за автоматично генериране на код. Това се поддържа от Xcode и е предпочитаният начин от инженерите на Apple, когато те демонстрират.

ако #available (iOS 11, *) {
  viewController.navigationItem.searchController = searchController
  viewController.navigationItem.hidesSearchBarWhenScrolling = false
} else {
  viewController.navigationItem.titleView = търсенеController.searchBar
}

Направих репо с няколко полезни фрагмента на Swift, които мнозина се радват да използват.

Работа в мрежа

Мрежата в Swift е вид решен проблем. Има досадни и склонни към грешки задачи като анализиране на HTTP отговори, обработка на опашки за заявки, обработка на заявки с параметри. Виждах грешки около PATCH заявки, по-ниски HTTP методи ... Просто можем да използваме Alamofire. Тук няма нужда да губите време.

За това приложение, тъй като е просто и да избегнете ненужни зависимости. Просто можем да използваме URLSession директно. Ресурсът обикновено съдържа URL, път, параметри и HTTP метод.

Stru Resource {
  нека URL: URL
  нека пътека: String?
  нека httpMethod: String
  нека параметри: [String: String]
}

Една проста мрежова услуга може просто да анализира ресурса до URLRequest и казва на URL сесията да се изпълни

последен клас NetworkService: Мрежи {
  @discardableResult func fetch (ресурс: ресурс, завършване: @escaping (данни?) -> void) -> URLSessionTask? {
    охрана нека искане = 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 (данни?) -> void) -> URLSessionTask?
}
последен клас MockNetworkService: Мрежи {
  нека данни: Данни
  init (fileName: String) {
    нека пакет = пакет (за: MockNetworkService.self)
    нека url = bundle.url (forResource: fileName, withExtension: "json")!
    self.data = опитайте! Данни (contentOf: url)
  }
func fetch (ресурс: ресурс, завършване: @escaping (данни?) -> void) -> URLSessionTask? {
    завършване (данни)
    връщане нула
  }
}

Реализиране на кеш за офлайн поддръжка

Преди много допринасях и използвах библиотека, наречена Кеш. Това, от което се нуждаем от добра кеш библиотека, е памет и дисков кеш, памет за бърз достъп, диск за постоянство. Когато спестяваме, спестяваме както в паметта, така и на диска. Когато зареждаме, ако кешът на паметта не успее, се зареждаме от диска, след което обновяваме паметта отново. Има много разширени теми за кеша като пречистване, изтичане, честота на достъп. Прочетете за тях тук.

В това просто приложение, класът за обслужване на кеш в домашни условия е достатъчен и добър начин да научите как работи кеширането. Всичко в Swift може да бъде преобразувано в Data, така че можем просто да запишем Data в кеш. Swift 4 Codable може да сериализира обект към Data.

Кодът по-долу ни показва как да използваме FileManager за кеш на дискове.

/// Запазване и зареждане на данни в паметта и дисковия кеш
последен клас CacheService {
/// За получаване или зареждане на данни в паметта
  частна нека памет = NSCache  ()
/// URL адресът на пътя, който съдържа кеширани файлове (mp3 файлове и файлове с изображения)
  private let 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 (данни: данни, ключ: низ, завършване: (() -> void)? = nil) {
    пуснете ключ = MD5 (ключ)
serialQueue.async {
      self.memory.setObject (данни като NSData, forKey: ключ като NSString)
      направете {
        опитайте data.write (към: self.filePath (ключ: ключ))
        завършване? ()
      } улов {
        печат (грешка)
      }
    }
  }
}

За да избегнем неправилни и много дълги имена на файлове, можем да ги хешираме. Използвам MD5 от SwiftHash, което дава мъртъв просто използване let key = MD5 (ключ).

Как да тествате кеша

Тъй като аз проектирам кеш операциите да бъдат асинхронни, трябва да използваме тестово очакване. Не забравяйте да нулирате състоянието преди всеки тест, така че предишното тестово състояние да не пречи на текущия тест. Очакването в XCTestCase прави тестване на асинхронен код по-лесно от всякога.

клас CacheServiceTests: XCTestCase {
  нека услуга = CacheService ()
замени функцията setUp () {
    super.setUp ()
опитвам? service.clear ()
  }
func testClear () {
    нека очакване = self.expectation (описание: # функция)
    let string = "Здравей свят"
    нека data = 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) {
          // Запазване в кеш
          самостоятелно?. cacheService.save (данни: данни, ключ: url.absoluteString)
          DispatchQueue.main.async {
            завършване (изображение)
          }
        } else {
          print ("Грешка при зареждане на изображение при \ (url)")
        }
      })
самостоятелно? .task? .resume ()
    }
  })
}

Направете зареждането на изображението по-удобно за UIImageView

Нека добавим разширение към UIImageView, за да зададем отдалеченото изображение от URL адреса. Използвам свързан обект, за да запазя тази ImageService и да отменя стари заявки. Ние използваме добре свързания обект, за да прикачим ImageService към UIImageView. Въпросът е да отмените текущата заявка, когато заявката се задейства отново. Това е удобно, когато изгледите на изображения са изобразени в превъртащ се списък.

разширение UIImageView {
  func setImage (url: URL, заместител на място: UIImage? = нула) {
    ако imageService == nil {
      imageService = ImageService (networkService: NetworkService (), cacheService: CacheService ())
    }
self.image = заместител
    self.imageService? .fetch (url: url, завършване: {[слабо себе си] изображение в
      аз? .image = изображение
    })
  }
частна var imageService: ImageService? {
    получи {
      върнете objc_getAssociatedObject (самостоятелно и 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 config: ((T, Cell) -> void)?
  var select: ((T) -> void)?
  var cellHeight: CGFloat = 60
}

Контролер и изглед

Отпаднах от „Спортборд“ поради много ограничения и много проблеми. Вместо това използвам код, за да правя изгледи и да определям ограничения. Не е толкова трудно да се следва. По-голямата част от кодовата платка в UIViewController е за създаване на изгледи и конфигуриране на оформлението. Нека да преместим тези в изгледа. Можете да прочетете повече за това тук.

/// Използва се за разделяне между контролер и изглед
клас BaseController : UIViewController {
  нека корен = T ()
замени функцията loadView () {
    изглед = корен
  }
}
краен клас 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

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

Защита за транспорт на приложения

От iOS 9 всички приложения трябва да приемат App Transport Security

Защита за транспорт на приложения (ATS) налага най-добрите практики в защитените връзки между приложението и неговия заден край. ATS предотвратява случайно разкриване, осигурява сигурно поведение по подразбиране и е лесен за възприемане; той също е включен по подразбиране в iOS 9 и OS X v10.11. Трябва да приемете ATS възможно най-скоро, независимо от това дали създавате ново приложение или актуализирате съществуващо.

В нашето приложение някои изображения се получават чрез 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 (equTo: leftAnchor),
      scrollView.rightAnchor.constraint (EquTo: rightAnchor),
contentView.topAnchor.constraint (равно на: scrollView.topAnchor),
      contentView.bottomAnchor.constraint (равно на: scrollView.bottomAnchor),
      contentView.leftAnchor.constraint (equTo: leftAnchor),
      contentView.rightAnchor.constraint (EquTo: rightAnchor)
    ])
  }
}

Прикрепяме UIScrollView към краищата. Ние приковаваме лявата и дясната котва на contentView, като същевременно закрепваме горната и долната котва на contentView към UIScrollView.

Изгледите вътре в contentView имат горни и долни ограничения, така че когато се разширят, те разширяват и contentView. UIScrollView използва информация за автоматичното оформление от това contentView, за да определи неговия contentSize. Ето как ScrollableView се използва в RecipeDetailView.

scrollableView.setup (двойки: [
  ScrollableView.Pair (изглед: imageView, вмъкване: UIEdgeInsets (горе: 8, вляво: 0, долу: 0, вдясно: 0)),
  ScrollableView.Pair (изглед: компонентHeaderView, вмъкване: UIEdgeInsets (горе: 8, вляво: 0, отдолу: 0, вдясно: 0)),
  ScrollableView.Pair (изглед: sastoineLabel, вмъкване: UIEdgeInsets (горе: 4, вляво: 8, отдолу: 0, вдясно: 0)),
  ScrollableView.Pair (изглед: infoHeaderView, вмъкване: UIEdgeInsets (горе: 4, вляво: 0, отдолу: 0, вдясно: 0)),
  ScrollableView.Pair (изглед: инструкция Бутон, вмъкване: UIEdgeInsets (горе: 8, вляво: 20, отдолу: 0, вдясно: 20)),
  ScrollableView.Pair (изглед: оригинален бутон, вмъкване: UIEdgeInsets (горе: 8, вляво: 20, отдолу: 0, вдясно: 20)),
  ScrollableView.Pair (изглед: infoView, вмъкване: UIEdgeInsets (горе: 16, вляво: 0, долу: 20, вдясно: 0))
])

Добавяне на функционалност за търсене

От iOS 8 нататък можем да използваме UISearchController, за да получим опит за търсене по подразбиране с лентата за търсене и контролера на резултатите. Ще включим функционалността за търсене в SearchComponent, така че тя да може да бъде включена.

последен клас SearchComponent: NSObject, UISearchResultsUpdating, UISearchBarDelegate {
  нека рецептиService: РецептиService
  нека searchController: UISearchController
  нека receptListViewController = RecipeListViewController ()
}

Започвайки от iOS 11, на UINavigationItem има свойство, наречено searchController, което улеснява показването на лентата за търсене на лентата за навигация.

func add (за прегледController: UIViewController) {
  ако #available (iOS 11, *) {
    viewController.navigationItem.searchController = searchController
    viewController.navigationItem.hidesSearchBarWhenScrolling = false
  } else {
    viewController.navigationItem.titleView = търсенеController.searchBar
  }
viewController.definesPresentationContext = true
}

В това приложение засега трябва да деактивираме hidesNavigationBarDuringPresentation, тъй като е доста бъг. Надяваме се, че ще бъде решен в бъдещи актуализации на iOS.

Разбиране на контекста на презентацията

Разбирането на контекста на презентация е от решаващо значение за представянето на контролер на изглед. При търсене използваме searchResultsController.

self.searchController = UISearchController (searchResultsController: receptListViewController)

Трябва да използваме definesPresentationContext на контролера за изглед на източник (контролера на изгледа, в който добавяме лентата за търсене). Без това получаваме searchResultsController да бъде представен на цял екран !!!

Когато използвате стила currentContext или overCurrentContext за представяне на контролер на изглед, това свойство контролира кой съществуващ контролер на изглед във вашата йерархия на контролера на изглед всъщност се покрива от новото съдържание. Когато се появи контекстно представяне, UIKit стартира от представения контролер на изглед и разширява йерархията на контролера на изгледа. Ако намери контролер на изглед, чиято стойност за това свойство е вярна, той изисква от този контролер да представи новия контролер на изглед. Ако никой контролер на изглед не дефинира контекста на презентацията, UIKit моли контролера на изглед на прозореца да обработва презентацията.
Стойността по подразбиране за това свойство е невярна. Някои предоставени от системата контролери за изглед, като UINavigationController, променят стойността по подразбиране на истинска.

Отказ от действия за търсене

Не трябва да изпълняваме заявки за търсене за всеки ключов ход на потребителските типове в лентата за търсене. Следователно е необходимо някакво дроселиране. Можем да използваме DispatchWorkItem, за да капсулираме действието и да го изпратим на опашката. По-късно можем да го отменим.

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

Тестване на разделяне с обърнато очакване

За да тестваме Debouncer можем да използваме XCTest очакване в обърнат режим. Прочетете повече за това в Unit тестване на асинхронен код Swift.

За да проверите дали дадена ситуация не се случи по време на тестване, създайте очакване, което се изпълнява, когато възникне неочакваната ситуация, и задайте свойството муInInverted на true. Тестът ви ще се провали веднага, ако обърнатото очакване е изпълнено.
клас DebouncerTests: XCTestCase {
  func testDebouncing () {
    нека cancelExpectation = self.expectation (описание: "отмени")
    cancelExpectation.isInverted = true
нека completeExpectation = self.expectation (описание: "завършен")
    нека debuncer = Debouncer (забавяне: 0.3)
debouncer.schedule {
      cancelExpectation.fulfill ()
    }
debouncer.schedule {
      completeExpectation.fulfill ()
    }
изчакайте (за: [cancelExpectation, completeExpectation], изчакване: 1)
  }
}

Тестване на потребителски интерфейс с UITests

Понякога малкият рефакторинг може да има голям ефект. Един деактивиран бутон може да доведе до неизползваеми екрани след това. UITest помага да се гарантира целостта и функционалните аспекти на приложението. Тестът трябва да бъде декларативен. Можем да използваме модела Robot.

клас РецептиУИТести: XCTestCase {
  вар приложение: 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 има главна тема за проверка, току-що активирах това в Xcode.

Проверката на главните нишки е самостоятелен инструмент за езици на Swift и C, който открива невалидно използване на AppKit, UIKit и други API на фонова нишка. Актуализирането на потребителски интерфейс на тема, различна от основната нишка, е често срещана грешка, която може да доведе до пропуснати актуализации на потребителския интерфейс, визуални дефекти, повреждане на данни и сривове.

Измерване на представления и проблеми

Можем да използваме инструменти за цялостно профилиране на приложението. За бързо измерване можем да се насочим към раздела Debug Navigator и да видим използването на процесора, паметта и мрежата. Вижте тази готина статия, за да научите повече за инструментите.

Прототипиране с детска площадка

Детската площадка е препоръчителният начин за прототипиране и изграждане на приложения. На WWDC 2018, Apple представи Create ML, който поддържа Playground за обучение на модел. Вижте тази готина статия, за да научите повече за развитието, насочено към детската площадка в Swift.

Къде да отида от тук

Благодаря, че го направихте дотук. Надявам се да сте научили нещо полезно Най-добрият начин да научите нещо е просто да го направите. Ако случайно пишете един и същ код отново и отново, направете го като компонент. Ако проблемът ви затруднява, пишете за това. Споделете опита си със света, ще научите много.

Препоръчвам да разгледате статията Най-добри места за научаване на разработката на iOS, за да научите повече за iOS.

Ако имате въпроси, коментари или отзиви, не забравяйте да ги добавите в коментарите. И ако сте намерили тази статия за полезна, не забравяйте да ръкопляскате.

Ако ви харесва тази публикация, помислете за посещение на другите ми статии и приложения