iOS и Swift — разработка приложений для iPhone и iPad

iOS — закрытая экосистема Apple со строгими правилами, но и с высоким качеством приложений. Разработка ведётся на языке Swift в среде Xcode: от первой экранной формы на SwiftUI до публикации в App Store. Курс разберёт все этапы пути.

Уровень: средний 18 разделов 3–4 часа

1. Что такое iOS-разработка

iOS — операционная система для iPhone и iPad. Приложения компилируются в пакет .app и распространяются через App Store, где каждая сборка проходит проверку Apple. Официальный язык — Swift: современный, быстрый и безопасный по памяти.

Ключевые понятия: View — экран (в SwiftUI это View-структуры), ViewController — контроллер экрана в UIKit, AppDelegate — точка старта приложения. Современные проекты строятся на SwiftUI — декларативном описании интерфейса.

// Самая простая программа на Swift
print("Привет, iOS!")

// Тип с проверкой на этапе компиляции
let name: String = "Анна"
print(name.uppercased())
Swift впервые показали в 2014 году, и он быстро вытеснил Objective-C. В память об этом наследии до сих пор встречаются классы с префиксом NS (NSString, NSArray).

2. Установка Xcode

Xcode — официальная среда разработки, которая устанавливается только из Mac App Store. Комплект весит несколько гигабайт, поэтому закладывай время на скачивание и первичную индексацию.

# Проверка после установки
xcodebuild -version
xcrun simctl list devices

Первая команда показывает версию Xcode, вторая — список доступных симуляторов. Для сборки в терминале может потребоваться команда sudo xcode-select --switch /Applications/Xcode.app.

Версия iOS SDK в Xcode и версия macOS должны совпадать по совместимости: старые операционки не запускают новые версии Xcode. Перед обновлением системы проверь требования Apple.
Для iOS-разработки нужен Mac. Если его нет, но очень хочется попробовать Swift — можно изучить язык в песочнице Swift Playgrounds на iPad.

3. Первый проект: структура

File → New → Project → iOS → App. Выбери интерфейс SwiftUI и язык Swift. Ключевые файлы проекта:

ФайлНазначение
MyApp.swiftточка входа с аннотацией @main
ContentView.swiftстартовый экран приложения
Assets.xcassetsиконки и картинки всех размеров
Info.plistнастройки и разрешения приложения
Project.pbxprojструктура проекта и настройки сборки
import SwiftUI

@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
Файл .pbxproj нельзя редактировать руками — его перестраивает Xcode. Конфликты в нём при работе в команде решают через xcodeproj-совместимые инструменты.

4. Основы Swift

Константы — через let, переменные — через var. Тип выводится автоматически. Отличительная черта языка — опциональные типы (optionals): значение может быть или есть, или отсутствовать (nil).

let name = "Анна"           // константа, менять нельзя
var score = 0               // переменная
score += 10

func greet(_ name: String) -> String {
    "Привет, \(name)!"
}

var maybe: String? = nil    // опциональное значение
let length = maybe?.count ?? 0   // безопасный доступ + значение по умолчанию

Обращение к опциональному значению безопасно через ?., принудительно — через !. Второй путь короче, но упадёт с ошибкой, если значения нет.

Избегай восклицательного знака ! в рабочем коде: при неожиданном nil приложение мгновенно завершится. Это главная «бомба» Swift.

5. Storyboard и SwiftUI

Два подхода к интерфейсу: классический Storyboard (визуальный редактор Xcode с файлом .storyboard) и современный SwiftUI (интерфейс описывается кодом). Новые приложения стоит писать на SwiftUI.

// SwiftUI — экран описывается декларативно
struct ContentView: View {
    var body: some View {
        VStack {
            Text("Привет, SwiftUI!")
                .font(.title)
            Button("Нажать") {
                print("Кнопка нажата")
            }
        }
        .padding()
    }
}
Модификаторы вида .font(), .padding() — это цепочки настроек: SwiftUI собирает вид из «слоёв», а Xcode показывает предпросмотр рядом с кодом.

В Storyboard-проекте контроллер называется UIViewController, а элементы перетаскиваются мышью в редактор. Оба подхода используют одни и те же базовые виджеты: UILabel/Text, UIButton/Button.

6. Таблицы и списки

Списки — основа большинства приложений. В SwiftUI это List, в UIKit — исторический UITableView с источником данных UITableViewDataSource.

struct Post: Identifiable {
    let id = UUID()
    let title: String
}

struct PostsView: View {
    let posts = [
        Post(title: "Новость первая"),
        Post(title: "Новость вторая"),
    ]

    var body: some View {
        List(posts) { post in
            Text(post.title)
        }
    }
}

Протокол Identifiable требует уникальный id — по нему SwiftUI понимает, какой элемент обновился. Для вариантов ячеек подходит LazyVStack внутри ScrollView.

Исторический UITableView переиспользует ячейки через очередь — это важно для больших списков. В SwiftUI за производительность отвечает ленивый List, который делает то же самое автоматически.

7. Навигация и переходы

Навигационный стек SwiftUI оборачивает экраны в NavigationStack, а переходы создаёт NavigationLink. Заголовок экрана задаётся модификатором .navigationTitle.

NavigationStack {
    List(posts) { post in
        NavigationLink(post.title) {
            PostDetailView(post: post)
        }
    }
    .navigationTitle("Новости")
}

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

.sheet(isPresented: $showForm) {
    AddPostView()
}

Знак $ создаёт двустороннюю связь (binding): изменение состояния в форме автоматически видно родительскому экрану.

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

8. Работа с сетью

Сетевые запросы делаются классом URLSession, а JSON разбирается автоматически благодаря протоколу Codable.

struct Post: Codable {
    let id: Int
    let title: String
}

func fetchPosts() async throws -> [Post] {
    let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode([Post].self, from: data)
}

Вызвать функцию и показать результат можно так:

.task {
    do {
        posts = try await fetchPosts()
    } catch {
        print("Ошибка сети: \(error)")
    }
}
В iOS 9+ запрещён незашифрованный HTTP по умолчанию. Для локальной разработки пропиши исключение в Info.plist (ключ NSAppTransportSecurity), но на релизе оставляй только HTTPS.
Приложение изолировано в песочнице: доступ к камере, контактам и геолокации требует явного разрешения в Info.plist и объяснения пользователю.

9. Хранилище данных: Core Data

Core Data — фреймворк Apple для хранения объектов в SQLite-базе. Модель описывается в файле .xcdatamodeld: сущности, атрибуты, связи. Контейнер загружается так:

let container = NSPersistentContainer(name: "MyModel")
container.loadPersistentStores { _, error in
    if let error = error {
        print("Ошибка загрузки: \(error)")
    }
}

Добавить и прочитать объект:

// Запись
let note = Note(context: viewContext)
note.text = "Купить продукты"
try? viewContext.save()

// Чтение через fetch request
let request = NSFetchRequest<Note>(entityName: "Note")
let notes = (try? viewContext.fetch(request)) ?? []

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

Для современных приложений всё чаще используют лёгкую альтернативу — Swift Data, которая теснее интегрирована со SwiftUI и проще в синтаксисе.

10. Симулятор и отладка

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

# Работа с симулятором из терминала
xcrun simctl boot "iPhone 15"      # запустить устройство
xcrun simctl list devices          # список доступных
xcrun simctl io booted screenshot shot.png  # скриншот экрана

Отладка включает: консоль (print и LLDB), точки останова с пошаговым проходом и инструмент Instruments для анализа памяти и производительности. Траблы тренируются через панель Debug Area внизу окна.

Отладка на реальном устройстве требует бесплатного Apple ID и подписанного профиля; разблокировка возможностей (push-уведомления, встроенные покупки) — платной программы разработчика.

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

Путь до пользователей: Product → Archive → распределение сборки → App Store Connect. Перед этим подписывается сертификат сборки и настраивается список устройств для тестирования через TestFlight.

# Автоматизация релиза через fastlane
fastlane sigh        # скачать сертификаты и профили
fastlane beta        # отправить сборку в TestFlight
fastlane deliver     # загрузить метаданные и сборку
Программа разработчика стоит 99 $ в год. Без неё собранное приложение работает максимум на 7 дней и только на своих устройствах.
Приложение проверяют модераторы, поэтому соблюдай правила контента и используй только те API, которые есть в приватной документации Apple. Тестовых пользователей подключай через TestFlight ещё до релиза.

12. Параллельность: async/await

Современная параллельность в Swift строится на ключевых словах async и await. Асинхронная функция не блокирует интерфейс: пока она ждёт ответ, пользователь продолжает взаимодействовать с приложением.

func loadAvatar() async throws -> UIImage {
    let url = URL(string: "https://example.com/avatar.png")!
    let (data, _) = try await URLSession.shared.data(from: url)
    guard let image = UIImage(data: data) else {
        throw URLError(.badServerResponse)
    }
    return image
}

Task {
    do {
        avatarView.image = try await loadAvatar()
    } catch {
        print("Не удалось загрузить аватар: \(error)")
    }
}

Параллельный запуск нескольких задач даёт async let, а групповая работа — withThrowingTaskGroup.

Интерфейс обновляется только на главном потоке. Перед сменой UI из фоновой задачи возвращайся к нему через await MainActor.run или оборачивай код в @MainActor.
Для параллельных независимых загрузок используй async let: ускорение почти линейное, а код остаётся таким же читаемым, как последовательный.

13. Оптимизация приложения

Медленное приложение теряет пользователей быстрее, чем плохой дизайн. Первый шаг — найти узкие места встроенным инструментом Instruments (Product → Profile), а не оптимизировать «вслепую».

В Instruments есть готовые шаблоны: Time Profiler для CPU, Allocations для памяти, Energy Log для батареи. Каждый профиль закрывает свой вопрос — измеряйте то, что реально беспокоит пользователя, а не всё подряд.

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

14. Тестирование: XCTest

Тесты ловят регрессии до релиза и позволяют рефакторить без страха. В Xcode создай target Unit Testing Bundle и пиши тесты на функции логики.

import XCTest

final class CartTests: XCTestCase {
    func testAddIncreasesCount() {
        var cart = Cart()
        cart.add(item: "Молоко")
        XCTAssertEqual(cart.count, 1)
    }
}

Запуск: Product → Test (⌘U). Полезные мастхэвы: XCTAssertEqual, XCTAssertThrowsError для проверки ошибок и setUp/tearDown для чистой среды между тестами.

Тестируй «чистую» логику (расчёты цены, валидация), а не UI сразу: тесты на UI пишутся медленно и часто ломаются от косметических правок.

Производительность тестов тоже важна: медленный набор отбивает желание запускать его. Держите логику быстрой, а сетевые проверки выносите в моки или фейковые ответы URLProtocol.

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

15. Совместная работа SwiftUI и UIKit

Старые приложения написаны на UIKit, новые — на SwiftUI, и их приходится смешивать. Swift напоминает, что оба фреймворка уживаются в одном проекте.

// Обёртка для UIKit-контроллера
struct CameraController: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> UIImagePickerController {
        UIImagePickerController()
    }
    func updateUIViewController(_ vc: UIImagePickerController, context: Context) {}
}

// Использование в SwiftUI
CameraController().ignoresSafeArea()

Такой подход позволяет переносить проект постепенно: новые экраны на SwiftUI, а редкие UIKit-компоненты (камера, редактирование фото) оборачиваются точечно.

Не переписывай работающий UIKit в SwiftUI «для красоты»: ценность переписи есть только там, где это упрощает развитие.
В делегаты UIKit-обёрток передавай замыкания: var onPhotoPicked: (UIImage) -> Void — так родительский SwiftUI-код остаётся декларативным.

16. Безопасность iOS-приложения

Телефон — личное устройство, и пользователи ожидают, что данные защищены. Токены и пароли нельзя хранить в UserDefaults — они попадают в обычные файлы. Правильное место — Keychain.

// Сохранение в Keychain через SecItemAdd
let data = token.data(using: .utf8)!
let query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrAccount as String: "api_token",
    kSecValueData as String: data
]
SecItemDelete(query as CFDictionary)
SecItemAdd(query as CFDictionary, nil)
Никогда не логируй токены и пароли. Одна строчка print(token), утёкшая в краш-репорты, — это утечка данных.
Для шифрования файлов используй встроенные защищённые контейнеры (Data Protection) или библиотеки на базе CommonCrypto — собственные «схемы шифрования» почти всегда слабее.

17. Камера, галерея и файлы

Запрос доступа к камере и фото — обязательный шаг в iOS: система покажет объяснение из Info.plist и спросит разрешение пользователя.

// Info.plist — объяснение причин
NSCameraUsageDescription -> "Нужно для добавления фото профиля"
NSPhotoLibraryUsageDescription -> "Нужно для выбора изображений"

// Проверка разрешения
import Photos

PHPhotoLibrary.requestAuthorization { status in
    print(status == .authorized ? "Доступ ок" : "Доступ закрыт")
}
  1. Добавь ключи в Info.plist — без них приложение просто упадёт при вызове API.
  2. Запроси статус PHPhotoLibrary.requestAuthorization и обработай отказ.
  3. При отказе веди пользователя в Настройки: UIApplication.open на страницу приложения.

Извлечение фото: PHImageManager.requestImage с указанием размера; для камеры — UIImagePickerController или кастомная сессия AVFoundation.

Проси доступ в момент действия (нажал «добавить фото»), а не при старте: так пользователь понимает причину, и отказов меньше.
Загрузка больших видео без сжатия «съест» память. Перед отправкой уменьшай изображения до 2048 px и конвертируй видео в разумные битрейты.

18. Мониторинг приложения в продакшене

Локальные запуски не показывают, что происходит на устройствах пользователей: краши, падения FPS и долгие запуски видны только в системах мониторинга.

// Подключение MetricKit
import MetricKit

class MetricsDelegate: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            print(payload.cpuMetrics?.cumulativeCPUTime ?? 0)
        }
    }
}
MXMetricManager.shared.add(self)

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

Перед выходом новой версии сравнивай метрики с предыдущей: если crash-rate вырос на полпроцента, это повод разобраться, а не «маленький скачок».
Подключи диагностику сети: измерители вроде URLSessionMetric покажут, где теряются секунды — на сервере или на ответе клиента.

19. SwiftData и миграции данных

SwiftData — современная замена Core Data: объявление модели через макрос @Model и минимум кода для хранения. Он удобен в новых проектах, где важен язык, а не legacy-стек.

import SwiftData

@Model
final class Task {
    var title: String
    var isDone: Bool
    var createdAt: Date = Date()

    init(title: String, isDone: Bool = false) {
        self.title = title
        self.isDone = isDone
    }
}

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

ИзменениеТип миграции
новое обязательное полелёгкая + дефолт в init
переименование атрибуталёгкая с mapping
разделение одной сущноститяжёлая, ручная
Держите базы-«фикстуры» для тестов: маленькая подписка с данными старой схемы проверяет миграцию за секунды.
Не удаляйте данные «на глазах» при первой ошибке: сначала бэкап, потом тест миграции на копии профиля.
SwiftData и Core Data могут работать параллельно, но миграция между ними — отдельный проект: не делайте её «заодно».

20. Анимации и жесты в SwiftUI

Анимации в SwiftUI декларативны: вы описываете конечное состояние, а переход делает система. Вместе с жестами это основа «живого» интерфейса без ручных таймеров.

@State private var isExpanded = false

Button {
    withAnimation(.spring(response: 0.35)) {
        isExpanded.toggle()
    }
} label: {
    Image(systemName: "chevron.right")
        .rotationEffect(.degrees(isExpanded ? 90 : 0))
}

Ключевой принцип: анимируйте изменения, а не «рестартуйте» вью. Для простых случаев хватает animation(_:value:) при изменении конкретного значения.

Держите длительности короткими (0.2–0.4 с): пользователь воспринимает интерфейс как быстрый, а не как украшенный.
Анимированное перемещение из одного экрана в другой — это и про подачу: matchedGeometryEffect даёт плавный переход между вью.
Не анимируйте layout каждого касания без нужды: постоянные сбросы анимаций съедают батарею и выглядят «дёргано».

21. Combine: реактивные потоки

Combine — фреймворк для работы с потоком событий: таймеры, сеть, поля ввода, изменения пользователя. Поток описывается цепочкой операторов — это сильно отличается от императивного «вызвал и забыл».

import Combine

let search = searchField.textPublisher
    .debounce(for: .milliseconds(300), scheduler: RunLoop.main)
    .removeDuplicates()
    .map { $0.lowercased() }
    .sink { query in
        print("Ищем: \(query)")
    }

Практика: debounce для поиска, combineLatest для валидации формы, текущий токен + запрос к API через flatMap. Потоки легко тестировать — значения подаются вручную.

Начинайте с двух операторов (debounce, combineLatest) — этого хватает 80% задач «поиска и форм».
Combine — «реактивный» стиль: если проект прост — async/await честнее. Фреймворк окупается при обилии событий.
Подписки без AnyCancellable «сливаются» при освобождении scope: храните их в долгоживущих свойствах, иначе поток тихо прекратится.

22. Локализация и String Catalog

Локализация — не перевод слов, а готовность приложения к другой культуре: числовые форматы, даты, валюты, направление текста. В Xcode 15+ переводы живут в едином String Catalog (.xcstrings).

// Localizable.xcstrings (фрагмент)
{
  "items" : { "count": "товара" },
  "hello" : "Привет, {name}!"
}
АспектЧто учесть
Числа1 200,50 vs 1,200.5
Даты12.09.2026 vs Sep 12, 2026
Плюрализация1 товар / 2 товара / 5 товаров
RTL-языкизеркальная вёрстка и выравнивание

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

Заведите процесс: экспорт каталога для переводчиков и проверка скриншотами каждого языка перед релизом.
Плюрализация в русском языке — минное поле: правила меняются с последней цифрой. Доверяйтесь стандартному API, а не собственным условиям.
Локализация касается и метаданных приложения (название, описание в App Store) — это отдельный каталог, его легко забыть.

Что дальше

Углуби язык: описал базовый синтаксис — подтяни архитектуру через книги и реальные проекты. Для серверной интеграции пригодится курс REST API, для работы в команде — Git и GitHub. Сравнение подходов смотри в курсах Android (Kotlin), Flutter и React Native.

← Вернуться к курсам