Swift: краткий практический справочник
Краткое введение в повседневный Swift: объявления, типы, функции, коллекции, обработка ошибок, проектирование типов и конкурентность.
Основано на темах
The Swift Programming Language, Swift 6.4; примеры ниже написаны специально для этого конспекта.
Swift — компилируемый статически типизированный язык. Компилятор выводит типы из контекста, но проверяет их до запуска программы. Язык старается сделать обычный код безопасным: значения инициализируются до использования, выход за границы коллекции проверяется, отсутствие значения выражается явно через Optional, а ошибки можно обрабатывать типизированно.
1. Переменные, константы и типы
Используйте let, если значение не должно меняться; var — если оно будет переприсваиваться. Тип часто можно не указывать: Swift выведет его из начального значения. Тип можно записать явно после двоеточия.
let title = "Swift" // String
var attempts: Int = 0
attempts += 1
let price = 19.95 // Double
let isAvailable = true // Bool
let message = "\(title): \(attempts) attempt(s)"
String — строка,
Int — целое число,
Double — число с плавающей точкой,
Bool — логическое значение.
Swift не выполняет неявное преобразование между разными числовыми типами:
let whole = 4
let fraction = 0.5
let total = Double(whole) + fraction
Условие в if должно иметь тип Bool; значения вроде 1 и 0 не считаются логическими.
2. Условия и циклы
let score = 82
if score >= 90 {
print("отлично")
} else if score >= 60 {
print("зачёт")
} else {
print("нужно повторить")
}
for number in 1...3 {
print(number) // 1, 2, 3
}
for index in 0..<3 {
print(index) // 0, 1, 2
}
... включает обе границы, ..< не включает верхнюю. switch проверяет значение по нескольким шаблонам; каждый непустой вариант должен быть покрыт, обычно через default.
let statusCode = 404
switch statusCode {
case 200:
print("успешно")
case 400..<500:
print("ошибка запроса")
case 500..<600:
print("ошибка сервера")
default:
print("другой ответ")
}
Для раннего выхода из функции или цикла используйте guard:
func greeting(for name: String?) -> String {
guard let name, !name.isEmpty else {
return "Привет!"
}
return "Привет, \(name)!"
}
Как работает guard:
- Условие в
guardдолжно быть истинным, чтобы выполнение продолжилось. - Если условие ложно, выполняется ветка
else, и из текущего scope нужен обязательный выход:return,throw,break,continueилиfatalError. - Значения, извлечённые в
guard let, доступны послеguardв оставшейся части функции, что уменьшает вложенность по сравнению сif let.
enum GuardExampleError: Error {
case emptyMessage
case missingRecipient
}
func send(message: String?, to userID: String?) throws {
guard let message, !message.isEmpty else {
throw GuardExampleError.emptyMessage
}
guard let userID else {
throw GuardExampleError.missingRecipient
}
// message и userID здесь уже не Optional
print("Send '\(message)' to \(userID)")
}
3. Функции и замыкания
Функция объявляется через func. Внешние метки аргументов делают вызов читаемым; _ убирает метку. Возвращаемый тип указывается после ->.
func convert(_ value: Double, toUnit unit: String = "km") -> String {
"\(value) \(unit)"
}
let distance = convert(12.5, toUnit: "mi")
let defaultDistance = convert(12.5)
Замыкание — блок кода, который можно сохранить или передать функции. Методы коллекций map, filter и sorted принимают замыкания:
let values = [3, 1, 4, 2]
let squares = values.map { $0 * $0 }
let evenSquares = squares.filter { $0.isMultiple(of: 2) }
let ascending = values.sorted { left, right in left < right }
$0, $1 и далее — краткие имена параметров замыкания. Если логика сложная, дайте параметрам имена или вынесите код в отдельную функцию.
4. Строки и коллекции
Array хранит упорядоченный список, Set — уникальные значения без фиксированного порядка, Dictionary связывает ключи со значениями.
var tasks = ["прочитать", "повторить"]
tasks.append("попрактиковаться")
let uniqueTags: Set<String> = ["swift", "cli", "swift"]
let scores = ["Ada": 98, "Linus": 91]
if let adaScore = scores["Ada"] {
print("Оценка Ada: \(adaScore)")
}
for task in tasks {
print(task)
}
Индекс массива допустим только в диапазоне 0..<count; доступ по ключу словаря возвращает optional, поскольку ключа может не быть. Не обращайтесь к индексу, если коллекция может быть пустой.
Строки объединяют интерполяцией ("\(value)"). Многострочные литералы заключаются в """; граница отступа задаётся закрывающими кавычками.
let name = "Mina"
let note = """
Участник: \(name)
Статус: активен
"""
5. Optional и безопасная работа с отсутствием значения
Int? — это Optional<Int>: значение либо содержит Int, либо равно nil.
Optional заставляет явно обработать ситуацию, когда данных нет.
let rawAge = "34"
let age = Int(rawAge) // Int?
if let age {
print("Через год: \(age + 1)")
} else {
print("Возраст не распознан")
}
if let временно разворачивает optional только в успешной ветке. Для значения по умолчанию используйте ??; для последовательного доступа — optional chaining ?..
let nickname: String? = nil
let displayName = nickname ?? "Гость"
struct Profile {
let email: String?
}
let profile: Profile? = Profile(email: "user@example.org")
let email = profile?.email // String?
Не используйте ! для обычного разворачивания: если значения нет, программа аварийно завершится. Принудительное разворачивание уместно только там, где инвариант гарантирован и проверен.
6. Ошибки
Для ожидаемых сбоев функция может объявлять throws. Вызов такой функции помечается try, а ошибка обрабатывается через do/catch.
enum InputError: Error {
case empty
case notANumber(String)
}
func parseScore(_ text: String) throws -> Int {
guard !text.isEmpty else { throw InputError.empty }
guard let score = Int(text) else {
throw InputError.notANumber(text)
}
return score
}
do {
let score = try parseScore("87")
print("Баллы: \(score)")
} catch InputError.empty {
print("Введите значение")
} catch {
print("Не удалось разобрать ввод: \(error)")
}
Проброс ошибки через несколько функций
Если функция не может (или не должна) решать, как обрабатывать ошибку, она просто помечается throws и пробрасывает ошибку выше по цепочке вызовов.
enum MessagingError: Error {
case missingRecipient
case transportOffline
}
func validateRecipient(_ id: String?) throws -> String {
guard let id, !id.isEmpty else {
throw MessagingError.missingRecipient
}
return id
}
func buildPayload(text: String, recipientID: String?) throws -> [String: String] {
let recipient = try validateRecipient(recipientID) // ошибка идёт дальше
return ["to": recipient, "text": text]
}
func performRequest(payload: [String: String]) throws {
let online = false
guard online else { throw MessagingError.transportOffline }
print("Send payload: \(payload)")
}
func sendMessage(text: String, recipientID: String?) throws {
let payload = try buildPayload(text: text, recipientID: recipientID)
try performRequest(payload: payload) // проброс на уровень выше
}
do {
try sendMessage(text: "Hi", recipientID: nil)
} catch MessagingError.missingRecipient {
print("Укажите получателя")
} catch MessagingError.transportOffline {
print("Сеть недоступна, повторите позже")
} catch {
print("Неожиданная ошибка: \(error)")
}
try? превращает ошибку в nil, если достаточно знать только «получилось/не получилось». try! отключает обработку ошибки и подходит лишь для гарантированно безопасного вызова.
7. Свои типы: struct, enum, class
struct — тип-значение: при присваивании или передаче копируется само значение. Это хороший выбор по умолчанию для моделей данных. class — ссылочный тип: несколько переменных могут указывать на один экземпляр;
классы поддерживают наследование.
struct Book {
let title: String
var page: Int
mutating func turnPage() {
page += 1
}
}
var book = Book(title: "Практика Swift", page: 1)
book.turnPage()
mutating разрешает методу структуры изменять её свойства. let-экземпляр структуры неизменяем целиком.
Перечисление задаёт конечный набор вариантов. Варианты могут содержать значения; switch позволяет обработать их по типобезопасным шаблонам.
import Foundation
enum DownloadState {
case idle
case loading(progress: Double)
case finished(URL)
case failed(message: String)
}
func describe(_ state: DownloadState) -> String {
return switch state {
case .idle:
"ожидание"
case .loading(let progress):
"загрузка: \(Int(progress * 100))%"
case .finished(let url):
"готово: \(url.lastPathComponent)"
case .failed(let message):
"ошибка: \(message)"
}
}
Здесь URL требует import Foundation.
8. Протоколы, расширения и обобщения
Протокол описывает требования к типу. Тип принимает эти требования через conformance; общий код может работать с любым подходящим типом.
protocol IdentifiableItem {
var id: String { get }
var displayName: String { get }
}
struct Note: IdentifiableItem {
let id: String
let displayName: String
}
func printItem<T: IdentifiableItem>(_ item: T) {
print("\(item.displayName) [\(item.id)]")
}
T — параметр типа. Ограничение T: IdentifiableItem гарантирует, что у значения есть нужные свойства. Так сохраняется конкретный тип без приведения к небезопасному Any.
Расширение добавляет вычисляемые свойства и методы существующему типу, не изменяя его исходное объявление:
import Foundation
extension String {
var isBlank: Bool {
trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
}
Этот пример также требует import Foundation.
9. Ссылки и управление памятью
Классы управляются ARC (Automatic Reference Counting): объект освобождается, когда на него больше нет сильных ссылок. Сильные циклы ссылок могут удерживать объекты в памяти. Для обратных ссылок применяют weak или unowned с учётом их разных гарантий (weak становится nil, поэтому объявляется optional).
final class Controller {
var onClose: (() -> Void)?
func configure() {
onClose = { [weak self] in
self?.close()
}
}
private func close() {
print("Закрыто")
}
}
Захват [weak self] не даёт замыканию удерживать Controller сильной ссылкой. Для структур и перечислений проблема циклического удержания не возникает так же, как для ссылочных типов.
10. Асинхронность и конкурентность
async помечает асинхронную функцию, await — точку, где выполнение может приостановиться, пока функция получает результат. Асинхронный вызов обычно не блокирует поток.
func loadTitle() async throws -> String {
try await Task.sleep(for: .milliseconds(20))
return "Готово"
}
func showTitle() async {
do {
print(try await loadTitle())
} catch {
print("Задача отменена или завершилась с ошибкой")
}
}
Несколько независимых задач можно запустить параллельно через async let;
результат читается с await.
Для динамического количества дочерних задач используют task group. Структурированная конкурентность связывает время жизни дочерних задач с родительской задачей.
actor защищает своё изменяемое состояние: доступ к его изолированным свойствам из другого контекста требует await.
actor Counter {
private var value = 0
func increment() {
value += 1
}
func currentValue() -> Int {
value
}
}
func readCounter() async {
let counter = Counter()
await counter.increment()
let value = await counter.currentValue()
print(value)
}
В Swift 6 компилятор строже проверяет передачу данных между конкурентными контекстами. Тип Sendable означает, что значение безопасно передавать между задачами; value-типы с безопасными полями обычно проще сделать потокобезопасными, чем разделяемое изменяемое состояние.
Ниже — самые полезные практические паттерны async-кода.
10.1 Параллельные независимые запросы (async let)
Если операции не зависят друг от друга, запускайте их одновременно:
func loadProfileName() async throws -> String {
try await Task.sleep(for: .milliseconds(30))
return "Alex"
}
func loadUnreadCount() async throws -> Int {
try await Task.sleep(for: .milliseconds(40))
return 7
}
func loadDashboard() async throws -> String {
async let name = loadProfileName()
async let unread = loadUnreadCount()
// await здесь дождётся обоих результатов
return "User: \(try await name), unread: \(try await unread)"
}
async let хорошо подходит для фиксированного числа задач. Если задач динамически много — используйте TaskGroup.
10.2 Динамический fan-out (withThrowingTaskGroup)
TaskGroup позволяет создать переменное число дочерних задач и собрать их результаты по мере готовности.
func fetchValue(id: Int) async throws -> Int {
try await Task.sleep(for: .milliseconds(10))
return id * id
}
func loadAll(ids: [Int]) async throws -> [Int] {
try await withThrowingTaskGroup(of: Int.self, returning: [Int].self) { group in
for id in ids {
group.addTask {
try await fetchValue(id: id)
}
}
var result: [Int] = []
for try await value in group {
result.append(value)
}
return result.sorted()
}
}
Если одна дочерняя задача бросит ошибку, withThrowingTaskGroup пробросит её наружу, а остальные дочерние задачи будут отменены.
10.3 Отмена задач (cancellation)
Отмена кооперативная: задача должна сама проверять флаг отмены и корректно завершаться.
func longOperation() async throws -> Int {
var sum = 0
for i in 1...10_000 {
if i.isMultiple(of: 500) {
try Task.checkCancellation() // бросает CancellationError
try await Task.sleep(for: .milliseconds(1))
}
sum += i
}
return sum
}
let task = Task {
try await longOperation()
}
task.cancel()
defer и withTaskCancellationHandler полезны для очистки ресурсов при досрочном завершении.
10.4 UI-обновления и @MainActor
Код, который трогает состояние UI-модели, лучше изолировать на главном акторе.
@MainActor
final class ViewModel {
private(set) var title = "Loading..."
func refresh() async {
do {
let text = try await loadTitle()
title = text
} catch {
title = "Error"
}
}
}
@MainActor гарантирует последовательный доступ к этому состоянию из конкурентного кода и упрощает reasoning о гонках.
10.5 Потоки данных (AsyncSequence)
for await позволяет читать значения постепенно, как поток событий.
struct CounterStream: AsyncSequence {
typealias Element = Int
struct Iterator: AsyncIteratorProtocol {
var current = 1
mutating func next() async -> Int? {
guard current <= 3 else { return nil }
defer { current += 1 }
try? await Task.sleep(for: .milliseconds(20))
return current
}
}
func makeAsyncIterator() -> Iterator { Iterator() }
}
func consumeStream() async {
for await value in CounterStream() {
print("event:", value)
}
}
Такой стиль удобен для логов, сокетов, прогресса загрузки и любых долгоживущих последовательностей событий.
11. Инициализация и деинициализация
Инициализатор init подготавливает тип к использованию. Для структур это часто главный способ гарантировать инварианты объекта. У классов дополнительно есть deinit, который вызывается перед освобождением экземпляра ARC.
import Foundation
struct Temperature {
let celsius: Double
init(celsius: Double) {
self.celsius = celsius
}
init(fahrenheit: Double) {
self.celsius = (fahrenheit - 32) * 5 / 9
}
}
final class FileLogger {
private let handle: FileHandle
init?(path: String) {
guard FileManager.default.createFile(atPath: path, contents: nil) else {
return nil
}
guard let h = FileHandle(forWritingAtPath: path) else { return nil }
self.handle = h
}
func write(_ line: String) throws {
try handle.write(contentsOf: Data((line + "\n").utf8))
}
deinit {
try? handle.close()
}
}
init? — проваливающийся инициализатор (может вернуть nil), полезен для валидации входных данных.
12. Type Casting (is, as?, as!)
Когда работа идёт с иерархией классов или массивом значений базового типа, часто нужно узнать реальный тип объекта во время выполнения.
class MediaItem {
let title: String
init(title: String) { self.title = title }
}
final class Movie: MediaItem {
let director: String
init(title: String, director: String) {
self.director = director
super.init(title: title)
}
}
final class Song: MediaItem {
let artist: String
init(title: String, artist: String) {
self.artist = artist
super.init(title: title)
}
}
let library: [MediaItem] = [
Movie(title: "Blade Runner", director: "Ridley Scott"),
Song(title: "Nude", artist: "Radiohead")
]
for item in library {
if let movie = item as? Movie {
print("Фильм: \(movie.title), режиссёр \(movie.director)")
} else if let song = item as? Song {
print("Трек: \(song.title), артист \(song.artist)")
}
}
is— проверка типа (item is Movie).as?— безопасное приведение сOptional.as!— принудительное приведение; используйте только при гарантии типа.
13. Вложенные типы (Nested Types)
Типы можно объявлять внутри других типов, чтобы локализовать детали модели.
struct Payment {
enum Method {
case cash
case card(last4: String)
}
struct Receipt {
let id: String
let total: Double
}
let method: Method
let amount: Double
let receipts: [Receipt]
}
let p = Payment(
method: .card(last4: "1024"),
amount: 49.9,
receipts: [
.init(id: "r-001", total: 29.9),
.init(id: "r-002", total: 20.0)
]
)
// Снаружи nested-тип пишется через полное имя:
let allReceipts: [Payment.Receipt] = p.receipts
Так внешнее API остаётся компактным, а связанные сущности не «засоряют» глобальное пространство имён.
14. Opaque и Boxed protocol types (some и any)
some Protocol скрывает конкретный возвращаемый тип от вызывающего кода, но оставляет его фиксированным для конкретной функции. any Protocol хранит значение неизвестного конкретного типа (type erasure, «коробка»).
protocol Shape {
func area() -> Double
}
struct Square: Shape {
let side: Double
func area() -> Double { side * side }
}
func makeUnitShape() -> some Shape {
Square(side: 1)
}
func printArea(of shape: any Shape) {
print(shape.area())
}
let unit = makeUnitShape() // конкретный тип скрыт
printArea(of: unit) // принимаем "любую фигуру"
some полезен для стабильного API с сокрытием реализации, any — когда нужно хранить разнородные значения по протоколу в одной коллекции.
15. Memory Safety и эксклюзивный доступ
Swift запрещает одновременный конфликтующий доступ к памяти: например, две записи в одно место или чтение+запись, перекрывающиеся по времени.
var balance = 100
func deposit(_ amount: Int, to value: inout Int) {
value += amount
}
deposit(20, to: &balance) // OK
Типичный анти-паттерн — одновременно передавать одну переменную как inout в два параметра одной функции (или читать и писать её же внутри пересекающихся областей). Компилятор и рантайм отслеживают такие конфликты и останавливают опасный код.
16. Продвинутые операторы (фрагмент)
Swift поддерживает побитовые операции и пользовательские операторы.
let mask: UInt8 = 0b0000_1111
let value: UInt8 = 0b1010_1100
let lowBits = value & mask // 0b0000_1100
let shifted = value >> 2 // 0b0010_1011
Пользовательский оператор стоит вводить только когда это реально улучшает читаемость доменной модели:
infix operator **: MultiplicationPrecedence
func ** (lhs: Double, rhs: Double) -> Double {
pow(lhs, rhs)
}
let x = 2.0 ** 3.0 // 8.0
17. Макросы (обзор)
Макросы генерируют Swift-код на этапе компиляции и уменьшают шаблонный код. В прикладном коде чаще встречается потребление уже готовых макросов, чем написание своих.
@attached(member, names: named(init))
macro AutoInit()
Важная мысль: макрос меняет исходный код до проверки типов, поэтому к диагностике и стилю надо относиться так же строго, как к обычному коду. Для реального проекта макрос обычно подключается из отдельного пакета.
18. Language Reference: короткий ориентир
Полный справочник языка значительно формальнее Language Guide. Ниже минимум, который полезно знать в повседневной работе.
18.1 Lexical Structure
Лексика определяет, как исходник разбивается на токены: идентификаторы, литералы, операторы, комментарии, пробелы.
let café = "unicode identifier"
let hex = 0xFF
let scientific = 1.2e3
18.2 Types / Expressions / Statements
- Types: именованные (
Int,String) и составные ((Int, String),() -> Void,[Int],[String: Int],Int?). - Expressions: вычисление значения (
a + b,foo(),x ? y : z). - Statements: управление потоком (
if,for,while,switch,guard,return,throw,defer).
let pair: (Int, String) = (1, "one")
let numbers: [Int] = [1, 2, 3]
func execute(_ body: () -> Void) {
defer { print("done") }
body()
}
18.3 Declarations / Attributes / Patterns
- Declarations вводят имена:
let,var,func,struct,class,enum,protocol,extension,typealias. - Attributes уточняют поведение декларации или типа:
@discardableResult,@available,@MainActor,@Sendable. - Patterns используются в
switch,if case,guard case, destructuring.
@discardableResult
func log(_ message: String) -> Int {
print(message)
return message.count
}
let point = (x: 2, y: 5)
switch point {
case (0, 0):
print("origin")
case let (x, y) where x == y:
print("diagonal \(x)")
default:
print("(\(point.x), \(point.y))")
}
18.4 Generic Parameters and Arguments
Обобщения объявляют параметры типа (<T>) и ограничения (where), а при вызове компилятор подставляет конкретные аргументы типа.
func firstMatch<T>(in values: [T], where predicate: (T) -> Bool) -> T? {
for v in values where predicate(v) {
return v
}
return nil
}
let match = firstMatch(in: [10, 15, 20]) { $0.isMultiple(of: 3) } // 15
18.5 Summary of Grammar
Грамматика в книге — формальная EBNF-нотация для полного синтаксиса Swift.
Практически она нужна, когда вы:
- разбираете сложный синтаксис из diagnostics компилятора;
- пишете инструменты (lint/format/parser);
- уточняете граничный случай языка.
19. Пакеты, зависимости, форматирование и Makefile
Даже в небольшом CLI-проекте лучше сразу держать единый workflow: зависимости через SwiftPM, одинаковое форматирование и повторяемые команды сборки.
19.1 Как добавлять dependencies через SwiftPM
В Package.swift зависимость объявляется в секции dependencies, а затем подключается к нужному target в targets[].dependencies.
// swift-tools-version: 6.4
import PackageDescription
let package = Package(
name: "demo-cli",
platforms: [.macOS(.v13)],
products: [
.executable(name: "demo-cli", targets: ["demo-cli"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.8.2")
],
targets: [
.executableTarget(
name: "demo-cli",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
)
]
)
Базовые команды:
swift package resolve # резолв зависимостей и запись Package.resolved
swift build # сборка
swift test # тесты
swift package update # обновить версии в допустимых границах
Практика:
– фиксируйте минимально нужную версию (from:), не «слишком новую заранее»;
– коммитьте Package.resolved для воспроизводимых сборок;
– после добавления dependency сразу проверяйте swift build и swift test.
19.2 Форматирование и линтинг (swift-format)
Обычно используют swift-format c конфигом в корне проекта.
Пример .swift-format:
{
"version": 1,
"indentation": { "spaces": 4 },
"lineLength": 120
}
Полезные команды:
swift format lint --strict --recursive Sources Tests
swift format format --in-place --recursive Sources Tests
lintпроверяет стиль без изменения файлов;formatавтоматически переписывает код по правилам.
19.3 Пример Makefile для повседневной работы
Makefile убирает «магические» команды из головы и стандартизирует CI/локалку.
.PHONY: build test lint fmt check release clean docs
build:
swift build
test:
swift test
lint:
swift format lint --strict --recursive Sources Tests
fmt:
swift format format --in-place --recursive Sources Tests
check: build test lint
release:
swift build -c release
docs:
swift package --allow-writing-to-package-directory generate-docc-reference
clean:
swift package clean
Минимальный цикл перед коммитом:
make check
Для релиза:
make release
Если проект использует дополнительные инструменты (например, swiftlint), их добавляют отдельными целями (lint-swiftlint, lint-all) без смешивания с базовым swift format lint.
20. Как правильно организовать структуру проекта
Хорошая структура проекта решает три задачи: понятная навигация, независимое тестирование и безопасное расширение без «комка» зависимостей.
20.1 Базовая структура SwiftPM-проекта
Для CLI и библиотек удобен стандарт SwiftPM:
my-project/
├─ Package.swift
├─ Package.resolved
├─ README.md
├─ Sources/
│ ├─ AppCLI/ # executable target (точка входа)
│ │ ├─ main.swift # или @main type
│ │ ├─ Commands/
│ │ │ ├─ RootCommand.swift
│ │ │ ├─ DeckCommand.swift
│ │ │ └─ CardCommand.swift
│ │ └─ Support/
│ │ └─ Output.swift
│ └─ Core/ # library target с бизнес-логикой
│ ├─ Domain/
│ │ ├─ Deck.swift
│ │ └─ Card.swift
│ ├─ Services/
│ │ ├─ SchedulerService.swift
│ │ └─ ReviewService.swift
│ └─ Persistence/
│ ├─ Database.swift
│ └─ Repositories/
├─ Tests/
│ ├─ CoreTests/
│ └─ AppCLITests/
├─ docs/
└─ Makefile
Идея: AppCLI занимается только парсингом аргументов/выводом, а вся логика живёт в Core. Это упрощает тесты и повторное использование кода.
20.2 Принцип «слои и зависимости в одну сторону»
Хорошее правило для модулей:
Domainне зависит от CLI/БД/сети;Servicesзависят отDomain;Persistenceреализует протоколы (репозитории/хранилища);AppCLIзависит отCore, но не наоборот.
Такой граф зависимостей предотвращает циклы и делает замену инфраструктуры (например, SQLite → Postgres, CLI → HTTP API) локальной задачей.
20.3 Группировка по feature vs по техническим слоям
Есть два рабочих подхода:
- По слоям (
Domain/Services/Persistence) — проще для небольших и средних проектов. - По фичам (
Deck/Review/Import) — удобнее, когда команд и сценариев становится много.
Компромисс: в Core держать feature-папки, а внутри каждой — мини-слои.
Sources/Core/
├─ Deck/
│ ├─ Deck.swift
│ ├─ DeckService.swift
│ └─ DeckRepository.swift
└─ Review/
├─ Review.swift
├─ ReviewService.swift
└─ ReviewRepository.swift
20.4 Тестовая структура
Tests/CoreTests— юнит-тесты чистой логики;Tests/AppCLITests— проверка команд, аргументов и текстового вывода;- общие тестовые фикстуры:
Tests/TestSupport(builders, mocks, sample data).
Полезный принцип: тесты должны повторять структуру Sources, чтобы нужный тест находился по имени файла/фичи за несколько секунд.
20.5 Именование файлов и типов
- Один основной тип на файл (
DeckService.swift→DeckService); - суффиксы по роли:
...Service,...Repository,...Command; - протоколы рядом с use-case (
DeckRepository+SQLiteDeckRepository); - не смешивайте в одном файле CLI-команды и предметную логику.
20.6 Что часто ломает структуру
- «Утечка»
Foundation/SQL/HTTP в domain-модели без необходимости; - создание god-file вроде
Utils.swiftилиHelpers.swiftс десятками несвязанных функций; - дублирование моделей отдельно для CLI и core без чёткой причины;
- циклические импорты между target-ами.
Если сомневаетесь, выносите не по «красоте», а по границам изменений: код, который изменяется вместе, должен лежать рядом.
20.7 Как AppCLI видит Core и что делать с одинаковыми именами
AppCLI «видит» Core, только если зависимость явно объявлена в
Package.swift, а в коде есть import Core.
// Package.swift (фрагмент)
targets: [
.target(name: "Core"),
.executableTarget(
name: "AppCLI",
dependencies: ["Core"]
)
]
// Sources/AppCLI/main.swift
import Core
let service = DeckService()
Важно: файлы Deck.swift и Card.swift внутри одного и того же target образуют один namespace модуля Core. Поэтому две глобальные функции с одним и тем же именем и сигнатурой вызовут ошибку компиляции, даже если лежат в разных файлах.
Плохой пример (конфликт):
// Deck.swift
func parse(_ text: String) -> Int { 1 }
// Card.swift
func parse(_ text: String) -> Int { 2 } // redeclaration error
Правильные варианты:
- держать функции как методы соответствующих типов;
- использовать namespace через
enum/structбез состояния; - различать сигнатуры (например, argument labels), если это действительно разные операции.
enum DeckParser {
static func parse(_ text: String) -> Int { 1 }
}
enum CardParser {
static func parse(_ text: String) -> Int { 2 }
}
let a = DeckParser.parse("...")
let b = CardParser.parse("...")
Если одинаковые имена приходят из разных модулей, обычно помогает явная квалификация через имя модуля/типа, чтобы снять неоднозначность.
21. Подключение внешних lib*.so
На Linux в Swift есть два основных сценария:
- статическое подключение API на этапе сборки (рекомендуется): через
systemLibrarytarget в SwiftPM; - динамическая загрузка в рантайме: через
dlopen/dlsym.
21.1 Рекомендуемый путь: systemLibrary + modulemap
Если у вас есть libmylib.so и заголовок mylib.h, подключайте библиотеку как C-модуль.
Package.swift (пример):
// swift-tools-version: 6.4
import PackageDescription
let package = Package(
name: "demo",
targets: [
.systemLibrary(
name: "CMyLib",
pkgConfig: "mylib"
),
.executableTarget(
name: "app",
dependencies: ["CMyLib"],
linkerSettings: [
.unsafeFlags(["-L/opt/mylib/lib"]),
.linkedLibrary("mylib")
]
)
]
)
Sources/CMyLib/module.modulemap:
module CMyLib [system] {
header "/opt/mylib/include/mylib.h"
link "mylib"
export *
}
Использование в Swift:
import CMyLib
let rc = mylib_init()
print("init rc =", rc)
Что важно:
– библиотека должна иметь C ABI;
– если исходник на C++, обычно нужен C-wrapper (extern "C");
– рантайм должен находить .so (через LD_LIBRARY_PATH, rpath или установку в системный путь линковщика).
21.2 Динамическая загрузка в рантайме: dlopen/dlsym
Нужна, если библиотека опциональна или выбирается по конфигу во время запуска.
import Glibc
typealias InitFn = @convention(c) () -> Int32
let handle = dlopen("/opt/mylib/lib/libmylib.so", RTLD_NOW)
guard handle != nil else {
fatalError(String(cString: dlerror()))
}
defer { dlclose(handle) }
guard let sym = dlsym(handle, "mylib_init") else {
fatalError(String(cString: dlerror()))
}
let fn = unsafeBitCast(sym, to: InitFn.self)
let rc = fn()
print("init rc =", rc)
Минусы этого подхода: меньше проверок на этапе компиляции и выше риск ошибок в подписи функции/символах. Поэтому для обычного проекта предпочтителен systemLibrary.
22. Один проект: lib.so + несколько утилит
Да, в одном SwiftPM-проекте можно собирать сразу несколько артефактов: динамическую библиотеку и несколько исполняемых файлов, которые от неё зависят.
22.1 Пример Package.swift
// swift-tools-version: 6.4
import PackageDescription
let package = Package(
name: "my-stack",
products: [
.library(name: "Core", type: .dynamic, targets: ["Core"]), // libCore.so
.executable(name: "server", targets: ["Server"]),
.executable(name: "client", targets: ["Client"])
],
targets: [
.target(name: "Core"),
.executableTarget(name: "Server", dependencies: ["Core"]),
.executableTarget(name: "Client", dependencies: ["Core"])
]
)
Сборка:
swift build -c release
swift build -c release --product server
swift build -c release --product client
По умолчанию артефакты лежат в .build/..., а не в ./lib и ./bin.
22.2 Как получить layout lib/ + bin/
Обычно добавляют install-цель в Makefile, которая копирует артефакты после swift build -c release.
.PHONY: release install
release:
swift build -c release
install: release
mkdir -p ./lib ./bin
cp ./.build/release/libCore.so ./lib/
cp ./.build/release/server ./bin/
cp ./.build/release/client ./bin/
Чтобы server/client нашли libCore.so при запуске, используйте один из вариантов:
LD_LIBRARY_PATH=./lib ./bin/server- rpath при линковке (предпочтительно для поставки)
- установка
.soв системный путь линковщика
22.3 Рекомендуемая структура для такого проекта
my-stack/
├─ Package.swift
├─ Sources/
│ ├─ Core/
│ │ ├─ Domain/
│ │ ├─ Services/
│ │ └─ Persistence/
│ ├─ Server/
│ │ └─ main.swift
│ └─ Client/
│ └─ main.swift
├─ Tests/
│ ├─ CoreTests/
│ ├─ ServerTests/
│ └─ ClientTests/
├─ bin/ # install-артефакты (не из git)
├─ lib/ # install-артефакты (не из git)
└─ Makefile
Core держит бизнес-логику и контракты, Server/Client — только transport, CLI/сеть и composition root. Это уменьшает дублирование и упрощает тестирование.
23. Парсинг аргументов CLI: команды и подкоманды
Для production CLI в Swift обычно используют
swift-argument-parser:
он даёт типобезопасные аргументы, автогенерацию --help и понятную структуру команд.
23.1 Базовый шаблон команды
import ArgumentParser
@main
struct App: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "app",
abstract: "Demo CLI"
)
@Argument(help: "Имя пользователя")
var name: String
@Option(name: .shortAndLong, help: "Количество повторов")
var count: Int = 1
@Flag(name: .shortAndLong, help: "Подробный вывод")
var verbose = false
mutating func run() throws {
guard count > 0 else {
throw ValidationError("--count must be > 0")
}
for _ in 0..<count {
print(verbose ? "Hello, \(name) [verbose]" : "Hello, \(name)")
}
}
}
Здесь:
– @Argument — позиционный аргумент;
– @Option — параметр со значением (--count 3);
– @Flag — булев переключатель (--verbose).
23.2 Иерархия команд и подкоманд
Корневой тип описывает дерево подкоманд через CommandConfiguration.
import ArgumentParser
@main
struct Recall: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "recall",
abstract: "Flashcards CLI",
subcommands: [Deck.self, Card.self]
)
}
struct Deck: ParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Manage decks",
subcommands: [DeckAdd.self, DeckList.self, DeckDelete.self]
)
}
struct DeckAdd: ParsableCommand {
static let configuration = CommandConfiguration(
commandName: "add",
abstract: "Add deck"
)
@Argument(help: "Deck name")
var name: String
mutating func run() throws {
print("add deck:", name)
}
}
Так пользователь получает предсказуемый UX:
– recall deck add rust
– recall deck list
– recall card add ...
23.3 Где делать валидацию аргументов
Есть два основных места:
validate()— проверка корректности параметров доrun();run()— выполнение команды и бизнес-логика.
struct ImportCommand: ParsableCommand {
@Option(help: "Path to file")
var file: String
func validate() throws {
guard file.hasSuffix(".txt") else {
throw ValidationError("Only .txt files are supported")
}
}
mutating func run() throws {
print("Import from", file)
}
}
23.4 Рекомендуемая структура файлов для CLI-команд
Sources/AppCLI/
├─ main.swift
├─ Commands/
│ ├─ RootCommand.swift
│ ├─ Deck/
│ │ ├─ DeckCommand.swift
│ │ ├─ DeckAddCommand.swift
│ │ ├─ DeckListCommand.swift
│ │ └─ DeckDeleteCommand.swift
│ └─ Card/
│ ├─ CardCommand.swift
│ └─ CardAddCommand.swift
└─ Support/
├─ Output.swift
└─ Parsing/
└─ CommonOptions.swift
Правило: корневые команды/подкоманды живут в Commands, переиспользуемые типы аргументов и опций — в Support.
24. Mock в тестах: как подменять зависимости
В Swift чаще всего делают mock через протокол и подставную реализацию в тесте. Это позволяет тестировать бизнес-логику без реальной БД, сети или файловой системы.
24.1 Базовый паттерн: Protocol + Service + Mock
protocol DeckRepository {
func save(name: String) throws
}
enum DeckError: Error {
case emptyName
}
struct DeckService {
let repository: DeckRepository
func createDeck(name: String) throws {
guard !name.isEmpty else { throw DeckError.emptyName }
try repository.save(name: name)
}
}
Mock-реализация для теста:
final class MockDeckRepository: DeckRepository {
var savedNames: [String] = []
var saveError: Error?
func save(name: String) throws {
if let saveError { throw saveError }
savedNames.append(name)
}
}
Пример теста:
import Testing
@testable import Core
@Test
func createDeck_savesName() throws {
let mock = MockDeckRepository()
let service = DeckService(repository: mock)
try service.createDeck(name: "swift")
#expect(mock.savedNames == ["swift"])
}
24.2 Mock, Stub, Spy — в чём разница
- Stub: возвращает заранее подготовленный результат.
- Mock: проверяет ожидаемые взаимодействия (вызов, аргументы, количество).
- Spy: записывает факты вызовов, которые проверяются после выполнения.
На практике одна тестовая реализация может совмещать роли (например, MockDeckRepository выше — и stub, и spy).
24.3 Правила для удобных тестов
- зависимости сервиса передавайте через инициализатор (dependency injection);
- не создавайте реальные БД/HTTP-клиенты внутри бизнес-методов;
- для каждого внешнего ресурса сначала определяйте протокол, затем конкретную реализацию и тестовый double;
- в тестах проверяйте наблюдаемое поведение (результат и контракты вызовов), а не внутренние детали реализации.
25. Как правильно работать с тестами
Хорошие тесты в Swift-проекте — это не только assertions, но и понятная структура каталогов, корректные импорты и предсказуемый запуск.
25.1 Размещение тестов
Обычно структура повторяет Sources:
Sources/
├─ Core/
│ ├─ Deck/
│ └─ Review/
└─ AppCLI/
Tests/
├─ CoreTests/
│ ├─ Deck/
│ │ └─ DeckServiceTests.swift
│ └─ Review/
│ └─ ReviewServiceTests.swift
├─ AppCLITests/
│ └─ DeckCommandTests.swift
└─ TestSupport/
├─ Mocks/
├─ Builders/
└─ Fixtures/
Плюс такого layout: по имени production-файла сразу понятно, где лежит тест.
25.2 Импорт модулей в тестах
Основные варианты:
import Core— тестируете только публичное API;@testable import Core— нужен доступ кinternalсущностям target-а.
import Testing
@testable import Core
@Test
func createDeck_rejectsEmptyName() {
let service = DeckService(repository: MockDeckRepository())
#expect(throws: DeckError.emptyName) {
try service.createDeck(name: "")
}
}
@testable работает, когда модуль собран с -enable-testing (для swift test
это обычно уже так).
25.3 Именование тестов
- Файл:
<TypeName>Tests.swift(DeckServiceTests.swift); - Тест:
scenario_expectedResult(createDeck_emptyName_throws); - Один тест проверяет одну наблюдаемую вещь.
Это упрощает чтение отчёта CI и ускоряет локальную диагностику.
25.4 TestSupport: где держать mocks/fixtures/builders
- Mocks/Spies — в
Tests/TestSupport/Mocks; - Fixtures (json/sql/txt sample data) — в
Tests/TestSupport/Fixtures; - Builders/Factories — в
Tests/TestSupport/Builders.
Не переносите тестовые helper-ы в Sources, если они не нужны production-коду.
25.5 Запуск и изоляция тестов
Минимальные команды:
swift test
swift test --filter DeckServiceTests
swift test --filter createDeck_emptyName_throws
Рекомендации:
– юнит-тесты не должны зависеть от сети/времени/случайности без контроля;
– для времени используйте внедрение Clock/DateProvider через протокол;
– для I/O используйте временные директории и очищайте их в defer.
25.6 Частые проблемы и как избежать
No such module 'Core'— target не добавлен в dependencies тестового target-а вPackage.swift.- Недоступен
internalсимвол — нужен@testable importвместо обычногоimport. - Хрупкие тесты из-за глобального состояния — убирайте синглтоны из тестового пути, внедряйте зависимости явно.
26. defer: гарантированная очистка и финализация
defer откладывает выполнение блока кода до выхода из текущего scope (функции/метода/замыкания). Это удобно, когда cleanup должен произойти при любом исходе: успешный return, ранний throw или выход по условию.
26.1 Базовая идея
func process() {
print("start")
defer { print("finish") }
print("work")
}
process()
// start
// work
// finish
defer выполняется один раз, прямо перед выходом из scope.
26.2 defer + ошибки
При throw отложенный блок всё равно выполнится:
enum WorkError: Error { case failed }
func runWork(shouldFail: Bool) throws {
defer { print("cleanup") }
if shouldFail { throw WorkError.failed }
print("success")
}
Это делает defer безопасным местом для закрытия ресурсов, unlock и rollback.
26.3 Порядок нескольких defer
Если defer несколько, они выполняются в порядке LIFO (последний добавленный выполняется первым).
func orderDemo() {
defer { print("third") }
defer { print("second") }
defer { print("first") }
print("body")
}
orderDemo()
// body
// first
// second
// third
26.4 Типичные use-cases
- закрытие файловых дескрипторов/соединений;
- снятие блокировок (
lock/unlock); - откат временного состояния;
- финальный лог результата операции.
import Foundation
func writeTempFile() throws {
let path = "/tmp/swift-defer-demo.txt"
FileManager.default.createFile(atPath: path, contents: nil)
guard let handle = FileHandle(forWritingAtPath: path) else { return }
defer { try? handle.close() } // выполнится всегда
try handle.write(contentsOf: Data("hello\\n".utf8))
}
Главное правило: в defer помещайте только завершение/cleanup, а не основную бизнес-логику.
27. Практические правила
- Выбирайте
letпо умолчанию; меняйте данные только черезvar. - Предпочитайте
structи перечисления, если не нужна идентичность объекта или наследование. - Представляйте отсутствие данных через
Optional, а ожидаемые сбои — черезthrows. - Давайте функциям и аргументам ясные имена; используйте протоколы для требований, а обобщения — для повторно используемого типобезопасного кода.
- Не применяйте
!как замену проверке; не передавайте разделяемое состояние конкурентным задачам без изоляции. - Держите детали реализации закрытыми: по умолчанию top-level объявления имеют доступ
internal; используйтеprivateдля локальных деталей иpublicтолько для API модуля.
Этот конспект остаётся кратким, но теперь покрывает не только базовый Language Guide, а также ключевые темы Language Reference. Для полной глубины и всех edge-cases смотрите The Swift Programming Language и документацию Swift.