Архитектура ios: Архитектурные паттерны в iOS / Блог компании Badoo / Хабр

iOS-архитектура для студии аутсорс разработки / Хабр

Хороший код начинается с архитектуры, и iOS-приложения не исключение. Есть много стандартных паттернов, но цель этой статьи рассказать не о них, а об опыте адаптации одного из них и выработке собственного. Мы назвали эту адаптацию HandsAppMVP.

В iOS-разработке архитектура в первую очередь определяет организацию классов и зависимостей для одного конкретного ViewController. Впрочем, центральным компонентом может выступать не только он, но и просто UIView. Выбор зависит от конкретной задачи.

Содержание

Сравнение архитектур


Существует несколько стандартных архитектурных шаблонов для iOS: MVC, MVP, MVVM, VIPER (ссылки на описание каждого можно найти в конце статьи).

Выбирая архитектуру для разработки, мы выделили основные параметры, которым она должна соответствовать: скорость разработки, гибкость и низкий порог входа. Далее мы занялись сравнением трех известных архитектур с учетом этих параметров (шаблон MVC iOS-комьюнити давно закопало из-за грубого несоблюдения single responsibility).

Для аутсорс-команды особенно важна скорость разработки. VIPER — самая сложная и «медленная» архитектура, быстрее разработка идет с применением чистого MVP или MVVM, так как в них меньше компонентов.

Гибкость подразумевает безболезненное добавление или удаление функционала в приложении. Этот параметр сильно коррелирует со скоростью разработки на всех этапах жизни приложения, кроме начального. Также гибкость тесно связана с простотой тестирования — автоматические тесты дают разработчику уверенность в том, что он ничего не сломает, и позволяют избежать багов. Классическая MVP плохо покрывается тестами, особенно если не использовать рассмотренные далее интерфейсы классов. MVVM с точки зрения тестирования также имеет плохие показатели, потому что тестирование реактивного кода занимает значительно больше времени. VIPER отлично подходит для написания тестов, потому что в нем максимально соблюдается принцип единственной ответственности и классы зависят от абстракций.

И последний параметр, который мы рассматривали, — порог входа. Он показывает, насколько быстро новые разработчики (в первую очередь — джуны) вникают в архитектуру. Здесь MVVM с применением сторонних реактивных библиотек (RxSwift, PromiseKit и т. п.) занимает почетное последнее место по очевидным причинам. VIPER также довольно сложная архитектура в силу большого количества компонентов. MVP имеет самый низкий порог входа.

Взвесив все за и против, мы пришли к выводу, что нам нужно что-то такое же простое, как MVP, и такое же гибкое, как VIPER. Так и родилась идея создать на их основе свою архитектуру — HandsAppMVP.

Расширяем MVP


Основные компоненты нашей архитектуры — Model, View, Presenter. Они выполняют те же функции, что и в классической MVP по известной схеме:


[Схема классического MVP]

Здесь и далее на схемах каждый компонент взаимодействия (синий квадрат) — это класс, время жизни которого совпадает с временем жизни View. Сплошная стрелка обозначает владение другим объектом, строгую ссылку, а пунктирная — слабую ссылку. С помощью слабых ссылок мы предотвращаем циклические зависимости и утечку памяти.

Интерфейсы


Первым делом мы добавили в эту классическую схему интерфейсы ViewInput и ViewOutput. Учли пятый принцип SOLID — принцип инверсии зависимостей. Он является скорее не дополнением, а уточнением для MVP. Зависимость от абстракций помогает избавиться от строгой связанности компонентов и позволяет нормально писать тесты. Схема с учетом интерфейсов выглядит так:


[Добавление интерфейсов ViewInput и ViewOutput]

Маленький прямоугольник — интерфейс.

Внимательный разработчик спросит, где интерфейсы для Model? Сейчас к ним переходим.

Работа с данными


Модель данных в мобильных архитектурах — собирательное понятие. Стандартный пример: приложение стучится в сеть для взаимодействия с сервером, затем сохраняет данные в CoreData для офлайн-работы, некоторую простую информацию записывает в UserDefaults и хранит JWT в Keychain. Все эти данные, с которыми ведется взаимодействие, составляют Model.

Класс, который отвечает за взаимодействие с контейнером данных конкретного типа, мы называем сервисом данных. Для каждого контейнера (удаленная база данных, локальная база данных, UserDefaults и пр.) в HandsAppMVP добавляется сервисный класс, который взаимодействует с презентером. Теперь можно также добавить интерфейсы input/output для каждого сервиса данных:


[Добавление сервисов для работы с данными]

Не каждый сервисный класс необходимо подключать к презентеру с помощью интерфейса, как, например, при использовании Moya. Moya — open-source-библиотека для работы с сетью. Moya предоставляет готовый сервисный класс (MoyaProvider), и при написании тестов нам не приходится делать mock-объект, заменяющий ApiProvider. В Moya предусмотрен специальный тестовый режим, при включении которого MoyaProvider не стучится в сеть, а возвращает тестовые данные (подробнее можно почитать по ссылке). Презентер при этом ссылается не на абстракцию MoyaProvider, а на реализацию. А обратную связь от этого сервиса мы получаем с помощью замыканий. Пример реализации можно посмотреть в демопроекте.

Этот пример скорее исключение, чем правило, и показывает, что беспрекословное соблюдение SOLID не всегда лучшее решение.

Навигация


Навигацию в приложении мы рассматриваем как отдельную ответственность. Для нее в HandsAppMVP используется специальный класс — Router. Router содержит weak-ссылку на View, с помощью которой может показать новый экран или закрыть текущий. Router также взаимодействует с презентером c помощью интерфейса RouterInput:


[Добавление компонента для навигации (Router)]

Сборка компонентов


Последнее дополнение классического MVP, которое мы используем, это Assembly — класс-сборщик. Он используется для инициализации View и остальных компонентов HandsAppMVP, а также для внедрения зависимостей. Assembly содержит единственный открытый метод — `assemble() -> UIViewController`, результатом выполнения которого является нужный UIViewController (или UIView) c необходимым графом зависимостей.

Мы не будем показывать Assembly на схеме архитектуры, так как он не связан с компонентами MVP и его жизненный цикл заканчивается сразу после их создания.

Кодогенерация


Для экономии времени мы автоматизировали процесс создания классов HandsAppMVP с помощью Generamba. Используемые шаблоны для Generamba можно найти в нашем репозитории. Пример конфига для Generamba есть в демопроекте.

В результате генерации конкретного экрана получаем набор классов, соответствующий схеме HandsAppMVP, набор unit-тестов для создания и внедрения компонентов, а также шаблонный класс для тестов презентера.

Что получилось


Если сравнить лоб-в-лоб HandsAppMVP и VIPER, то можно заметить, что они очень похожи и первая отличается только отсутствием компонента Interactor. Но, избавившись от прослойки между сервисами и презентом (интерактора), а также упростив взаимодействие с сетью с помощью Moya, мы получили ощутимый прирост скорости разработки.

Советуем уделять архитектуре достаточно внимания на стадии проектирования, чтобы в дальнейшем избежать глобальных ошибок, споров с заказчиками и мучений разработчиков, а вместо всего этого грамотно и прогнозируемо вести процесс разработки.

Помните, что любая архитектура может не подойти конкретно вашему проекту, поэтому не спешите слепо цепляться за готовые шаблоны и успешные истории их применения. Не бойтесь разрабатывать и применять свои решения, — они могут стать для вас более ценными и гибкими, чем уже готовые.

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

  1. Архитектурные паттерны в iOS
  2. iOS Swift: MVP Architecture
  3. Разбор архитектуры VIPER на примере небольшого iOS-приложения на Swift 4
  4. Реализация MVVM в iOS с помощью RxSwift

Также очень помогла и вдохновила открытая документация компании SurfStudio.

Наконец, прикладываем ссылку на демопроект, написанный на HandsAppMVP, который мы не раз упоминали в статье.

Архитектурные подходы в iOS-приложениях / Блог компании Агентство AGIMA / Хабр

Сегодня поговорим об архитектурных подходах в iOS-разработке, про некоторые нюансы и наработки реализации отдельных вещей. Расскажу, каких подходов придерживаемся мы и немного углубимся в детали.

Сразу раскроем все карты. Мы используем MVVM-R (MVVM + Router).

По сути, это обычный MVVM, в котором навигация между экранами вынесена в отдельный слой – Router, а логика получения данных – в сервисы. Далее рассмотрим наши наработки в реализации каждого слоя.


Почему MVVM, а не VIPER или MVC?

В отличии от MVC в MVVM достаточно разделена ответственность между слоями. В нем нет такого количества «‎обслуживающего»‎ кода, как в VIPER, хотя ViewModel для экранов также закрываются протоколами. Эта архитектура чем-то похожа на VIPER, только Presenter и Interactor объединены во ViewModel, и связи между слоями упрощены за счет применения реактивного программирования и биндингов (мы используем ReactiveSwift).


Entity

Мы используем два слоя моделей данных: первый – привязанный к базе данных (далее managed objects), второй – так называемые plain objects, которые к базе данных не имеют никакого отношения.

Каждая plain-сущность реализует протокол Translatable, который может быть инициализирован из managed object’a и из которого можно создать managed object. В качестве базы данных используем Realm, в нашем случае

ManagedObject – это RealmSwift.Object. Маппинг происходит через Codable: маппятся как plain-объекты и сохраняются как managed-объекты. Далее сервисы и ViewModel работают только с plain-объектами.

protocol Translatable {
    associatedtype ManagedObject: Object

    init(object: ManagedObject)
    func toManagedObject() -> ManagedObject
}

Для сохранения, получения и удаления объектов из базы данных используется отдельная сущность – Storage. Поскольку Storage закрыта протоколом, мы не зависим от реализации конкретной базы данных и при необходимости можем заменить Realm на CoreData.

protocol StorageProtocol {
    func cachedObjects<T: Translatable>() -> [T]
    func object<T: Translatable>(byPrimaryKey key: AnyHashable) -> T?
    func save<T: Translatable>(objects: [T]) throws
    func save<T: Translatable>(object: T) throws
    func delete<T: Translatable>(objects: [T]) throws
    func delete<T: Translatable>(object: T) throws
    func deleteAll<T: Translatable>(ofType type: T.Type) throws
}

Какие плюсы и минусы у такого подхода?

У каждой базы данных есть свои особенности. Например, Realm-объект, уже сохраненный в базу данных, может быть использован в только рамках потока, в котором он был создан. Это доставляет неудобства.

Также, объект может быть удален из базы данных, при этом он лежит в оперативной памяти, и при обращении к нему будет краш. У Core Data такие же особенности. Поэтому мы получаем объекты из базы данных, конвертируем их в plain-объекты и далее работаем с ними.

При таком подходе код становится больше, и его необходимо поддерживать. Без зависимости от особенностей базы данных мы теряем возможность использования крутых фишек. В случае CoreData это FetchedResultsController, где мы можем контролировать все вставки, удаления, изменения в рамках массива сущностей. Примерно такой же механизм у Realm.


Core Components

Core-компоненты – это сущности, которые выполняют одну свою задачу. Например, маппинг, взаимодействие с базой данных, посыл и обработка сетевых запросов. Storage из предыдущего пункта как раз является одним из core-компонентов.


Protocols

Мы активно используем протоколы. Все core-компоненты закрываются протоколами, и есть возможность сделать mock или тестовую реализацию для unit-тестов. Таким образом мы получаем определенную гибкость реализации. Все зависимости передаются в init. При инициализации каждого объекта мы понимаем, какие там зависимости, что он использует внутри себя.


HTTP Client

Сетевой запрос описывается протоколом NetworkRequestParams.

protocol NetworkRequestParams {
    var path: String { get }
    var method: HTTPMethod { get }
    var parameters: Parameters { get }
    var encoding: ParameterEncoding { get }
    var headers: [String: String]? { get }
    var defaultHeaders: [String: String]? { get }
}

Мы используем enum для описания сетевых запросов. Выглядит это так:

enum UserNetworkRouter: URLRequestConvertible {
    case info
    case update(userJson:[String : Any])
}

extension UserNetworkRouter: NetworkRequestParams {
    var path: String {
        switch self {
        case .info:
            return "/users/profile"
        case .update:
            return "/users/update_profile"
        }
    }

    var method: HTTPMethod {
        switch self {
        case .info:
            return .get
        case .update:
            return .post
        }
    }

    var encoding: ParameterEncoding {
        switch self {
        case .info:
            return URLEncoding()
        case .update:
            return JSONEncoding()
        }
    }

    var parameters: Parameters {
        switch self {
        case .info:
            return [:]
        case .update(let userJson):
            return userJson
        }
    }
}

Каждый NetworkRouter реализрует протокол URLRequestConvertible. Отдаем его сетевому клиенту, который преобразует его в URLRequest и использует по своему назначению.

Сетевой клиент выглядит следующим образом:

protocol HTTPClientProtocol {
    func load(request: NetworkRequestParams & URLRequestConvertible) -> SignalProducer<Data, Error>
}

Mapper

Мы используем Codable для маппинга данных.

protocol MapperProtocol {
    func map<MappingResult: Codable>(data: Data, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy) -> SignalProducer<MappingResult, Error>
}

Пуш — уведомления

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


Сервисы

Грубо говоря, один сервис отвечает за одну сущность. Рассмотрим это на примере приложения соцсети. Есть сервер пользователя, который получает пользователя – себя, и отдает измененные сущности, если мы его отредактировали. Есть сервис постов, который получает список постов, детальный пост, сервис платежей и т.д. и т.п.

Все сервисы содержат в себе core-компоненты. Когда мы вызываем метод у сервиса, он начинает дергать различные методы core-компонентов и в итоге отдает результат наружу.

Сервис, как правило, выполняет работу для определенного экрана, вернее для вьюмодели экрана(об этом ниже). Если при уходе с экрана сервис не уничтожится, а продолжит выполнять уже ненужный сетевой запрос и будет тормозить другие запросы. Этим можно управлять вручную, но поддерживать такую систему будет сложнее. Однако, у такого подхода есть и минус: если результат работы сервиса нужен даже после того, как мы вышли с экрана, придется искать другие решения, возможно, делать некоторые сервисы синглтонами.

Сервисы не содержат состояния. Поскольку сервисы не синглтоны, мы можем иметь несколько экземпляров одного сервиса, в котором состояния могут отличаться друг от друга. Это может привести к некорректному поведению.

Пример метода одного из сервисов:

func currentUser() -> SignalProducer<User, Error> {
        let request = UserNetworkRouter.info
        return httpClient.load(request: request)
            .flatMap(.latest, mapUser)
            .flatMap(.latest, save)
    }

ViewModel

ViewModel мы поделим на 2 типа:


  • ViewModel для экрана (ViewController)
  • ViewModel для UIView (в том числе для ячеек таблицы или UICollectionView)

ViewModel для ViewController отвечает за логику работы экрана. Как правило, это отправка сетевых запросов, подготовка данных, реакция на UI-события.

ViewModel подготавливает все данные для view, которые пришли от сервиса. Если пришел список сущностей, то ViewModel трансформирует его в список ViewModel и биндит их на view. Если есть состояния (есть галочка / нет галочки), это тоже управляется и передается во ViewModel.

Также ViewModel управляет логикой навигации. Для навигации существует отдельный слой Router, но команды дает именно ViewModel.

Типичные функции view-модели: получить юзера, обратиться к юзер-сервису, сделать ViewModel из полученного значения. Когда все загрузится, View берет ViewModel и отрисовывает view-ячейку.

ViewModel для экрана закрыта протоколом по тем же соображениям, что и сервисы. Однако есть еще один интересный кейс: например, банковское приложение, где каждое действие (перевод средств, открытие счета, блокировка счета) подтверждается по смс. На экране подтверждения есть поле ввода кода и кнопка «отправить заново».

ViewModel закрыта таким протоколом:

protocol CodeInputViewModelProtocol {
  /// Отправить введенный код
    func send(code: String) -> SignalProducer<Void, Error>
    /// Отправить смс заново
    func resendCode() -> SignalProducer<Void, Error>
}

Во ViewController она хранится в таком виде:

var viewModel: CodeInputViewModelProtocol?

В зависимости от того, что именно мы пытаемся подтвердить по смс, отправка кода и переотправка смс могут быть представлены абсолютно разными запросами, а после подтверждения нужны переходы на разные экраны и т.п. Поскольку ViewController’у без разницы, какой на самом деле тим имеет ViewModel, мы можем иметь несколько реализаций ViewModel для различных кейсов, а UI будет общий.

ViewModel для View и ячеек, как правило, занимается форматированием данных и обработкой пользовательского ввода. Например, хранение состояния «выбрано / не выбрано».

final class FeedCellViewModel {

    let url: URL?
    let title: String
    let subtitle: String

    init(feed: FeedItem) {
        url = URL(string: feed.imageUrl)
        title = feed.title
        subtitle = DateFormatter.feed.string(from feed.publishDate)
    }
}

Навигация

Переходы между экранами осуществляет Router.

class BaseRouter {
    init(sourceViewController: UIViewController) {
        self.sourceViewController = sourceViewController
    }

    weak var sourceViewController: UIViewController?
}

Каждый экран имеет свой роутер, который наследуется от базового. Он имеет методы переходов на конкретные экраны.

final class FeedRouter : BaseRouter {
    func showDetail(viewModel: FeedDetailViewModelProtocol) {
        let vc = FeedDetailViewController()
        vc.viewModel = viewModel
        sourceViewController?.navigationController?.pushViewController(vc, animated: true)
    }
}

Как видно из примера выше, сборка «модуля» происходит в роутере. Это формально противоречит букве S из SOLID, но на практике оказывается довольно удобно и не вызывает проблем.

Бывают случаи, когда один и тот же метод нужен в разных роутерах. Чтобы не писать его несколько раз, создаем протокол, в котором будут общие методы, и реализуем extension к нему. Теперь достаточно подписать нужный роутер на этот протокол, и он будет иметь необходимые методы.

protocol FeedRouterProtocol {
    func showDetail(viewModel: FeedDetailViewModelProtocol)
}

extension FeedRouterProtocol where Self: BaseRouter {
    func showDetail(viewModel: FeedDetailViewModelProtocol) {
        let vc = FeedDetailViewController()
        vc.viewModel = viewModel
        sourceViewController?.navigationController?.pushViewController(vc, animated: true)
    }
}

View

View отвечает традиционно за отображение информации для пользователя и обработку пользовательских действий. В MVVM мы считаем, что ViewController – это View. Важно, чтобы там не было сложной логики, которой место во ViewModel. В любом случае, даже в MVC не стоит нагружать сильно ViewController, хоть сделать это сложно.

View командует ViewModel. Если загрузился ViewController, мы даем команду ViewModel: загрузить данные из сети или из кеша. Также View принимает сигналы с ViewModel. Если ViewModel говорит, что что-то изменилось (например, загрузились те самые данные), то View на это реагирует и перерисовывается.

Мы не используем сториборды. Навигация сильно завязана на ViewController, и это тяжело вписать в архитектуру. В сторибордах зачастую возникают конфликты, править которые – отдельное «удовольствие».


Что делать дальше?

Можно использовать кодогенерацию для моделей (Translatable), поскольку вся инициализация из объекта базы данных в плэйн-объект и наоборот сейчас прописывается вручную.

Также можно использовать более универсальную схему запросов, поскольку много методов сервисов выглядят так: сходи в сеть, примени маппинг, сохрани в базу данных. Это тоже можно универсализировать, задать общий скелет.

Мы с вами рассмотрели архитектурные подходы, однако не стоит забывать о том, что качественное приложение – это не только архитектура, но и плавный, отзывчивый, удобный интерфейс. Любите своих пользователей и пишите качественные приложения.

Общее представление об архитектуре Clean Swift / Хабр

Привет, читатель!

В этой статье я расскажу об архитектуре iOS приложений — Clean Swift. Мы рассмотрим основные теоретические моменты и разберем пример на практике.



Теория


Для начала разберем основную терминологию архитектуры. В Clean Swift приложение состоит из сцен, т.е. каждый экран приложения — это одна сцена. Основное взаимодействие в сцене идет через последовательный цикл между компонентами ViewController -> Interactor -> Presenter. Это называется VIP цикл.

Мостом между компонентами выступает файл Models, который хранит в себе передаваемые данные. Так же есть Router, отвечающий за переход и передачу данных между сценами, и Worker, который берет часть логики Interactor’a на себя.

View


Storyboard’ы, XIB’ы или UI элементы, написанные через код.

ViewController


Отвечает только за конфигурацию и взаимодействие с View. В контроллере не должно находиться никакой бизнес логики, взаимодействия с сетью, вычислений и так далее.
Его задача обрабатывать события с View, отображать или отправлять данные (без обработки и проверок) в Interactor.

Interactor


Содержит в себе бизнес логику сцены.

Он работает с сетью, базой данных и модулями устройства.

Interactor получает запрос из ViewController’a (с данными или пустой), обрабатывает его и, если это требуется, передает новые данные в Presenter.

Presenter


Занимается подготовкой данных для отображения.

Как пример, добавить маску на номер телефона или сделать первую букву в названии заглавной.
Обрабатывает данные, получение из Interactor’a, после чего отправляет их обратно во ViewController.

Models


Набор структур для передачи данных между компонентами VIP цикла. Каждый круг цикла имеет в себе 3 вида структур:
  • Request — Структура с данными (текст из TextField и т.д.) для передачи из ViewController’a в Interactor
  • Response — Структура с данными (загруженными из сети и т.д.) для передачи из Interactor в Presenter
  • ViewModel — Структура с обработанными данными (форматирование текста и т.д.) в Presenter’e для передачи обратно во ViewController

Worker


Разгружает Interactor, забирая на себя часть бизнес логики приложения, если Interactor стремительно разрастается.

Так же можно создавать общие для всех сцен Worker’ы, если их функционал используется в нескольких сценах.

Как пример, в Worker можно выносить логику работы с сетью или базой данных.

Router


В Router выносится вся логика, отвечающая за переходы и передачу данных между сценами.

Для прояснения картины работы VIP цикла приведу стандартный пример — авторизация.
  1. Пользователь ввел свой логин и пароль, нажал на кнопку авторизации
  2. У ViewController срабатывает IBAction, после чего создается структура с введенными, в TextField’ы, данными пользователя (Models -> Request)
  3. Созданная структура передается в метод fetchUser в Interactor’e
  4. Interactor отправляет запрос в сеть и получает ответ об успешности авторизации
  5. На основе полученных данных, создает структуру с результатом (Models -> Response) и передается в метод presentUser в Presenter’e
  6. Presenter форматирует данные по необходимости и возвращает их (Models -> ViewModel) в метод displayUser в ViewController’e
  7. ViewController отображает полученные данные пользователю. В случае с авторизацией, может выводиться ошибка или срабатывать переход на другую сцену с помощью Router

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

Практика


А теперь разберем небольшой практический пример, который покажет как проходит VIP цикл. В этом примере мы сымитируем подгрузку данных при открытии сцены (экрана). Основные участки кода я пометил комментариями.

Весь VIP цикл завязан на протоколах, что обеспечивает возможностью подмены каких-либо модулей без нарушения работы приложения.
Для ViewController’a создается протокол DisplayLogic, ссылка на который передается в Presenter для последующего вызова. Для Interactor’a создаются два протокола BusinessLogic, отвечающий за вызов методов из ViewController’a, и DataSource, для хранения данных и передачу через Router в Interactor другой сцены. Presenter подписывается под протокол PresentationLogic, для вызова из Interactor’a. Связующим элементом всего этого выступает Models. Он содержит в себе структуры, с помощью которых идет обмен информаций между компонентами VIP цикла. С него и начнем разбор кода.

Models


В примере ниже, для сцены Home, я создал файл HomeModels, который содержит в себе набор запросов для VIP цикла.
Запрос FetchUser будет отвечать за подгрузку данных о пользователе, который мы и будем рассматривать дальше.

ViewController


При инициализации класса мы создаем экземпляры классов Interactor’a и Presenter’a этой сцены и устанавливаем зависимости между ними.
Далее во ViewController’e остается ссылка только на Interactor. С помощью этой ссылки мы будем создавать запрос к методу fetchUser(request:) в Interactor’е, для запуска VIP цикла.

Здесь стоит обратить внимание, как происходит запрос к Interactor. В методе loadUserInfromation() мы создаем экземпляр структуры Request, куда передаем начальное значение. Оно может быть взято из TextField, таблицы и так далее. Экземпляр структуры Request передается в метод fetchUser(request:), который находится в протоколе BusinessLogic нашего Interactor’a.


Interactor


Экземпляр класса Interactor’a содержит в себе ссылку на протокол PresentationLogic, под который подписан Presenter.

В методе fetchUser(request:), может содержаться любая логика подгрузки данных. Для примера я просто создал константы, с якобы полученными данными.

В этом же методе создается экземпляр структуры Response и заполняется, полученными ранее, параметрами. Response передается в PresentationLogic с помощью метода presentUser(response:). Другими словами, здесь мы получили сырые данные и передали их на обработку в Presenter.


Presenter


Имеет ссылку на протокол DisplayLogic, под который подписан ViewController. Не содержит никакой бизнес логики, а только форматирует полученные данные перед отображением. В примере мы отформатировали номер телефона, подготовили экземпляр структуры ViewModel и передали его на ViewController, с помощью метода displayUser(viewModel:) в протоколе DisplayLogic, где уже происходит отображение данных.

Заключение


С данной архитектурой мы получили возможность распределить обязанности, улучшить удобство тестирования приложения, ввести заменяемость отдельных участков реализации и стандарт написания кода для работы в команде.

Спасибо, что дочитали до конца.

Серия статей


  1. Общее представление об архитектуре Clean Swift (вы здесь)
  2. Router и Data Passing в архитектуре Clean Swift
  3. Workers архитектуры Clean Swift
  4. Unit тестирование в архитектуре Clean Swift
  5. Пример простого интернет-магазина на архитектуре Clean Swift

Все компоненты сцены: Ссылка
Помощь в написании статьи: Bastien

Пентестинг приложений iPhone – часть 5

Рассматривается динамический анализ iOS-приложений

Автор: satishb3

В первой статье данной серии мы обсуждали анализ трафика приложений iPhone. Вторая, третья и четвертая части были посвящены подробному анализу небезопасных мест хранения данных на iPhone. В данной статье мы рассмотрим динамический анализ приложений iOS (анализ во время выполнения). Динамический анализ позволяет атакующему манипулировать поведением приложения во время его выполнения, чтобы обойти механизмы безопасности и извлечь из памяти конфиденциальные данные. Перед тем, как перейти собственно к анализу, нам сначала нужно рассмотреть архитектуру приложения iOS и защитные механизмы, действующие во время его выполнения.

Архитектура приложений iOS

Приложение iOS представляет собой zip-архив с расширением .ipa. Архив содержит исполняемый двоичный файл и файл iTunesArtwork, который используется iTunes для управления приложением. Ниже показана типичная структура приложения iOS.

Папка в директории Payload, имя которой заканчивается на .app, содержит двоичный файл приложения, все ресурсы приложения вроде изображений и аудиозаписей, профиль, определяющий разрешения приложения, и подпись кода.

Двоичный файл приложения iOS скомпилирован под архитектуру ARM и использует файловый формат Mach-O (mach object). Данный файловый формат состоит из трех основных разделов: заголовка, загрузочных команд и сегментов/секций. Ниже показана структура файлового формата Mach-O.

Структура Mach-O файлов iOS-приложения может быть просмотрена на подвергшемся джейлбрейку устройстве с помощью утилиты otool. Otool доступен из пакета Darwin CC Tools в Cydia.

Заголовок

Раздел позволяет идентифицировать файл Mach-O и содержит основную информацию о типе файла вроде целевой архитектуры и флагов, влияющих на интерпретацию остальной части файла. Чтобы просмотреть Mach-O заголовок приложения iOS, подключитесь к iPhone через SSH и выполните команду:

otool –h ApplicationBinary

Вот заголовок Mach-O приложения Facebook для iOS.

Значения cputype и subtype в заголовке Mach-O определяют целевую архитектуру приложения. В показанном примере видно, что приложение Facebook имеет архитектуру ARM7.

ARM7s (iPhone 5) = cputype 12/ subtype 11
ARM7 (iPhone 4 & 4S) = cputype 12/ subtype 9
ARM6 (iPhone 3GS) = cputype 12/ subtype 6

Приложения, которые скомпилированы для нескольких архитектур, содержат более одного файла Mach-O. Соответствующие двоичные файлы также называются универсальными или толстыми. Чтобы посмотреть Mach-O заголовок универсального двоичного файла, выполните в SSH-терминале следующую команду:

otool –arch all -h ApplicationBinary

На рисунке выше видно, что приложение CardInfo скомпилировано для процессоров с архитектурами ARM7 и ARM7s.

Загрузочные команды

Загрузочные команды определяют то, как сегменты файла располагаются в памяти, а также линковочные характеристики файла (связи с библиотеками). Они содержат первоначальную разметку файла в виртуальной памяти, расположение таблицы символов, состояние выполнения главного потока программы и информацию о подключаемых разделяемых библиотеках. Загрузочные команды (LC_ENCRYPTION_INFO) также определяют, зашифрован ли файл. Чтобы просмотреть загрузочные команды, выполните в SSH-терминале следующую строку:

otool –Vl ApplicationBinary

Данные:

Файл Mach-O содержит собственно данные приложения в одном или нескольких сегментах. В каждом сегменте может быть некоторое количество секций, в том числе, нулевое. Каждая секция сегмента содержит код или данные определенного типа. Точное количество сегментов, секций и их расположение в памяти описывается в разделе загрузочных команд.

Механизмы защиты приложений iOS во время выполнения

Платформа iOS предоставляет множество защитных механизмов, например, ASLR, защита от повреждения стека и ARC. Понимание этих механизмов защиты важно для выполнения реверсирования и анализа iOS-приложений.

Рандомизация адресного пространства (ASLR)

ASLR – важный метод противодействия эксплоитам, появившийся в iOS 4.3. ASLR значительно усложняет удаленную эксплуатацию уязвимостей к повреждению памяти за счет рандомизации размещения объектов приложения в памяти. По умолчанию приложения iOS используют ограниченную ASLR, которая рандомизирует размещение только части объектов в памяти. Чтобы задействовать ASLR максимально, приложение должно быть скомпилировано с флагом -fPIE -pie (в XCode этому флагу соответствует опция «Генерировать зависимый от расположения код»). В последнем выпуске XCode этот флаг выбран по умолчанию. На рисунке ниже показано состояние различных разделов памяти при частичном и полном применении ASLR.

Чтобы определить, было ли приложение скомпилировано с флагом PIE, подключитесь к iPhone по SSH и выполните команду:

otool –Vh ApplicaitonBinary

На рисунке выше в конце вывода мы видим слово PIE. Это означает, что приложение Facebook было скомпилировано с данным флагом.

Защита от повреждения стека

Защита от повреждения стека – это механизм борьбы с эксплоитами, который защищает от атак переполнения стека путем помещения перед локальными переменными некоторого случайного, но известного значения, называемого стековой канарейкой (по аналогии с живыми канарейками, которых брали с собой шахтеры, чтобы проверить отсутствие взрывоопасных газов). При возврате из функции проверяется значение стековой канарейки. В случае переполнения канарейка будет повреждена, и, узнав об этом, приложение сможет защититься от переполнения. Чтобы включить защиту от повреждения стека, приложение нужно скомпилировать с флагом –fstack-protector-all.

Приложения iOS, которые используют стековых канареек, содержат в двоичном коде строки _stack_chk_fail и _stack_chk_guard. Чтобы определить, использует ли приложение защиту от повреждения стека, подключитесь к iPhone по SSH и выполните следующую команду:

otool –I –v ApplicationBinary | grep stack

Данный снимок экрана свидетельствует о том, что приложение Facebook использует защиту от повреждения стека.

Автоматический подсчет ссылок (ARC)

ARC – еще один механизм для борьбы с эксплоитами, введенный в iOS 5. Он защищает приложения от уязвимостей к повреждению памяти путем перекладывания ответственности за управление памятью с разработчика на компилятор. ARC можно включить, установив значение «yes» для опции «Objective-C Automatic Reference Counting». По умолчанию данный механизм включен.

Приложения iOS с включенным ARC содержат в двоичном файле строку _objc_release. Чтобы определить, использует ли приложение ARC, выполните в SSH-терминале следующую команду:

otool –I –v ApplicationBinary | grep _objc_release

Показанный выше снимок экрана говорит о том, что приложение Facebook скомпилировано с поддержкой ARC.

Механизмы защиты во время выполнения создают дополнительный слой безопасности приложения. Поэтому хорошей практикой является явное включение этих механизмов при компиляции приложения, а в ходе тестирования на проникновение стоит проверить их наличие.

Реверсирование приложений iOS

Чаще всего iOS-приложения пишутся на языке Objective-C. Objective-C – это динамический язык, основанный на принципах передачи сообщений между объектами. Поскольку язык является динамическим, все классы, методы и прочие компоненты хранятся внутри двоичного файла. Данная информация может быть извлечена с помощью утилиты class-dump-z, созданной kennytm.

Установка class-dump-z
  1. На подвергшемся джейлбрейку устройстве установите из Cydia утилиты wget и unzip.
  2. Подключитесь к iPhone по SSH и выполните указанные ниже команды

    wget -U Mozilla/5.0 http://www.securitylearn.net/wp-content/uploads/tools/iOS/class-dump-z.zip
    unzip class-dump-z.zip
    mv class-dump-z /usr/bin

  3. Чтобы выгрузить из приложения iOS информацию о классах, перейдите в поддиректорию приложения, заканчивающуюся на .app, и запустите следующую команду:

    class-dump-z ApplicationBinary

На рисунке ниже показана выгруженная информация о классах приложения Gmail для iOS. Здесь не содержится каких-либо полезных данных, поскольку двоичный файл зашифрован. Приложения, загруженные из AppStore, шифруются с помощью FairPlay DRM для защиты от пиратства.

Чтобы выяснить, является ли приложение зашифрованным, запустите в SSH-терминале следующие команды:

otool –l ApplicationBinary | grep crypt

cryptid 1 говорит о том, что приложение зашифровано. Для незашифрованных приложений значение cryptid равно 0.

Часто первым шагом при реверсировании iOS-приложений является снятие шифрования. Расшифровка приложения позволяет атакующему лучше понять, как работает приложение, внутреннюю структуру его классов, и подготовить двоичный файл для реверсирования. Изначально незашифрованными являются приложения, которые используют модель распространения in-house или являются самоподписанными.

Расшифровка приложений iOS

Когда iOS-приложение запускается, загрузчик расшифровывает его и загружает в память. Данный факт лег в основу хорошо отлаженного метода, позволяющего снимать с приложения защиту от копирования FairPlay. Процесс расшифровки включает в себя следующие шаги:

  1. Нахождение смещения и размера зашифрованных данных внутри двоичного файл приложения.
  2. Нахождение адреса, по которому приложение загружается в память (если оно было скомпилировано с флагом PIE, то этот адрес будет меняться при каждой загрузке).
  3. Выгрузка расшифрованной части приложения из памяти с помощью отладчика (например, gdb).
  4. Замещение зашифрованных данных приложения выгруженными расшифрованными данными.
  5. Изменение значение cryptid на 0.

Весь данный процесс можно автоматизировать с помощью приложений из Cydia, которые называются Clutch и Rasticrac.

Расшифровка iOS-приложения Gmail с помощью Clutch
  1. На подвергшемся джейлбрейку устройстве зайдите в Cydia и добавьте репозиторий http://AppAddict.org/repo, перейдя к пункту Manage->Sources.
  2. Скачайте из Cydia ClutchPatched и IPA Installer.
  3. Подключитесь к iPhone по SSH и наберите команду ‘Clutch’. Она перечислит все установленные на iPhone приложения.
  4. Если теперь указать Clutch определенное приложение, он расшифрует его и сохранит расшифрованные данные в файле с расширением ipa внутри папки /var/root/Documents/Cracked/. На рисунке ниже показано, что Clutch взломал приложение Gmail и сохранил расшифрованные данные в файле /var/root/Documents/Cracked/Gmail-v2.2.0.8921.ipa.
  5. Данный файл ipa можно установить на iPhone прямо из SSH-терминала, используя следующую команду:

    installipa –c [iPAPath]

На рисунке ниже показаны выгруженные данные о классах расшифрованного приложения Gmail.

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

Динамический анализ через Cycript

Динамический анализ включает в себя реверсирование приложения и анализ его потока управления для обхода блокировок, аутентификации, доступа к конфиденциальной информации в памяти, нарушения логических проверок и доступа к закрытым областям приложения. Objective-C – рефлективный язык, он позволяет модификацию своего поведения во время выполнения. На iPhone поведение приложения во время выполнения можно легко менять с помощью CyCript. CyCript можно найти среди пакетов Cydia.

Cycript – это язык программирования, сочетающий в себе Objective-C и Javascript. Cycript позволяет вмешиваться в процесс ОС и дает доступ ко всем классам, полям и методам выполняющегося приложения.

Перед тем, как приступить к динамическому анализу, необходимо изучить поток выполнения iOS-приложения.

Objective-C по сути является надстройкой над C, поэтому его выполнение начинается с функции main(). Фактически, main() вызывается, когда пользователь нажимает на значок приложения. Главная функция в свою очередь вызывает метод UIApplicationMain. Данный метод инициализирует объект UIApplication, отображает графический интерфейс (окна, представления), создает делегат приложения и запускает цикл обработки сообщений. Делегат приложения отслеживает высокоуровневые действия/события в приложении. С помощью Cycript мы можем подключиться к запущенному процессу и получить доступ к его объекту UIApplication. UIApplication – это синглтон, который представляет приложение и действует как центр управления. Так что, получение доступа к данному объекту – это получение доступа к внутренней структуре приложения.

Возможности Cycript показаны в следующих далее примерах. В целях демонстрации я использовал старую версию приложения Photo Vault. Перед внедрением в процесс с помощью cycript мы сначала выгрузим информацию о классах приложения через class-dump-z.

Доступ к полям объектов через Cycript

Приложение Photo Vault сохраняет конфиденциальность фотографий за счет ограничения доступа по паролю. Когда мы запускаем приложение в первый раз, оно просит пользователя установить пароль. В последующие разы для получения доступа к защищенным фотографиям пользователю нужно ввести правильный пароль. Ниже описаны шаги, как узнать пароль через динамический анализ посредством Cycript.

  1. Запустим приложение Photo Vault. Оно запросит пароль.
  2. Подключимся к iPhone по SSH и получим информацию о процессе с помощью команды ps ax.
  3. Внедримся в процесс, используя команду cycript –p [PID].
    Замечание: Если вы знаете имя приложения, вы можете также внедриться в его процесс, используя команду cycript –p [ApplicationName].
  4. В командной строке cycript мы можем получить доступ к экземпляру приложения либо путем вызова метода [UIApplication sharedApplication], либо используя переменную UIApp.
  5. Чтобы найти делегат приложения, выполним команду UIApp.delegate.
  6. Найдем в выгруженной информации о классах делегат приложения PhotoVaultAppDelegate и рассмотрим секцию @interface (также полезной будет секция @protocol). Секция @interface содержит объявление всех методов и свойств, а @protocol – группу связанных методов, которые объект может вызвать через свой делегат.
  7. В делегате приложения содержится интересующее нас свойство – strPass. Запросим значение, хранимое в этом свойстве, набрав в командной строке UIApp.delegate.strPass.
  8. Ура! Мы получили пароль к фотографиям. Можем ввести его в приложении и получить доступ к защищенным фотографиям пользователя.

Cycript также позволяет модифицировать значения полей, вызывать методы и перезаписывать существующие методы прямо во время выполнения программы.

Это пятая часть серии статей о пентестинге приложений iPhone. В шестой части будет рассмотрен динамический анализ приложений iOS с помощью gdb и подмена методов с помощью Cycript.

Ссылки

  1. Debunking NSLog Misconceptions http://oleb.net/blog/2011/06/app-launch-sequence-ios/
  2. Hacking and Securing iOS Applications by Jonathan Zdziarski
  3. «Apple iOS 4 Security Evaulation» by Dino Dai Zovi
31 ссылка для тех, кто хочет освоить iOS-разработку — Академия Яндекса

Развитие языка Swift снизило и так невысокий порог вхождения в iOS-разработку. Изучать сам язык, среду разработки и практики написания кода на нём — одно удовольствие. Но это не значит, что писать для платформ Apple просто или непрестижно: iOS-разработчики востребованы в большинстве крупных компаний. Ссылки на статьи и другие материалы в этом списке подобрал Артур Антонов — разработчик в команде приложения Яндекс.Переводчик. Советы будут полезны будущим стажёрам Яндекса, а также всем остальным, кто хочет делать приложения в режиме полного цикла, знать инструменты и основные фреймворки, придумывать архитектуру сервисов, писать производительный код без багов и угадывать мысли цензоров App Store. 

Инструменты платформы

Если вы только начинаете знакомиться с SDK, набором библиотек для iOS или хотите систематизировать знания в области создания приложений — пройдитесь по этим ссылкам.

Документация Apple, конечно же

Когда в марте 2008 года Apple представила первый SDK (тогда ещё для iPhone OS), больше ста тысяч человек загрузили его за первые две недели после релиза. Но тогда мало кто подозревал, какой бум iOS ждёт нас впереди. Сейчас Apple предлагает очень много полезной информации: ссылки на API, статьи, код. Лучше сначала ознакомиться с содержанием, а потом возвращаться в документацию по необходимости. 

Статьи про отдельные библиотеки iOS

Полезнее документации, инструкций и учебников может быть только советы опытных разработчиков. Авторы журнала NSHipster — именно такие ребята. Стоит отметить очень удобную навигацию по темам и классные рекомендации книг. 

Рассылка про iOS-разработку

Если вы мобильный разработчик или только собираетесь им стать, то вы наверняка уже слышали рекомендации подписаться на ряд email-рассылок. Вот всего одна, зато исчерпывающая и с очень чёткой структурой. Её ведёт независимый iOS-разработчик Дэйв Вервер. Внутри — новости индустрии за неделю, ссылки на полезные тулзы, GitHub и многое другое.

На кого стоит подписаться в твиттере

Твиттер — источник остросоциальных тем, новых мемов и идей для iOS-разработки. По ссылке вы найдёте список из 52 сильнейших специалистов индустрии: подписывайтесь, чтобы первыми узнавать важные новости, участвовать в обсуждениях и просто быть в теме.

Интерфейс

Фреймворк UIKit позволяет строить интерфейсы iOS-приложений и обрабатывать действия пользователя. В прошлом году Apple представила SwiftUI, который однажды должен заменить UIKit — но переходный период будет долгим, и ещё в течение нескольких лет большинству разработчиков потребуется знать оба фреймворка.

Документация по UIKit

Официальная документация от Apple очень подробная и становится со временем всё лучше: её точно будет полезно изучить новичкам, но даже при наличии опыта получится найти что-то интересное. Она покрывает большинство тем — от структуры приложения и методов пользовательского ввода до защиты данных и взаимодействия с самой iOS. Обратите внимание на раздел про UIView и его наследников.

Видеокурс по созданию приложения с UIKit

Если вам пока сложно разобраться с UIKit самостоятельно, обратите внимание на этот англоязычный видеокурс. Он создан для абсолютных новичков: опыт в создании iOS-приложений или знание Swift не понадобятся. Первые уроки в игровой форме рассказывают про основные понятия и термины. Все видео короткие — самые длинные идут около 9 минут — и бесплатные.

Туториалы по созданию интерфейса

Статьи про UI в. iOS-приложениях. Тут и про добавление разных элементов (например, контекстного меню или навигации), и про начало работы с анимацией, и про SnapKit для iOS. Основная ценность статей заключается в том, что это полноценные инструкции: со всеми подробностями и комментариями для новичков. Тексты, конечно, тоже на английском языке.

Туториалы по SwiftUI

UIKit — это прошлое и настоящее, а SwiftUI (по крайней мере, по замыслу Apple) — будущее. Apple предлагает начать создавать красивые, динамичные и быстрые приложения с новым декларативным фреймворком. Авторы собрали целый учебник: множество туториалов с разделением на секции и шаги. Каждый шаг проиллюстрирован скриншотом страницы или кода — словом, точно не запутаетесь. В конце каждого туториала можно пройти короткий тест, который проверит, насколько хорошо вы разобрались в теме.

Архитектура

Самый ответственный этап в создании приложения — выбор архитектуры и принципов, по которым вы планируете вести разработку. Чем позже вы найдёте ошибку, допущенную при проектировании, тем сложнее будет её исправить. Изучите материалы по этим ссылкам, чтобы сразу выбрать правильную концепцию приложения.

Примеры SOLID

Существует много разных подходов к проектированию архитектур. Но все они так или иначе опираются на принципы SOLID. iOS-разработчик Сергей Крапивенский в своём докладе доходчиво рассказывает, как эти фундаментальные правила могут применяться в стандартных задачах разработки. Самая полезная часть доклада — разбор распространенных ошибок и способов, которые помогут их избежать или оперативно устранить.

GoF-паттерны с примерами

На этом сайте собрана исчерпывающая информация о паттернах проектирования. Автор предлагает начать с основ: изучить историю создания, задуматься о пользе, почитать критику и узнать всё о классификации. Самое ценное здесь — это сам каталог паттернов.

Clean Architecture

Статья на Хабре, которая призвана донести до сообщества детали концепции Clean Architecture и популярные заблуждения, связанные с ней.

Обзор архитектурных паттернов в iOS

iOS-разработчик из Badoo сравнивает популярные архитектурные практики и рассказывает о своих выводах. Всего автор разбирает четыре архитектурных паттерна: MVC, MVP, MVVM и VIPER. Впечатления от каждого из них в формате «ожидание/реальность» от практикующего разработчика — полезное чтение для новичков в этой теме.

Список опенсорсных iOS-приложений

Действительно огромный список опенсорсных приложений для iOS, watchOS и tvOS. Они распределены по категориям, и к каждому приложению есть небольшое описание Посмотрите, как устроены приложения, или примите участие в развитии любого из проектов на GitHub.

Многопоточность

Концепция многопоточного программирования отлично укладывается в общую идеологию iOS. Запускать процессы в отдельных потоках можно с помощью понятного набора инструментов, который только улучшился с развитием языка Swift. Эта часть списка посвящена Grand Central Dispatch — технологии Apple для управления параллельными операциями. Можно почитать и о некоторых других опциях — знания в области многопоточности пригодятся и на собеседовании, и в продакшене.

Введение в многопоточность iOS

Туториал по улучшению отзывчивости приложений при помощи GCD. Это первая часть большого учебника, которая поможет разобраться, как использовать GCD, а также познакомит с основными функциями, плюсами и минусами API. В рамках туториала авторы предлагают не просто почитать теорию, но и попробовать применить её на практике. Для этого вместе с учебными материалами вы получите почти готовый проект под названием GooglyPuff. Сможете оптимизировать его с помощью GCD — и миссия выполнена!

Архивный гайд от Apple

Несмотря на то, что это руководство за 2012 год, мы советуем не обходить его стороной. Возможно, будет полезно даже начать с него, если вы впервые знакомитесь с темой многопоточности. Внутри вас ждёт подробное описание главных процессов: вы познакомитесь с основами асинхронного проектирования приложений, узнаете про выполнение задач с помощью объектов Objective-C и асинхронную обработку системных событий. Бонус — словарь с основными терминами.

objc.io про многопоточность

objc.io — проект трёх разработчиков из Берлина: Криса Эйдхофома, Даниэля Эггерта и Флориана Куглера. В далёком 2013 году они создали этот сайт, чтобы обсуждать темы, актуальные для всех разработчиков iOS и macOS. Прошло много времени, ребята выпустили целых пять книг и написали множество материалов — самостоятельно и с крутыми экспертами. По ссылке — выпуск на тему многопоточности. Вместе с автором библиотеки PSPDFKit Питером Штейнбергером и опытным разаботчиком Тобиасом Кранцером они рассказывают об основных методах, проблемах и подводных камнях параллельного программирования.

Отладка

Отладка здесь — это не только поиск багов. Инструментарий iOS-разработчика позволяет вам делать структуру кода более прозрачной и видеть больше свойств приложения прямо во время программирования.

Cессия WWDC

Видео доклада с WWDC 2018 — это целый час ценнейшей информации про методы отладки Xcode. Вы узнаете, как использовать популярный дебаггер LLDB и брейкпоинты для исправления ошибок в вашем приложении и что нужно сделать, чтобы получить максимум от инструментов отладки Xcode. Всё это с примерами и подробными объяснениями.

Выпуск objc.io про отладку

Целый урок про отладку приложений от objc.io. Начинается он с разбора кейса — автор рассказывает о процессе и инструментах, которые он использовал для отслеживания ошибки регрессии в UIKit. После этого полезного чтения вас ждут не менее интересные размышления про LLDB и технологии DTrace и Activity Tracing.

Отладка приложений под iOS

Роман Ермолов руководит группой разработки приложения Яндекс для iOS. В этом докладе от 2015 года он говорит про интересные возможности LLDB, отладку иерархии UIView и отладку без исходников. Бонус — реальные примеры и дискуссия по теме в конце доклада.

Как работает LLDB

Во всех вышеперечисленных источниках много внимания уделяется именно этому отладчику. Хотите разобраться во всех нюансах его работы? Тогда вам точно пригодится этот доклад с WWDC 2019. Вы узнаете про разные способы отображения значений, форматирование пользовательских типов данных и (самое интересное!) расширение LLDB с помощью собственных сценариев Python 3.

Устройство Objective-C Runtime

Майк Эш — программист и пилот планера, который живет в Вашингтоне. Впечатляет? Это вы ещё не видели его блог! В нём он делится полезным софтом, делает остроумные посты в формате Q&A по пятницам и рассказывает о полётах. В этом старом (2009 год), но всё ещё полезном материале он рассуждает об Objective-C Runtime. Максимально подробное объяснение поможет разобраться в теме даже новичкам.

Оптимизация

Недостаточно просто придумать приложение, написать код и опубликовать результат в App Store. Нужно, чтобы оно хорошо работало: запуск не занимал много времени, реакция на ввод данных была мгновенной, а батарея не разряжалась из-за большого количества сетевых запросов.

Обзорная статья Apple

В своей обзорной статье Apple советует, как применять цикл непрерывного улучшения для оптимизации работы приложений. Помимо верхнеуровневых сведений и советов, в материале есть ссылки на полезные инструменты.

Вводная сессия WWDC об инструментах

Если вы хотите больше узнать про инструментарий Xcode, посмотрите видео с WWDC-2019. Это получасовой рассказ с примерами, который поможет разобраться с такими вещами, как шаблоны для профилирования производительности приложений и поиск «узких» мест в коде. Все описанные спикером инструменты призваны существенно повысить скорость отклика вашего приложения.

Сессия WWDC о подходах к оптимизации

Ещё одно видео с конференции Apple, но уже за 2018 год. Оно позволит глобально взглянуть на тему оптимизации: спикеры говорят об общем подходе и стратегиях, которых стоит придерживаться. Однако тут тоже не обошлось без практических советов, основанных на опыте авторов: они приложили руку к нескольким популярным приложениям от самой Apple. В видео рассказывается о том, как научиться пользоваться пакетом Instruments и другими возможностями Xcode.

Книга о внутреннем устройстве iOS и macOS

Продолжаем погружаться в тему — нужно ещё больше теории. По ссылке вы найдёте почти 800 страниц авторства Джонатана Левина с информацией практически обо всём, что когда-либо интересовало вас в работе с iOS. Чтобы разобраться в принципах работы системы, автор активно пользуется реверс-инжинирингом (обратной разработкой) и учит читателей делать то же самое. Вас ждёт большое количеством практических примеров, иллюстраций, скриншотов и ссылок на открытый исходный код от Apple.

Доклад об оптимизации запуска приложения

Вернёмся к практике. В этом видео руководитель службы мобильной разработки Яндекс. Карт Николай Лихогруд рассказывает об оптимизации времени запуска iOS-приложения Карт. На примере реального кейса вы узнаете, как правильно измерять время запуска, оптимизировать системную и пользовательскую части и поддерживать результат в следующих версиях.

Публикация в App Store

Многие разработчики, включая сотрудников Яндекса, недооценивали сложность процесса подписи iOS-приложения и модерации в App Store. Казалось бы, у вас всё готово: программа работает, вы хотите начать распространять её среди клиентов. Но у Apple есть правила, которым ваш код должен соответствовать.

Как загрузить приложение в App Store

Начните с пошаговой инструкции. Она выгодно отличается от публикаций на других ресурсах своей актуальностью: это популярный гайд от разработчиков Густаво Амброзио и Тони Дабура, обновлённый в 2020 году — с информацией из последней версии Xcode.

Подробный разбор подписи приложения

Ещё одна классная статья на сайте objc.io. Автор считает, что механизм подписи и подготовки кода — одна из самых сложных вещей, с которыми сталкивается iOS-разработчик. Поэтому он подробно описывает процесс: почитайте, чтобы понимать, что и зачем вы делаете. Но учитывайте, что статья написана в далёком 2014 году.

Обзор инструментов Xcode для подписи приложения

Для тех, кто хочет совсем углубиться в тему и разобраться: презентация Apple про функции Xcode, которые упрощают процессы управления сертификатами, подпись приложений и настройку параметров сборки проекта. Это видео с конференции WWDC 2016. Именно тогда компания представила обновлённый способ управления конфигурацией подписи с включенным по умолчанию автоматическим режимом.

Непрерывная интеграция

Пара дополнительных ссылок для тех, кто уже программирует под iOS и теперь беспокоится, как соотнести и объединить свой труд с результатами коллег по команде.

Внедрения реактива в архитектуру iOS приложений / ХабрБольшинство статей о функционально-реактивном программировании ограничиваются демонстрацией возможностей определенного инструмента в определенной задачи и не дают понимания как использовать всю мощь в рамках целого проекта.

Хотелось бы поделиться опытом проектирования с использованием функционально-реактивного программирования под iOS. Это не зависит от выбранного инструмента, будь то RAC, RxSwift, Interstellar или же что-то еще. Так же это применимо при разработке под MacOS.

В определенных моментах я буду писать, используя Swift + RAC4, поскольку это мои основные инструменты на данный момент. Однако, я не буду использовать в статье терминологию и особенности RAC4.

Может быть вы зря отказывались от реактивного программирования и пора начать его использовать?

Для начала коротко о мифах среди людей, которые о реактиве только слышали и слышали не самое хорошее:

Миф 1 — порог вхождения в реактивное программирование слишком велик


Никто не говорит, что вам надо с первых минут использовать все доступные возможности. Вам нужно всего лишь понять концепцию и основные базовые операторы (ниже я напишу о 4х минимально необходимых операторах, а к остальным вы будете приходить по мере решения различного рода задач).
На это не надо тратить месяцы и года, достаточно нескольких дней/недель (в зависимости от имеющегося бэкграунда), а при наличии опытной команды вхождение будет намного быстрее.

Миф 2 — реактив используется только в UI слое


Реактив удобно использовать в бизнес логике и ниже я покажу, что мы от этого получим.

Миф 3 — реактивный код очень сложно читать и разбирать.


Все сильно зависит от уровня написанного кода. При должном разделении системы это повышает понимание кода.
Более того, это не сильно сложнее, чем использование множества калбеков.
А написать нечитаемый код можно практически всегда.
Процесс написания реактивного кода похож на детскую игру в ручеек. Мы создаем путь для воды и только потом запускаем воду. Водой в нашем случае являются вычисления. Сетевой запрос, получение данных из базы, получение координат и многие другие вещи. Путь для воды в данном случае — это сигналы.

Итак, сигнал — это основной строительный блок, содержащий некие вычисления. Над сигналами в свою очередь могут быть совершены определенные операции. Применяя операцию к сигналу, мы получаем новый сигнал, включающий в себя предыдущие конфигурации.

Применяя операции над сигналами и комбинируя с другими сигналами, мы создаем поток данных для вычислений (dataflow). Весь этот поток начинает свое выполнение в момент подписки на сигнал, что похоже на ленивые вычисления. Это дает возможность более тонко контролировать момент старта выполнения и последующие действия. Наш код разделяется на логические части (что увеличивает читаемость), а мы получаем возможности создавать новые «методы» буквально «налету», что повышает переиспользуемость кода в системе. Похоже на функции высшего порядка, не правда ли?

Минимально необходимыми операциями для конфигурирования dataflow на первое время следует выделить map, filter, flatMap и combineLatest.
И напоследок небольшая особенность, dataflow — это данные + ошибки, что дает возможность описывать последовательность действий в 2х направлениях.

Это минимум необходимой теории.


Возьмем для примера SOA, но разумеется, это никак не ограничивает вас.

Используемая схема может отличаться от других или быть такой же, я не претендую на эталон, как и не претендую на универсальность. В большинстве наших задач мы придерживаемся данного решения, скрывая за сервисом обработку данных (и это вовсе не обязательно должны быть сетевые запросы).

Транспорт


Итак, это наш первый претендент на реактивность. Поэтому я остановлюсь на этом месте более подробно.
Для начала посмотрим на типичные решения данной задачи:Использование 2х калбеков
    typealias EmptyClosure = () -> ()

    func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, success: EmptyClosure, failed: EmptyClosure) -> NSURLSessionTask


Использование 1го калбека
    typealias Response = (data: NSData?, code: Int)
    typealias Result = (response: Response, failed: NSError?)
    
    func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, result: Result) -> NSURLSessionTask


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

А еще нужна возможность отменить сетевой запрос, да и + нужно бы инкапсулировать работу Транспорта.
Скорее всего, в этом случае наш код будет выглядеть примерно

вот так:
protocol Disposable {
    func dispose()
}

    typealias Response = (data: NSData?, code: Int)
    typealias Result = (response: Response, failed: NSError?)
    
    func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, result: Result) -> Disposable?
...
...
...    
    typealias EmptyClosure = () -> ()

    func getReguestJSON(urlPath: String, parameters: [String : AnyObject]?, success: EmptyClosure, failed: EmptyClosure) -> Disposable?


А теперь посмотрим на решение с использованием сигналов
    func getRequestJSON(urlPath: String, parameters: [String : String]) -> SignalProducer<Response, NSError> {
        return SignalProducer<Response, NSError> { observer, disposable in
            let task = ... {
                observer.sendNext(data: data, code: code) 
                observer.sendCompleted()
                
                //or observer.sendFailed(error)
            }
            
            disposable.addDisposable {
                task.cancel()
            }
        }
    }


Ранее я сознательно упустил один важный момент — при создании сигнала мы не только пишем что выполнить при подписке на сигнала, но и что делать при отмене сигнала.
Подписка на сигнал возвращает экземпляр класса Disposable (не написанный нами выше, поболее), который позволяет отменить сигнал.Пример кодом
    let disposable = getRequestJSON(url, parameters: parameters) //создали сигнал
        .startWithNext { data, code in
            ...
            ...
            ...
    } //с момента вызова startWithNext начался выполнятся сигнал
    disposable.dispose() //отменяем выполнение сигнала


Теперь вызываемая сторона может запросто отложить выполнение запроса, объединить результат с другими запросами, написать некоторые действия на события сигнала (из примера выше на завершение запроса), а так же что делать при получении данных и что делать при возникновении ошибки.

Но перед демонстрацией такого кода, я бы хотел рассказать о таком понятии, как

Side Effect


Если даже вы не сталкивались с этим понятием, то 100% его наблюдали (либо вы сюда заглянули случайно).
Простым языком — это когда наш поток вычислений зависит от окружающей его среды и меняет ее.

Мы стараемся писать сигналы, как отдельную деталь кода, тем самым повышая ее возможную переиспользуемость.
Однако сайд эффекты порой необходимы и ничего ужасного в этом нет. Рассмотрим на рисунке, как мы можем использовать Side Effect в реактивном программировании:

Весьма просто, не правда ли? Мы вклиниваемся между выполнениями сигналов и совершаем определенные действия. По сути, мы выполняем действия на определенные события сигнала. Но при этом мы сохраняем сигналы все так же чистыми и готовыми для переиспользования.

Например, из ранее созданной задачи: «Показать лоадер при старте сигнала и убрать при завершении».

Парсинг

Вспомним типичную ситуацию — данные от сервера либо пришли в верном, либо в не верном формате. Варианты решений:
1) Калбеки «данные + ошибка»
2) Подход Apple, используя NSError + &
3) try-catch

А что нам может дать реактив?
Создадим сигнал, в котором будем парсить ответ от сервера и выдавать результат в определенных событиях (next/failed).
Использование сигнала даст возможность более явно видеть работу кода + объединить работу с сигналом сетевого запроса. Но стоит ли?

Пример
class ArticleSerializer {
    
    func deserializeArticles(data: NSData?, code: Int) -> SignalProducer<[Article], NSError> {
        return SignalProducer<[Article], NSError> { observer, _ in
            ...
            ...
            ...
        }
    }

Объединим сетевой запрос, парсинг и добавим возможность сохранить результат парсинга в DAO.

пример кода
class ArticleService {
    ...
    ...
    ...
    func downloadArticles() -> SignalProducer<[Article], NSError> {
        let url = resources.articlesPath
        let request = transport.getRequestJSON(url, parameters: nil)
            .flatMap(.Latest, transform: serializer.deserializeArticles)
            .on(next: dao.save)
        return request
    }

Нет вложенности, все очень просто и легко читается. Вообще весьма последовательный код, не правда ли? И он сохранится таким же простым, даже если сигналы будут выполнятся на разных потоках. Кстати, рассмотрим использование combineLatest:

Синхронизация параллельных запросов
userService.downloadRelationshipd() //сигнал с сетевым запросом
	.combineLatestWith(inviteService.downloadInvitation()) //сигнал с сетевым запросом + запустить параллельно
	.observeOn(UIScheduler()) //результат сигналов вернуть на главный поток (неважно на каком будут выполнятся)
	.startWithNext { users, invitations in
		//работаем с результатом операций
	}


При этом стоит заметить, что код, написанный выше, не начнет выполнятся, пока его не запустят, подписавшись на сигнал. Фактически мы лишь указываем действия.

И теперь сервисы стали еще более прозрачными. Они всего лишь связывают между собой части бизнес логики (включая другие сервисы) и возвращают dataflow этих связей. А человек, использующий полученный сигнал, может очень быстро добавлять реакцию на события или объединять с другими сигналами.

А еще…

Но все это было бы не так интересно, если бы не множество операций над сигналами.
Устанавливать задержку, повторять результат, различные системы комбинирования, установка получения результата на определенный поток, свертки, генераторы… Вы только посмотрите на этот список.

Почему я не стал рассказывать о работе с UI и биндинге, который для многих самый «сок»?
Это тема на отдельную статью, а их и так достаточно много, поэтому я просто приведу пару ссылок и закончу

Лучший мир с ReactiveCocoa
ReactiveCocoa. Concurrency. Multithreading

На этом у меня все. Вместо бесполезного заключения я оставлю несколько практических выводов из последнего проекта:
1) Получение Permission неплохо себя зарекомендовало в качестве сигналов.
2) CLLocationManager отлично повел себя с сигналами. Особенно накопление и редактирование точек.
3) Так же удобно было работать с сигналами на такие действия как: выбор фотографии, отправка SMS и отправка email.

Чистая архитектура на Android и iOS

Понятие «чистая архитектура» пошло из одноименной статьи Роберта Мартина 2012 года. Оно включает в себя несколько принципов:

  • Независимость от фреймворков. Архитектура не должна полагаться на существование какой-либо библиотеки. Так вы сможете использовать фреймворки как инструменты, а не пытаться загнать свою систему в их ограничения.
  • Тестируемость. Бизнес-логика должна быть тестируемой без любых внешних элементов вроде интерфейса, базы данных, сервера или любого другого элемента.
  • Независимость от интерфейса. Интерфейс должен легко изменяться и не требовать изменения остальной системы. Например, веб-интерфейс должен заменяться на интерфейс консоли без необходимости изменения бизнес-логики.
  • Независимость от базы данных. Ваша бизнес-логика не должна быть привязана и к конкретным базам данных.
  • Независимость от любого внешнего агента. Ваша бизнес-логика не должна знать вообще ничего о внешнем мире.

Отражение этих принципов в архитектуре программного обеспечения можно представить следующим образом:

В этой схеме слои обозначают:

  • Entities — бизнес-логика, общая для всех приложений, а в случае отдельного приложения — наиболее базовые бизнес-объекты.
  • Use Cases — логика приложения, “сценарии применения”, которые управляют потоком данных из предыдущего слоя.
  • Interface Adapters — адаптеры между Use Cases и внешним миром. Этот слой конвертирует данные в формат, подходящий для внешних слоев, например Web или базы данных, а также превращает внешние данные в формат для внутренних слоев.
  • Frameworks and Drivers — внешний слой, содержащий фреймворки, инструменты, базы данных и так далее. В этом слое код должен связываться с предыдущим слоем, но не влиять в значительной степени на внутренние слои.

Все слои связаны правилом зависимости — Dependency Rule, которое гласит, что в исходном коде все зависимости могут указывать только вовнутрь. Например, ничто из внешнего круга не может быть упомянуто кодом из внутреннего круга. Это относится к функциям, классам, переменным или любым другим сущностям. Сам Uncle Bob говорит, что эту схему можно изменять: добавлять или убирать слои, но основным правилом в архитектуре приложения должно всегда оставаться Dependency Rule.

Чистая архитектура в iOS-разработке

Для iOS-разработки чистая архитектура реализована в модели VIP или VIPER, которая позиционируется как замена шаблону MVC. Модель VIP (View — Interactor — Presenter) выглядит следующим образом:

Подробнее о каждом компоненте:

  • Models — класс, содержащий структуры Request, Response, ViewModel;
  • Router — простой компонент, передающий данные между viewControllers;
  • Worker — компонент для управления запросами и ответами API/CoreData, а также передачи информации об успехе или неудаче к Interactor.
  • Interactor — посредник между Worker и Presenter. Сначала он связывается с ViewController, который передает все параметры запроса, необходимые для Worker. До передачи данных к Worker, выполняется проверка, и если все в порядке, Worker возвращает ответ, и Interactor передает ответ на Presenter.
  • Presenter —  Response от Interactor форматируется в ViewModel и передается на ViewController. Presenter отвечает за логику отображения и решает, как данные будут представлены пользователю.
  • Configurator — “суперкласс”, который инициализирует все упомянутые выше компоненты.

Примеры проектов с чистой архитектурой на iOS:

Чистая архитектура в Android-разработке

Схему чистой архитектуры Android-приложений предложил разработчик Фернандо Сехас. Выглядит она так:

Внутренним слоем в этом случае является Domain Layer, который хранит всю бизнес-логику. В этом же слое находятся и все use cases и реализации. Этот слой является чистым модулем Java без Android-зависимостей. При связи с этим слоем все внешние компоненты используют интерфейсы.

В Presentation Layer происходит логика, связанная с представлениями и анимациями. Он использует модель Model View Presenter, но шаблон может быть другим, например, MVC или MVVM. Фрагменты и активности в этом слое — это представления, в которых происходит только UI-логика и изменение формата данных. Presentors в этом слое формируются из interactors (use cases), которые производят работу вне основного потока UI Android и возвращаются с данными, которые обрабатываются в View.

Data Layer передает все данные через UserRepository, который использует Repository Pattern со стратегией, выбирающей разные источники данных в зависимости от условий. Например, при поиске пользователя по ID с условием существования пользователя в кэше источником данных будет кэш, в ином случае для получения данных будет отправлен запрос в облако. Источник данных прозрачен для клиента, которого волнует, будут ли получены данные.

Пример приложения, созданного автором, чтобы проиллюстрировать свои схемы, можно посмотреть здесь. Другие примеры чистой архитектуры на Android:

  • Easy MVP — фреймворк для создания приложений по принципу чистой архитектуры;
  • Шаблон для создания приложения с чистой архитектурой на Kotlin;
  • Пример приложения на Java.
Если вы нашли опечатку — выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать [email protected].
onmyway133 / awesome-ios-Architecture: лучшие способы структурирования приложений для iOS перейти к содержанию Зарегистрироваться
  • Почему GitHub? Особенности →
    • Обзор кода
    • Управление проектами
    • Интеграция
    • Действия
    • Пакеты
    • Безопасность
    • Управление командой
    • Хостинг
    • Мобильный
    • Отзывы клиентов →
    • Безопасность →
  • команда
  • предприятие
  • Проводить исследования
    • Исследуйте GitHub →
    учиться и внести свой вклад
    • Темы
    • Коллекции
    • Тенденции
    • Learning Lab
    • Руководства с открытым исходным кодом
    Общайтесь с другими
    • События
    • Общественный форум
.

GitHub — tailec / ios-Architecture: коллекция архитектур для iOS

перейти к содержанию Зарегистрироваться
  • Почему GitHub? Особенности →
    • Обзор кода
    • Управление проектами
    • Интеграция
    • Действия
    • Пакеты
    • Безопасность
    • Управление командой
    • Хостинг
    • Мобильный
    • Отзывы клиентов →
    • Безопасность →
  • команда
  • предприятие
  • Проводить исследования
    • Исследуйте GitHub →
    учиться и внести свой вклад
    • Темы
    • Коллекции
    • Тенденции
    • Learning Lab
    • Руководства с открытым исходным кодом
    Общайтесь с другими
    • События
    • Общественный форум
.
шаблонов архитектуры iOS и передовые практики для расширенного программирования
Войдите, чтобы оставить комментарий
  • Оставить отзыв
  • Товары
  • Сервисы
  • Связаться с нами
  • Блог
  • Для рецензентов
  • Получить в списке
  • Индекс сайта

Поиск

Получите $$$ для обзора

Последний В категории

    Navigation — Архитектура приложения — iOS — Руководство по интерфейсу пользователя

    Навигация

    Люди, как правило, не знают о навигации приложения, пока оно не соответствует их ожиданиям. Ваша задача — реализовать навигацию таким образом, чтобы она поддерживала структуру и назначение вашего приложения, не привлекая к себе внимания. Навигация должна быть естественной и знакомой, и не должна доминировать над интерфейсом или отвлекать внимание от контента. В iOS есть три основных стиля навигации.

    Иерархическая навигация

    Сделайте один выбор на экране, пока не достигнете места назначения. Чтобы перейти в другой пункт назначения, вы должны пересмотреть свои шаги или начать все сначала и сделать другой выбор. Настройки и Почта используют этот стиль навигации.

    Flat Navigation

    Переключение между несколькими категориями контента. Музыка и магазин приложений используют этот стиль навигации.

    Навигация по контенту или по опыту

    Свободно перемещайтесь по контенту, или сам контент определяет навигацию.Игры, книги и другие иммерсивные приложения обычно используют этот стиль навигации.

    Некоторые приложения объединяют несколько стилей навигации. Например, приложение, которое использует плоскую навигацию, может реализовать иерархическую навигацию в каждой категории.

    Всегда обеспечивайте четкий путь. Люди всегда должны знать, где они находятся в вашем приложении и как добраться до следующего пункта назначения. Независимо от стиля навигации важно, чтобы путь через контент был логичным, предсказуемым и легким для отслеживания.В общем, дайте людям один путь к каждому экрану. Если им нужно видеть экран в нескольких контекстах, рассмотрите возможность использования листа действий, оповещения, всплывающего окна или модального представления. Чтобы узнать больше, см. Таблицы действий, Оповещения, Всплывающие окна и Модальность.

    Разработать информационную структуру, которая позволяет быстро и легко получить доступ к контенту. Организуйте свою информационную структуру таким образом, чтобы требовалось минимальное количество нажатий, пролистываний и экранов.

    Используйте жесты для создания плавности. Позволяет легко перемещаться по интерфейсу с минимальным трением. Например, вы можете позволить людям проводить пальцем по краю экрана, чтобы вернуться к предыдущему экрану.

    Используйте стандартные навигационные компоненты. По возможности используйте стандартные элементы управления навигацией, такие как элементы управления страниц, панели вкладок, сегментированные элементы управления, представления таблиц, представления коллекций и разделенные представления. Пользователи уже знакомы с этими элементами управления и интуитивно знают, как обойти ваше приложение.

    Используйте панель навигации для просмотра иерархии данных. Заголовок панели навигации может отображать текущую позицию в иерархии, а кнопка «Назад» позволяет легко вернуться к предыдущему местоположению. Для конкретного руководства см. Панель навигации.

    Используйте панель вкладок для представления одноранговых категорий контента или функциональности. Панель вкладок позволяет людям быстро и легко переключаться между категориями, независимо от текущего местоположения. Для получения конкретных указаний см. Панели вкладок.

    На iPad используйте разделенное представление вместо панели вкладок. Разделенные виды обеспечивают такую ​​же быструю навигацию, как и панели вкладок, при этом более эффективно используя большой дисплей. Для руководства см. Разделенные виды.

    Используйте элемент управления страницы, если у вас есть несколько страниц одного типа контента. Элемент управления страницы четко сообщает количество доступных страниц и какая из них в настоящее время активна. Приложение Погода использует элемент управления страницей для отображения страниц с погодой в зависимости от местоположения. Для получения конкретных указаний см. Элементы управления страницей.

    СОВЕТ Сегментированные элементы управления и панели инструментов не включают навигацию.Используйте сегментированный элемент управления, чтобы организовать информацию в разные категории. Используйте панель инструментов, чтобы предоставить элементы управления для взаимодействия с текущим контекстом. Для получения дополнительной информации об этих типах элементов см. Сегментированные элементы управления и панели инструментов.

    ,

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *