Irbisa · Шпаргалка

System Design на iOS

Раунд проектирования: лента, чат, трекинг в реальном времени, загрузки, пагинация, кэш, идемпотентность, server-driven UI, аналитика, суперприложения

Senior-разработчик · 16 вопросов

  1. 01

    У вас от 45 до 60 минут, чтобы спроектировать фичу iOS-приложения на доске. Как распределить время и что на самом деле оценивает интервьюер?

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

  2. 02

    REST, GraphQL или gRPC: какой стиль API вы предложите для iOS-клиента и чего каждый из них стоит приложению?

    По умолчанию берите REST с JSON: URLSession, Codable, HTTP-кэш и любой отладочный прокси работают из коробки.

  3. 03

    Почему offset-пагинация показывает дубли и пропуски в ленте, которая меняется, пока её листают, и что исправляет курсор?

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

    -- Offset: "skip 40 rows". One post inserted at the top shifts every row down,-- so page 3 starts with the last post of page 2. A deleted post is skipped the same way.SELECT id, created_at FROM postsORDER BY created_at DESC, id DESCLIMIT 20 OFFSET 40;…
  4. 04

    Каждый экран должен быть «свежим», поэтому на холодном старте приложение шлёт сорок запросов. Какие данные вы кэшируете, на сколько и где устаревшие данные допустимы?

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

  5. 05

    Главный экран банка показывает шесть виджетов из шести сервисов и остаётся пустым, пока не ответит самый медленный. Что вы поменяете на клиенте и на бэкенде?

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

    enum Section: CaseIterable, Sendable { case accounts, cards, loans, savings, cashback, offers }enum SectionState: Sendable { case loading, loaded(SectionData), failed } @MainActor @Observablefinal class HomeModel {  private(set) var sections: [Section: SectionState] = [:]   // filled from the disk cache first…
  6. 06

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

    @MainActor final class PostCellModel {  private(set) var post: Post  private let api: any LikeAPI  init(post: Post, api: any LikeAPI) { self.post = post; self.api = api }   func likeTapped() {    post.isLiked.toggle()                       // optimistic: the heart fills at once    post.likeCount += post.isLiked ? 1 : -1    Task {      do { try await api.toggleLike(postID: post.id) }      catch {        post.isLiked.toggle()                   // roll back        post.likeCount += post.isLiked ? 1 : -1      }    }  }}

    Между двумя относительными запросами toggleLike возникает гонка, а откат переворачивает текущее состояние вместо того, чтобы вернуть серверное: когда первый запрос падает по таймауту уже после того, как сервер его применил, его catch снова закрашивает сердечко, очищенное вторым тапом.

  7. 07

    Запрос на оплату отвалился по таймауту, и пользователь снова нажимает «Оплатить». Как клиент и API гарантируют, что карту спишут только один раз?

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

    // The key names the payment, not the HTTP requeststruct PaymentIntent: Codable, Sendable {  let key: UUID  let orderID: String  let amount: Decimal}…
  8. 08

    Распродажа должна закончиться в полночь, но часть пользователей открывает её и на следующее утро. Каким часам доверяет приложение и кто обеспечивает окончание?

    Никаким часам на устройстве: дату можно поменять в Настройках, а «полночь» в каждом поясе своя.

    // The API sends absolute instants: { "serverTime": "...Z", "offers": [{ "endsAt": "...Z" }] }struct Offer: Decodable, Sendable {  let id: String  let endsAt: Date                           // an instant in UTC, never "until midnight"}…
  9. 09

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

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

  10. 10

    Спроектируйте просмотр сторис как в Instagram: что вы предзагружаете, сколько видеоплееров держите живыми и каков бюджет памяти?

    Предзагружайте на шаг вперёд в каждом направлении, куда может пойти пользователь: следующий сегмент текущей сторис и первый сегмент следующего человека.

  11. 11

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

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

  12. 12

    Спроектируйте клиент мессенджера вроде Telegram: транспорт, статусы сообщений, локальное хранилище, порядок и что происходит офлайн или в фоне.

    Чат живёт в локальной базе: UI рисуется из неё, исходящее сообщение сначала записывается туда с клиентским id, а слой синхронизации пользуется сокетом, пока приложение открыто, и догоняет пропуски по номеру последовательности.

  13. 13

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

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

  14. 14

    Как бы вы построили собственный слой аналитики приложения: пакетная отправка событий, хранение офлайн, сэмплирование, учёт согласия и смена вендора без правок в фичах?

    Фичи вызывают один типизированный API track, который сразу возвращает управление; события попадают в очередь на диске, уходят сжатыми пачками с уникальным id у каждого и раздаются адаптерам вендоров за протоколом.

  15. 15

    Когда server-driven UI того стоит и как старая версия приложения справляется с компонентом, которого никогда не видела?

    Он окупается на экранах, которые продукт перестраивает каждую неделю (главная, промо, раскладки каталога), где узкое место в App Review и медленных обновлениях.

    // { "type": "stories", "props": { ... },//   "fallback": { "type": "banner", "props": { "title": "New: stories", "url": "app://stories" } } }indirect enum Component: Decodable, Sendable {  case banner(Banner)  case productGrid(ProductGrid)  case unsupported(fallback: Component?)      // a type this build has never seen…
  16. 16

    Дюжина команд выпускает банк, такси и еду в одном iOS-супераппе. Как его устроить, чтобы ни одна команда не замедляла запуск и чужие релизы?

    Как тонкий хост на общем платформенном слое с парой модулей на бизнес: маленький интерфейсный модуль, который импортируют другие команды, и реализация, которую линкует только хост.