Skip to content

Swift Шпаргалка

Язык Apple для iOS, macOS и других платформ.

01

Основы

Переменные и константы

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

swift
var name = "Alice"        // mutable
let age = 30               // immutable constant
let pi: Double = 3.14159   // explicit type
var count: Int = 0
count = 10
print(type(of: age))       // Int

Опционалы

Опционалы представляют отсутствие значения. Используйте if-let для безопасного извлечения, ?? для значений по умолчанию и ! только когда вы уверены, что значение существует (рискованно — может вызвать сбой).

swift
var nickname: String? = nil
nickname = "Al"
if let n = nickname {
    print("Hi, \(n)")
}
print(nickname ?? "Anonymous")  // nil-coalescing
let force: String = nickname!   // force unwrap (crashes if nil)

Кортежи

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

swift
let person = (name: "Alice", age: 30)
print(person.name)   // Alice
print(person.0)      // Alice
let (n, a) = person
print("\(n), \(a)")  // Alice, 30

Преобразование типов

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

swift
let intVal = 42
let doubleVal = Double(intVal)
let strVal = String(intVal)
let fromStr = Int("100")!   // 100
let invalid = Int("abc")    // nil
print(doubleVal, strVal)

Утверждения и предусловия

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

swift
let age = -5
assert(age >= 0, "Age cannot be negative")
precondition(age >= 0, "Age must be non-negative")
// In debug builds, assert crashes if false
// precondition checks in release builds too
02

Строки и символы

Интерполяция строк

Интерполяция строк с \(expr) встраивает любое выражение в строку. Она типобезопасна и вычисляется во время компиляции, что делает её безопаснее строк форматирования.

swift
let name = "Alice"
let age = 30
let msg = "Name: \(name), Age: \(age)"
let calc = "5 + 3 = \(5 + 3)"
print(msg)        // Name: Alice, Age: 30
print(calc)       // 5 + 3 = 8

Распространённые строковые методы

Строки Swift корректны с точки зрения Unicode, то есть count отражает фактическое количество символов (кластеров графем), а не байтов. Используйте эти методы вместо ручной индексации для безопасности.

swift
let s = "Hello, World"
print(s.count)              // 12
print(s.uppercased())       // HELLO, WORLD
print(s.lowercased())       // hello, world
print(s.hasPrefix("Hello")) // true
print(s.contains("World"))  // true
print(s.reversed())         // "dlroW ,olleH"

Подстроки и индексация

Индексы строк не являются целыми числами, поскольку символы могут иметь разный размер в байтах. Используйте index(_:offsetBy:) для навигации. Подстроки разделяют память с оригиналом — преобразуйте в String для долгосрочного хранения.

swift
let s = "Hello, World"
let idx = s.index(s.startIndex, offsetBy: 7)
print(s[idx])                     // W
let sub = s[idx...]               // "World"
let newStr = String(sub)          // convert to String
print(s.prefix(5))                // "Hello"

Многострочные строки

Строки в тройных кавычках сохраняют переносы строк и отступы. Закрывающая """ определяет базовый отступ. Идеально для HTML, JSON или длинных текстовых блоков.

swift
let poem = """
Roses are red,
Violets are blue,
Swift is great,
And so are you.
"""
print(poem)
// Use \\(expr) for interpolation in multiline

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

Используйте joined(separator:) для конкатенации с разделителем и split(separator:) для токенизации. Это эффективнее ручных циклов с конкатенацией через +.

swift
let parts = ["apple", "banana", "cherry"]
let joined = parts.joined(separator: ", ")
print(joined)  // apple, banana, cherry
let csv = "a,b,c"
let fields = csv.split(separator: ",")
print(fields)  // ["a", "b", "c"]
let reversed = String(s.reversed())
03

Структуры данных

Массив

Массивы — это упорядоченные коллекции с нулевой индексацией. Используйте append/insert для добавления, filter/map/reduce для преобразований. Предпочитайте типы-значения (Array является структурой) для потокобезопасности.

swift
var nums = [1, 2, 3]
nums.append(4)
nums.insert(0, at: 0)
nums[1] = 20
print(nums.count)        // 5
print(nums.contains(20)) // true
let evens = nums.filter { $0 % 2 == 0 }
print(nums.first, nums.last)

Словарь

Словари хранят пары ключ-значение со средним временем поиска O(1). Используйте default: для избежания nil при доступе к отсутствующим ключам. Ключи должны соответствовать Hashable.

swift
var ages: [String: Int] = ["Alice": 30]
ages["Bob"] = 25
ages["Alice"] = 31
print(ages["Alice", default: 0])  // 31
for (name, age) in ages {
    print("\(name): \(age)")
}
print(ages.keys.sorted())

Множество

Множества хранят уникальные значения с проверкой принадлежности за O(1). Идеально для дедупликации и операций над множествами (объединение, пересечение, разность). Элементы должны соответствовать Hashable.

swift
var a: Set<Int> = [1, 2, 3]
var b: Set<Int> = [3, 4, 5]
print(a.union(b))           // {1,2,3,4,5}
print(a.intersection(b))    // {3}
print(a.subtracting(b))     // {1,2}
print(a.isSubset(of: b))    // false
a.insert(6)

Диапазоны

..< — полузакрытый диапазон (исключает верхнюю границу), ... — закрытый диапазон (включает обе границы). Диапазоны полезны в циклах, срезах и сопоставлении шаблонов с оператором ~=.

swift
for i in 0..<5 { print(i) }   // 0,1,2,3,4
for i in 0...5 { print(i) }   // 0,1,2,3,4,5
let nums = Array(1...5)       // [1,2,3,4,5]
let prefix = nums.prefix(3)   // [1,2,3]
if 5...10 ~= 7 { print("in range") }

Функции высшего порядка

map преобразует каждый элемент, filter выбирает подходящие элементы, reduce объединяет все в один. Это основа функционального программирования в Swift и позволяет создавать лаконичные, читаемые конвейеры обработки данных.

swift
let nums = [1, 2, 3, 4, 5]
let doubled = nums.map { $0 * 2 }       // [2,4,6,8,10]
let evens = nums.filter { $0 % 2 == 0 } // [2,4]
let sum = nums.reduce(0, +)             // 15
let strs = nums.map { String($0) }      // ["1","2",...]
print(doubled, evens, sum)
04

Поток управления

If / Else If / Else

Стандартное условное ветвление. Условия должны быть логическими (тип Bool). Swift требует фигурных скобок даже для одиночных операторов для предотвращения таких ошибок, как уязвимость Apple goto fail.

swift
let score = 85
if score >= 90 {
    print("A")
} else if score >= 80 {
    print("B")
} else if score >= 70 {
    print("C")
} else {
    print("F")
}

Switch (сопоставление шаблонов)

Switch в Swift мощный: поддерживает привязку значений, кортежи, условия where и диапазоны. Он должен быть исчерпывающим (default покрывает остальные случаи) и не проваливается по умолчанию.

swift
let point = (2, 0)
switch point {
case (0, 0):
    print("origin")
case (_, 0):
    print("on x-axis")
case (0, _):
    print("on y-axis")
case let (x, y) where x == y:
    print("on diagonal")
default:
    print("elsewhere: \(point.0),\(point.1)")
}

Циклы For-In

For-in перебирает диапазоны, массивы, словари и любой Sequence. Используйте enumerated(), когда нужен индекс. Используйте _ для игнорирования неиспользуемых переменных цикла.

swift
for i in 0..<5 { print(i) }
let fruits = ["apple", "banana"]
for (i, fruit) in fruits.enumerated() {
    print("\(i): \(fruit)")
}
let dict = ["a": 1, "b": 2]
for (k, v) in dict { print("\(k)=\(v)") }
for _ in 0..<3 { print("tick") }

While и Repeat-While

while проверяет условие перед каждой итерацией; repeat-while проверяет после (как do-while в C). Используйте repeat-while, когда тело должно выполниться хотя бы один раз.

swift
var n = 5
while n > 0 {
    print(n)
    n -= 1
}
var x = 0
repeat {
    x += 1
} while x < 3
print(x)  // 3

Guard (ранний выход)

guard обеспечивает ранний выход, когда предпосылки не выполнены. Развёрнутые опционалы остаются доступными в остальной части области видимости, уменьшая вложенность. Предпочитайте guard глубокой вложенности if-let.

swift
func greet(_ name: String?) {
    guard let n = name, !n.isEmpty else {
        print("No name provided")
        return
    }
    print("Hello, \(n)")
    // n is unwrapped and available here
}
greet("Alice")
greet(nil)
05

Функции и замыкания

Определение функции

Используйте _ для пропуска меток аргументов ради читаемости. Значения параметров по умолчанию делают параметры необязательными. Функции с одним выражением имеют неявный возврат (Swift 5.9+).

swift
func greet(_ name: String, greeting: String = "Hello") -> String {
    return "\(greeting), \(name)!"
}
print(greet("Alice"))               // Hello, Alice!
print(greet("Bob", greeting: "Hi")) // Hi, Bob!
func add(_ a: Int, _ b: Int) -> Int { a + b }
print(add(3, 4))  // 7

Множественные возвращаемые значения

Кортежи позволяют возвращать несколько именованных значений. Возврат опционального кортежа сигнализирует о возможной неудаче. Доступ через r.min или r.0 — именованные элементы понятнее.

swift
func minMax(_ nums: [Int]) -> (min: Int, max: Int)? {
    guard let first = nums.first else { return nil }
    var mn = first, mx = first
    for n in nums { mn = min(mn, n); mx = max(mx, n) }
    return (mn, mx)
}
if let r = minMax([3, 1, 4, 1, 5]) {
    print("min=\(r.min), max=\(r.max)")
}

Замыкания

Замыкания — это самодостаточные блоки функциональности. Используйте $0, $1 для сокращённых имён аргументов. Синтаксис завершающего замыкания (опускание метки последнего аргумента) идиоматичен для map/filter/reduce.

swift
let square: (Int) -> Int = { x in x * x }
print(square(5))  // 25
let add: (Int, Int) -> Int = { $0 + $1 }
print(add(3, 4))  // 7
let nums = [1, 2, 3]
let doubled = nums.map { $0 * 2 }
print(doubled)  // [2, 4, 6]

Escaping и авто-замыкания

@escaping помечает замыкания, сохраняемые или вызываемые после возврата из функции (нужно для асинхронных обратных вызовов). @autoclosure оборачивает выражение в замыкание, откладывая его вычисление (используется в assert).

swift
var handlers: [() -> Void] = []
func saveHandler(_ fn: @escaping () -> Void) {
    handlers.append(fn)
}
saveHandler { print("done") }
handlers.first?()
// @autoclosure delays evaluation
func logIfTrue(_ cond: @autoclosure () -> Bool) {
    if cond() { print("true") }
}
logIfTrue(2 > 1)

Параметры inout

Параметры inout позволяют функциям изменять переменную вызывающего (передача по ссылке). Используйте умеренно — предпочитайте возврат новых значений для ясности. Префикс & отмечает место изменения.

swift
func swap(_ a: inout Int, _ b: inout Int) {
    let temp = a
    a = b
    b = temp
}
var x = 10, y = 20
swap(&x, &y)
print(x, y)  // 20 10
06

Классы и структуры

Структура (тип-значение)

Структуры — типы-значения, копируемые при присваивании. Используйте для простых контейнеров данных. Вычисляемые свойства (distance) вычисляются при доступе. Методы mutating изменяют self.

swift
struct Point {
    var x: Double
    var y: Double
    var distance: Double {
        (x * x + y * y).squareRoot()
    }
    mutating func moveBy(dx: Double, dy: Double) {
        x += dx; y += dy
    }
}
var p = Point(x: 3, y: 4)
print(p.distance)  // 5.0

Класс (ссылочный тип)

Классы — ссылочные типы, разделяемые через ссылки, поддерживают наследование и deinit. Используйте, когда нужна идентичность, разделяемое изменяемое состояние или совместимость с Objective-C. В остальном предпочитайте структуры.

swift
class Person {
    var name: String
    var age: Int
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    deinit { print("\(name) deallocated") }
}
let p = Person(name: "Alice", age: 30)
print(p.name)  // Alice

Наследование и переопределение

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

swift
class Animal {
    func speak() { print("...") }
}
class Dog: Animal {
    override func speak() { print("Woof!") }
}
class Cat: Animal {
    override func speak() { print("Meow!") }
}
let pets: [Animal] = [Dog(), Cat()]
for pet in pets { pet.speak() }

Свойства (вычисляемые и lazy)

Вычисляемые свойства имеют блоки get/set; set использует newValue по умолчанию. lazy откладывает инициализацию до первого доступа — полезно для дорогих или редко нужных свойств. Должно быть var.

swift
class Circle {
    var radius: Double
    var area: Double {
        get { .pi * radius * radius }
        set { radius = (newValue / .pi).squareRoot() }
    }
    lazy var heavy = loadExpensiveData()
    init(radius: Double) { self.radius = radius }
}
func loadExpensiveData() -> String { "data" }

Наблюдатели свойств

willSet/didSet наблюдают за изменениями свойств. Используйте didSet для проверки или реакции на изменения (например, ограничение значений, запуск обновлений UI). Наблюдатели не срабатывают во время init.

swift
class Counter {
    var count: Int = 0 {
        willSet { print("about to set \(newValue)") }
        didSet {
            print("was \(oldValue), now \(count)")
            if count > 10 { count = 10 }
        }
    }
}
let c = Counter()
c.count = 5
c.count = 20  // clamped to 10
07

Протоколы и расширения

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

Протоколы определяют чертёж методов и свойств. Типы соответствуют им, реализуя их. Используйте для абстракции, полиморфизма и слабой связности — похоже на интерфейсы в Java/C#.

swift
protocol Greetable {
    var name: String { get }
    func greet() -> String
}
struct User: Greetable {
    let name: String
    func greet() -> String { "Hi, \(name)" }
}
let u = User(name: "Alice")
print(u.greet())  // Hi, Alice

Расширения протоколов (реализация по умолчанию)

Расширения протоколов предоставляют реализации по умолчанию. Это обеспечивает ретроактивное моделирование и повторное использование кода без базового класса. Мощная функция для разделения поведения между несвязанными типами.

swift
protocol Describable {
    func describe() -> String
}
extension Describable {
    func describe() -> String { "A \(type(of: self))" }
}
struct Box: Describable { }
let b = Box()
print(b.describe())  // A Box

Расширения

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

swift
extension Int {
    var squared: Int { self * self }
    func times(_ block: () -> Void) {
        for _ in 0..<self { block() }
    }
}
print(5.squared)  // 25
3.times { print("hi") }  // prints hi 3 times

Обобщённые типы

Обобщённые типы позволяют писать гибкий, переиспользуемый код, работающий с любым типом с сохранением типобезопасности. Используйте <T> для параметров типа. Ограничения (where T: Equatable) сужают допустимые типы.

swift
func stackOf<T>(_ items: T...) -> [T] {
    var arr: [T] = []
    for item in items { arr.append(item) }
    return arr
}
struct Stack<Element> {
    private var items: [Element] = []
    mutating func push(_ e: Element) { items.append(e) }
}
let s = stackOf(1, 2, 3)  // [Int]

Протокол с ассоциированным типом

Ассоциированные типы позволяют протоколам использовать типы-заполнители, как обобщённые типы для протоколов. Соответствующий тип указывает фактический тип. Используйте typealias для явного указания или позвольте Swift вывести его.

swift
protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}
struct IntStack: Container {
    typealias Item = Int
    private var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}
08

Обработка ошибок

Определение и генерация ошибок

Ошибки соответствуют протоколу Error (обычно enum). throw сигнализирует об ошибке. Ассоциированные значения (coinsNeeded) несут контекст. Используйте throws для пометки функций, которые могут завершиться неудачей.

swift
enum VendingError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}
func vend(item: String, coins: Int) throws -> String {
    guard item == "Candy" else { throw VendingError.invalidSelection }
    guard coins >= 2 else { throw VendingError.insufficientFunds(coinsNeeded: 2) }
    return "Dispensing \(item)"
}

Do-Catch и Try

do-catch обрабатывает сгенерированные ошибки. try помечает вызовы, генерирующие ошибки. Сопоставление с конкретными случаями для целевой обработки. catch-all обрабатывает неожиданные ошибки. Ошибки распространяются вверх по стеку вызовов.

swift
do {
    let result = try vend(item: "Candy", coins: 1)
    print(result)
} catch VendingError.insufficientFunds(let needed) {
    print("Need \(needed) more coins")
} catch VendingError.outOfStock {
    print("Sold out")
} catch {
    print("Other error: \(error)")
}

Try? / Try!

try? преобразует ошибки в nil (возвращает Optional). try! предполагает успех и вызывает сбой при ошибке — используйте только когда неудача действительно невозможна. Предпочитайте try? с опциональной привязкой для постепенной деградации.

swift
let result1 = try? vend(item: "Candy", coins: 1)
print(result1)  // nil (error converted to optional)
let result2 = try! vend(item: "Candy", coins: 5)
print(result2)  // Dispensing Candy (crashes if error)
if let r = try? vend(item: "Candy", coins: 5) {
    print(r)
}

Тип Result

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

swift
enum FetchError: Error { case network, parse }
func fetch(_ url: String) -> Result<String, FetchError> {
    url.isEmpty ? .failure(.network) : .success("data from \(url)")
}
switch fetch("https://api.com") {
case .success(let data): print(data)
case .failure(let err): print("Error: \(err)")
}
let value = try? fetch("").get()

Defer (очистка)

defer планирует выполнение кода очистки при выходе из области видимости, независимо от способа (нормальный возврат, throw, ошибка). Используйте для освобождения ресурсов (файлы, блокировки). Несколько defer выполняются в порядке LIFO.

swift
func processFile(_ path: String) throws {
    let handle = openFile(path)
    defer { closeFile(handle) }
    // ... work that might throw ...
    let data = try read(handle)
    // closeFile always runs, even if throw
    print("processed \(data)")
}
09

Файловый ввод-вывод и дата/время

Чтение и запись файлов

Foundation предоставляет простые файловые API. write(toFile:atomically:) записывает безопасно (временный файл + переименование). Используйте String(contentsOfFile:) для текста. Для больших файлов используйте FileHandle для потоковой обработки.

swift
import Foundation
let path = "test.txt"
let content = "Hello, File I/O!"
try content.write(toFile: path, atomically: true,
                  encoding: .utf8)
let read = try String(contentsOfFile: path, encoding: .utf8)
print(read)  // Hello, File I/O!
let lines = read.split(separator: "\n")
print(lines.count)

URL и FileManager

FileManager обрабатывает операции с файловой системой (создание, удаление, проверка существования). Используйте URL (не пути) для современных API. .documentDirectory — постоянное хранилище приложения на iOS/macOS.

swift
import Foundation
let fm = FileManager.default
let url = fm.urls(for: .documentDirectory, in: .userDomainMask)[0]
    .appendingPathComponent("data.json")
try "data".write(to: url, atomically: true, encoding: .utf8)
let exists = fm.fileExists(atPath: url.path)
print("Exists: \(exists)")
try fm.removeItem(at: url)  // delete

Date и DateFormatter

Date представляет момент во времени (внутренне UTC). DateFormatter преобразует между Date и String — всегда устанавливайте locale в en_US_POSIX для разбора фиксированного формата, чтобы избежать ошибок локали.

swift
import Foundation
let now = Date()
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
fmt.locale = Locale(identifier: "en_US_POSIX")
let str = fmt.string(from: now)
print(str)  // e.g. 2024-01-15 14:30:00
let parsed = fmt.date(from: "2024-01-01 00:00:00")
print(parsed ?? "invalid")

Кодирование/декодирование JSON

Codable автоматизирует сериализацию JSON. JSONEncoder/JSONDecoder обрабатывают преобразование. Сделайте ваши типы соответствующими Codable — компилятор синтезирует логику. Используйте CodingKeys для настройки имён ключей.

swift
struct User: Codable {
    let name: String
    let age: Int
}
let user = User(name: "Alice", age: 30)
let json = try JSONEncoder().encode(user)
let str = String(data: json, encoding: .utf8)!
print(str)  // {"name":"Alice","age":30}
let decoded = try JSONDecoder().decode(User.self, from: json)
print(decoded.name)  // Alice

Вычисления с датами

Calendar обрабатывает арифметику дат с учётом часовых поясов и DST. Используйте dateComponents для извлечения полей или вычисления разниц. Никогда не используйте сырые секунды для математики дат — используйте Calendar API.

swift
import Foundation
let cal = Calendar.current
let now = Date()
let tomorrow = cal.date(byAdding: .day, value: 1, to: now)!
let components = cal.dateComponents([.year, .month, .day], from: now)
print(components.year!, components.month!)
let diff = cal.dateComponents([.hour], from: now, to: tomorrow)
print(diff.hour!)  // 24
10

Параллелизм и асинхронность

Async / Await

async/await (Swift 5.5+) делает асинхронный код похожим на синхронный. Task создаёт новый асинхронный контекст. async let выполняется параллельно и ожидает завершения. Устраняет ад обратных вызовов — предпочитайте обработчикам завершения.

swift
func fetchUser(_ id: Int) async -> String {
    try? await Task.sleep(nanoseconds: 1_000_000_000)
    return "User \(id)"
}
Task {
    let user = await fetchUser(42)
    print(user)  // User 42
    async let u1 = fetchUser(1)
    async let u2 = fetchUser(2)
    let users = await [u1, u2]  // concurrent
    print(users)
}

Task и отмена

Task представляет единицу асинхронной работы. Кооперативная отмена через Task.isCancelled — длительные задачи должны проверять периодически. task.value ожидает результат. Отмена кооперативная, не принудительная.

swift
let task = Task {
    for i in 1...100 {
        if Task.isCancelled { return }
        try? await Task.sleep(nanoseconds: 100_000_000)
        print(i)
    }
}
Task {
    try? await Task.sleep(nanoseconds: 500_000_000)
    task.cancel()  // request cancellation
}
await task.value  // wait for completion

Актёры (потокобезопасность)

Актёры (Swift 5.5+) защищают изменяемое состояние от гонок данных путем сериализации доступа. Весь доступ проходит через await. Используйте вместо блокировок/очередей для разделяемого состояния. Компилятор проверяет безопасность.

swift
actor Counter {
    private var count = 0
    func increment() { count += 1 }
    func value() -> Int { count }
}
let counter = Counter()
Task {
    await counter.increment()
    print(await counter.value())
}
// Actors serialize access—no data races

GCD (Grand Central Dispatch)

GCD — традиционный API параллелизма. DispatchQueue.global() для фоновой работы, .main для обновлений UI. QoS (.userInitiated, .background) приоритизирует задачи. Всё ещё полезен для не-асинхронного кода.

swift
import Dispatch
DispatchQueue.global(qos: .userInitiated).async {
    let result = heavyComputation()
    DispatchQueue.main.async {
        print("UI update: \(result)")
    }
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
    print("delayed by 2s")
}

Асинхронные последовательности

AsyncSequence позволяет перебирать асинхронные значения (как поток). Используйте for await для потребления. Идеально для пагинированных API, server-sent events или любого источника, производящего значения со временем.

swift
let urls = ["url1", "url2", "url3"]
func fetchAll() async {
    for url in urls {
        let data = await fetchData(url)
        print(data)
    }
}
func fetchData(_ url: String) async -> String {
    "data from \(url)"
}
// AsyncSequence: for await item in stream { ... }
11

Протоколы: глубокое погружение

Протоколы с ассоциированными типами

Ассоциированные типы (associatedtype) позволяют протоколам объявлять тип-заполнитель, который указывают соответствующие типы. Это эквивалент параметров обобщённого типа для протоколов в Swift. Тип выводится из методов соответствующего типа. Используйте предложения 'where' для ограничения ассоциированных типов. PAT (протоколы с ассоциированными типами) нельзя использовать как экзистенциальные типы напрямую без стирания типов или (Swift 5.7+) 'any Container'.

swift
protocol Container {
    associatedtype Item
    var count: Int { get }
    mutating func append(_ item: Item)
    subscript(i: Int) -> Item { get }
}

struct IntStack: Container {
    // associatedtype Item inferred as Int
    private var items: [Int] = []
    var count: Int { items.count }
    mutating func append(_ item: Int) { items.append(item) }
    subscript(i: Int) -> Int { items[i] }
}

// Generic constraint with associated type
func sum<C: Container>(_ c: C) -> Int where C.Item == Int {
    var total = 0
    for i in 0..<c.count { total += c[i] }
    return total
}

Расширения протоколов (реализации по умолчанию)

Расширения протоколов предоставляют реализации по умолчанию — соответствующие типы получают метод бесплатно, но могут его переопределить. Это способ Swift добавлять функциональность типам ретроактивно. Ограниченные расширения (where Element: Numeric) добавляют методы только к типам, соответствующим ограничению. Так стандартная библиотека добавляет map/filter/reduce всем коллекциям.

swift
protocol Describable {
    var description: String { get }
}

// Default implementation via extension
extension Describable {
    var description: String { "A \(type(of: self))" }
}

struct Point: Describable {
    let x, y: Int
    // Uses default description: "A Point"
}

struct Person: Describable {
    let name: String
    var description: String { "Person named (name)" }  // override
}

// Extension with constraints
extension Collection where Element: Numeric {
    var sum: Element { reduce(0, +) }
}
[1, 2, 3].sum  // 6

Композиция протоколов и экзистенциальные типы

Композиция протоколов (A & B) требует, чтобы значение соответствовало нескольким протоколам. Экзистенциальные типы (any Protocol) могут хранить любой соответствующий тип, но имеют накладные расходы динамической диспетчеризации — поиск таблицы свидетелей при каждом вызове. Используйте 'some Protocol' (непрозрачный возвращаемый тип), когда возвращаете один конкретный тип, но хотите его скрыть. Предпочитайте обобщённые типы экзистенциальным ради производительности; используйте экзистенциальные для неоднородных коллекций.

swift
protocol Named { var name: String { get } }
protocol Aged { var age: Int { get } }

struct Person: Named, Aged {
    let name: String
    let age: Int
}

// Protocol composition: requires both protocols
func wishHappyBirthday(to celebrant: Named & Aged) {
    print("Happy birthday, (celebrant.name), you're (celebrant.age)!")
}

let p = Person(name: "Alice", age: 30)
wishHappyBirthday(to: p)

// Existential types (any) — type erasure overhead
let items: [any Named] = [p, Pet(name: "Rex")]
for item in items { print(item.name) }

// Swift 5.7: 'any' keyword required for existentials
// 'some' for opaque types (returns one concrete type)

Протокол-ориентированное программирование

Протокол-ориентированное программирование — смена парадигмы Swift от ООП. Вместо иерархий классов проектируйте вокруг протоколов с реализациями по умолчанию. Преимущества: работает с типами-значениями (структуры/перечисления), поддерживает ретроактивное соответствие (расширение чужих типов), допускает множественное 'наследование' (тип может соответствовать многим протоколам). Классы всё ещё полезны для ссылочной семантики и совместимости с Objective-C, но структуры+протоколы предпочтительны для большинства моделей.

swift
// POP: design around protocols, not inheritance
protocol Drawable {
    func draw(on canvas: Canvas)
}

extension Drawable {
    // Default implementation + polymorphism
    func drawTwice(on canvas: Canvas) {
        draw(on: canvas)
        draw(on: canvas)
    }
}

struct Circle: Drawable { let radius: Double
    func draw(on canvas: Canvas) { /* ... */ }
}
struct Square: Drawable { let side: Double
    func draw(on canvas: Canvas) { /* ... */ }
}

// Value types + protocols = no inheritance needed
let shapes: [any Drawable] = [Circle(radius: 5), Square(side: 3)]
shapes.forEach { $0.draw(on: canvas) }

// Retroactive modeling (extend types you don't own)
extension Int: Drawable {
    func draw(on canvas: Canvas) { /* draw the number */ }
}

Пользовательские свидетели протоколов

Протоколы как зависимости (Repository) позволяют менять реализации (реальная vs mock) для тестирования и гибкости. Ассоциированные типы делают протоколы обобщёнными. Это внедрение зависимостей в Swift — внедряйте тип, соответствующий протоколу. 'Свидетель протокола' — соответствующий тип, предоставляющий конкретное поведение. Этот паттерн (репозиторий, источник данных) распространён в архитектуре Swift (VIPER, Clean Architecture).

swift
// Protocol with requirements
protocol Repository {
    associatedtype Entity
    func get(_ id: Int) -> Entity?
    func save(_ entity: Entity)
}

// Concrete implementation
struct UserRepo: Repository {
    typealias Entity = User
    func get(_ id: Int) -> User? { /* fetch */ nil }
    func save(_ user: User) { /* persist */ }
}

// Generic function using protocol
func display<R: Repository>(_ repo: R, id: Int) where R.Entity: CustomStringConvertible {
    if let entity = repo.get(id) {
        print(entity.description)
    }
}

// Test double for testing
struct MockUserRepo: Repository {
    typealias Entity = User
    var stored: [Int: User] = [:]
    func get(_ id: Int) -> User? { stored[id] }
    func save(_ user: User) { /* no-op for tests */ }
}
12

Обобщённые типы: глубокое погружение

Обобщённые функции и типы

Обобщённые типы позволяют писать гибкий, переиспользуемый код, работающий с любым типом с сохранением типобезопасности. T — параметр типа (заполнитель). Компилятор генерирует специализированные версии для производительности (без упаковки). Обобщённые типы (Stack<T>) сохраняют свой параметр типа. Element — обобщённый тип Array. Обобщённые типы разрешаются во время компиляции — накладных расходов во время выполнения нет, в отличие от экзистенциальных типов.

swift
// Generic function
func swap<T>(_ a: inout T, _ b: inout T) {
    let temp = a; a = b; b = temp
}
var x = 1, y = 2
swap(&x, &y)  // x=2, y=1

// Generic type
struct Stack<T> {
    private var items: [T] = []
    mutating func push(_ item: T) { items.append(item) }
    mutating func pop() -> T? { items.popLast() }
}

var intStack = Stack<Int>()
intStack.push(42)
var strStack = Stack<String>()
strStack.push("hello")

// Generic method on non-generic type
extension Array {
    func chunked(into size: Int) -> [[Element]] {
        stride(from: 0, to: count, by: size).map {
            Array(self[$0..<Swift.min($0 + size, count)])
        }
    }
}

Ограничения типов

Ограничения типов сужают допустимые типы: T: Equatable требует соответствия протоколу, T: SomeClass требует иерархии классов. Предложение where добавляет более сложные ограничения (например, сопоставление ассоциированных типов). Ограничения позволяют использовать методы протокола (== для Equatable, < для Comparable). Без ограничений можно только присваивать и передавать T — никаких операций.

swift
// Constraint: T must conform to Equatable
func indexOf<T: Equatable>(_ item: T, in array: [T]) -> Int? {
    for (i, elem) in array.enumerated() {
        if elem == item { return i }  // needs Equatable
    }
    return nil
}

// Multiple constraints
func max<T: Comparable>(_ a: T, _ b: T) -> T {
    return a > b ? a : b
}

// Constraint on associated type
protocol Repository {
    associatedtype Entity: Identifiable
    func find(_ id: Entity.ID) -> Entity?
}

// where clause for complex constraints
func merge<C1: Collection, C2: Collection>(
    _ c1: C1, _ c2: C2
) -> [C1.Element] where C1.Element == C2.Element {
    Array(c1) + Array(c2)
}

Непрозрачные типы (some)

Непрозрачные типы (some Protocol, Swift 5.1) возвращают конкретный тип, скрытый от вызывающего. В отличие от 'any' (экзистенциальный), тип фиксирован и известен компилятору — без упаковки, диспетчеризация на этапе компиляции. Это основа 'some View' в SwiftUI. Используйте 'some', когда хотите скрыть конкретный тип, сохранив производительность. Вызывающий может использовать методы протокола, но не может полагаться на конкретный тип.

swift
// Opaque return type: returns ONE concrete type
// (hidden from caller, but fixed per call site)
func makeStack() -> some Collection<Int> {
    return [1, 2, 3]  // concrete type is [Int]
}

let s = makeStack()  // type is 'some Collection<Int>'
print(s.count)  // 3

// Useful for hiding implementation details
protocol Shape { func draw() }
struct Circle: Shape { func draw() { print("circle") } }

func makeShape() -> some Shape {
    return Circle()  // caller doesn't know it's Circle
}

// Difference from existential (any):
// - some: one concrete type, no boxing, compile-time dispatch
// - any: can be different types, runtime dispatch, boxing

// Opaque types in properties
struct View {
    var body: some View { Text("Hello") }  // SwiftUI
}

Условное соответствие

Условное соответствие делает тип соответствующим протоколу только когда его параметры типа удовлетворяют определённым условиям. Array<Int> является Equatable, потому что Int является таковым; Array<MyStruct> — нет, если только MyStruct не является. Это распространяется: [[Int]] является Equatable, потому что [Int] является. Стандартная библиотека широко это использует — Array, Optional, Dictionary условно соответствуют Equatable, Hashable, Codable.

swift
// Array conforms to Equatable only if Element is Equatable
extension Array: Equatable where Element: Equatable {
    static func == (lhs: [Element], rhs: [Element]) -> Bool {
        return lhs.count == rhs.count && lhs.elementsEqual(rhs)
    }
}

[1, 2, 3] == [1, 2, 3]  // true (Int: Equatable)
// [SomeStruct] == [SomeStruct]  // error if SomeStruct isn't Equatable

// Conditional conformance for Hashable
extension Stack: Equatable where T: Equatable {
    static func == (lhs: Stack, rhs: Stack) -> Bool {
        return lhs.items == rhs.items
    }
}

// Enables: [Stack<Int>] == [Stack<Int>] (transitively)

Result и обобщённая обработка ошибок

Result<Success, Failure> — обобщённое перечисление для операций, которые могут завершиться неудачей, переносящих либо значение, либо ошибку. Полезно, когда нужно отложить обработку ошибок или сохранить результаты. map преобразует значение успеха; flatMap компонует операции, которые также могут завершиться неудачей. Result { try ... } преобразует функции, генерирующие ошибки. .get() преобразует обратно в throw. С async/await Result менее необходим, но всё ещё полезен для сохранённых/переданных ошибок.

swift
// Result: built-in generic for success/failure
enum Result<Success, Failure: Error> {
    case success(Success)
    case failure(Failure)
}

func fetchUser(_ id: Int) -> Result<User, FetchError> {
    if id < 0 { return .failure(.invalidId) }
    return .success(User(id: id))
}

switch fetchUser(42) {
case .success(let user): print(user)
case .failure(let err): print(err)
}

// map/flatMap for chaining
let result = fetchUser(1)
    .map { $0.name }
    .flatMap { name in fetchAvatar(name) }

// Throwing function → Result
let result = Result { try throwingFunction() }
// Result → throwing
let value = try result.get()
13

ARC и управление памятью

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

ARC (Automatic Reference Counting) управляет памятью для экземпляров классов (ссылочные типы). Каждая сильная ссылка увеличивает счётчик; удаление уменьшает. Когда счётчик достигает 0, объект освобождается немедленно (детерминированно, в отличие от сборки мусора). Структуры/перечисления (типы-значения) не используют ARC — они копируются. ARC не может обработать циклы ссылок — нужно использовать weak/unowned для их разрыва.

swift
class Person {
    let name: String
    init(name: String) { self.name = name; print("\(name) initialized") }
    deinit { print("\(name) deinitialized") }
}

var p1: Person? = Person(name: "Alice")  // "Alice initialized"
var p2 = p1  // strong ref count = 2
p1 = nil     // ref count = 1, not deinitialized
p2 = nil     // ref count = 0, "Alice deinitialized"

// ARC tracks strong references:
// - +1 on assignment/copy
// - -1 when variable goes out of scope
// - Object freed when count hits 0
// Unlike GC, deterministic (freed immediately when count=0)

Сильные, слабые и unowned ссылки

Сильные ссылки (по умолчанию) удерживают объекты живыми. weak ссылки не удерживают объект живым и становятся nil при освобождении объекта (должны быть optional var). unowned ссылки не удерживают объект живым, но не являются опциональными — используйте только когда уверены, что объект переживает ссылку (сбой при доступе после освобождения). Используйте weak для паттерна делегирования; unowned для родитель-потомок, где родитель всегда переживает потомка.

swift
class Person {
    var name: String
    var apartment: Apartment?  // strong
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // weak breaks cycle
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) deinit") }
}

var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")
alice?.apartment = apt
apt?.tenant = alice  // weak, doesn't keep alice alive

alice = nil  // "Alice deinit" (no strong cycle)
apt = nil    // "Apartment 4A deinit"

// unowned: non-optional, assumed to always have a value
// Use when the referenced object outlives the reference
class Customer {
    var card: CreditCard?  // strong
}
class CreditCard {
    unowned let owner: Customer  // non-nil, non-optional
    init(owner: Customer) { self.owner = owner }
}

Циклы удержания и замыкания

Замыкания захватывают ссылки сильно по умолчанию. Если класс хранит замыкание, захватывающее self, возникает цикл удержания (оба удерживают друг друга живыми → утечка памяти). Исправьте списками захвата: [weak self] (self становится опциональным, используйте guard let) или [unowned self] (не опциональный, сбой при nil). Всегда используйте weak self в замыканиях, хранимых как свойства или переданных долгоживущим объектам (наблюдатели, асинхронные задачи).

swift
class ViewModel {
    var name = "VM"
    var onUpdate: (() -> Void)?

    func setup() {
        // BAD: closure captures self strongly → cycle
        onUpdate = {
            print(self.name)  // strong capture
        }
        // ViewModel holds closure, closure holds ViewModel → leak!

        // GOOD: capture list with weak self
        onUpdate = { [weak self] in
            guard let self = self else { return }
            print(self.name)
        }

        // GOOD: unowned if self outlives closure
        onUpdate = { [unowned self] in
            print(self.name)
        }
    }
    deinit { print("VM deinit") }
}

Escaping и не-escaping замыкания

Не-escaping замыкания (по умолчанию) выполняются внутри функции и отбрасываются — нет риска цикла удержания, не нужен self. в замыкании. @escaping замыкания переживают функцию (сохраняются в свойстве, диспетчеризуются асинхронно и т.д.) — они могут вызывать циклы удержания, поэтому нужно явно использовать self. и рассмотреть [weak self]. Компилятор это обеспечивает. Большинство обработчиков завершения — @escaping; map/filter/reduce — не-escaping.

swift
class DataLoader {
    var storedHandler: (() -> Void)?

    // @escaping: closure outlives the function (stored or async)
    func load(with handler: @escaping (Data) -> Void) {
        storedHandler = { handler(cachedData) }  // stored
        DispatchQueue.global().async {
            handler(self.fetch())  // escapes via async
        }
    }

    // Non-escaping (default): closure runs during the function
    func process(_ transform: (Int) -> Int) -> [Int] {
        return [1, 2, 3].map(transform)  // runs now, no storage
    }
    // No need for [weak self]—self is alive during the call
}

// @escaping requires explicit self. in closures
class View {
    func load() {
        DataLoader().load { [self] data in  // self. required
            print(data)
        }
    }
}

Безопасность памяти и эксклюзивность

Swift обеспечивает эксклюзивность — переменная не может быть доступна (чтение+запись или запись+запись) одновременно из перекрывающихся областей. Это предотвращает гонки данных и неопределённое поведение. Параметры inout получают эксклюзивный доступ на время вызова. Методы mutating структур удерживают эксклюзивный доступ к self. Актёры (Swift 5.5+) обеспечивают эксклюзивность на уровне языка для разделяемого изменяемого состояния, устраняя гонки данных по конструкции.

swift
// Exclusivity: Swift prevents overlapping access to a variable
// This is a compile-time or runtime error:
var step = 1
func increment(_ n: inout Int) {
    step += 1  // accessing global while n is inout to step
}
// increment(&step)  // ERROR: overlapping access

// Fix with explicit copy
func safeIncrement(_ n: inout Int) {
    var copy = step
    copy += 1
    step = copy
}

// Struct mutating methods have exclusive access
struct Counter {
    var count = 0
    mutating func increment(_ other: inout Counter) {
        count += other.count  // ok, different instances
    }
}

// 'inout' parameters: exclusive access for duration
// 'isolation' (Swift 5.5+): actors enforce exclusivity
14

Основы SwiftUI

Представления и модификаторы

SwiftUI — декларативный UI-фреймворк Apple. Представления — структуры, соответствующие View со свойством 'body'. Модификаторы (.font, .padding) возвращают новые обёрнутые представления — они не мутируют. Представления — лёгкие типы-значения; SwiftUI сравнивает их для обновления фактического UI. Декларативный стиль описывает, как должен выглядеть UI для данного состояния, а SwiftUI обрабатывает переходы.

swift
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("Hello, World!")
                .font(.title)
                .fontWeight(.bold)
                .foregroundColor(.blue)

            Button(action: { print("tapped") }) {
                HStack {
                    Image(systemName: "star.fill")
                    Text("Tap me")
                }
            }
            .padding()
            .background(Color.orange)
            .cornerRadius(10)
        }
        .padding()
    }
}

// Views are value types (structs), declarative
// Modifiers return new views (immutability)

Состояние и привязка

@State для локального, изменяемого состояния представления — при изменении SwiftUI перерисовывает представление. @Binding позволяет дочернему представлению читать/писать @State родителя через префикс $ (создаёт привязку). State должен быть единственным источником истины; передавайте привязки вниз, а не значения для изменения. Для разделяемого/сложного состояния между представлениями используйте @StateObject/@ObservedObject/@EnvironmentObject с ObservableObject.

swift
struct CounterView: View {
    @State private var count = 0  // local, mutable state

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") { count += 1 }
        }
    }
}

// @Binding: child receives a reference to parent's state
struct StepperView: View {
    @Binding var value: Int
    var body: some View {
        HStack {
            Button("-") { value -= 1 }
            Text("\(value)")
            Button("+") { value += 1 }
        }
    }
}

// Parent passes binding with $
struct ParentView: View {
    @State var score = 0
    var body: some View {
        StepperView(value: $score)  // $ creates a Binding
    }
}

ObservableObject и EnvironmentObject

ObservableObject (класс) для разделяемого состояния между несколькими представлениями. Свойства @Published запускают обновления представления при изменении. @StateObject создаёт и владеет объектом (используйте на вершине иерархии). @EnvironmentObject внедряет объект в дерево представлений — любой потомок может получить к нему доступ без передачи через привязки. @ObservedObject получает существующий объект (не владеет им). Используйте @StateObject для владения, @ObservedObject/@EnvironmentObject для получения.

swift
// Model: ObservableObject for shared state
class AppModel: ObservableObject {
    @Published var username: String = ""  // @Published triggers UI update
    @Published var isLoggedIn: Bool = false

    func login() {
        // ... auth logic ...
        isLoggedIn = true
    }
}

// @StateObject: owns the model (created once)
struct RootView: View {
    @StateObject var model = AppModel()
    var body: some View {
        // Inject into environment
        ContentView().environmentObject(model)
    }
}

// @EnvironmentObject: receives from environment
struct ProfileView: View {
    @EnvironmentObject var model: AppModel
    var body: some View {
        Text("Hello, \(model.username)")
    }
}

// @ObservedObject: receives from parent (doesn't own)
struct LoginView: View {
    @ObservedObject var model: AppModel
}

Списки и навигация

List отображает прокручиваемые строки (как UITableView). Элементы должны соответствовать Identifiable (или вы предоставляете id:). NavigationStack (iOS 16+) управляет навигацией; NavigationLink помещает назначения. LazyVStack/LazyHStack загружают содержимое лениво для производительности. ForEach для повторяющихся представлений внутри других контейнеров. Используйте List для табличных данных, ScrollView+LazyVStack для пользовательских макетов.

swift
struct Item: Identifiable {
    let id = UUID()
    let name: String
}

struct ListView: View {
    let items = [Item(name: "Apple"), Item(name: "Banana")]

    var body: some View {
        NavigationStack {
            List(items) { item in
                NavigationLink(item.name) {
                    DetailView(item: item)
                }
            }
            .navigationTitle("Fruits")
        }
    }
}

struct DetailView: View {
    let item: Item
    var body: some View {
        Text("Detail: \(item.name)")
            .navigationTitle(item.name)
    }
}

// ForEach for custom layouts
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            Text(item.name)
        }
    }
}

Формы и листы

Form автоматически стилизует элементы управления для экранов настроек (сгруппированный, платформенно-подходящий). Распространённые элементы: TextField, Toggle, Slider, Picker, Stepper. .sheet представляет модальное окно; .fullScreenCover для полноэкранного. Привязки ($) соединяют элементы управления с состоянием. Form адаптируется к платформе (сгруппированный список на iOS, макет формы на macOS). Используйте Form для ввода данных и настроек; VStack для пользовательских макетов.

swift
struct SettingsView: View {
    @State private var name = ""
    @State private var notifications = true
    @State private var volume: Double = 0.5
    @State private var theme = "Light"
    @State private var showSheet = false

    var body: some View {
        Form {
            Section("Profile") {
                TextField("Name", text: $name)
                Toggle("Notifications", isOn: $notifications)
            }
            Section("Audio") {
                Slider(value: $volume, in: 0...1)
            }
            Section("Appearance") {
                Picker("Theme", selection: $theme) {
                    Text("Light").tag("Light")
                    Text("Dark").tag("Dark")
                }
            }
            Button("Show Modal") { showSheet = true }
        }
        .sheet(isPresented: $showSheet) {
            Text("Modal content")
        }
    }
}
15

Замыкания: глубокое погружение

Вариации синтаксиса замыканий

Замыкания имеют много форм синтаксиса. Полная форма: { (params) -> Type in body }. Вывод типа позволяет опускать типы. Сокращённые аргументы ($0, $1) заменяют именованные параметры. Синтаксис завершающего замыкания перемещает замыкание за () когда это последний аргумент. Множественные завершающие замыкания (Swift 5.3+) именуют дополнительные замыкания. Используйте наиболее лаконичную форму, которая остаётся читаемой — сокращённые аргументы хороши для коротких замыканий вроде map.

swift
// Full closure
let add: (Int, Int) -> Int = { (a: Int, b: Int) -> Int in
    return a + b
}

// Type inference
let add2: (Int, Int) -> Int = { a, b in a + b }

// Shorthand argument names ($0, $1...)
let add3: (Int, Int) -> Int = { $0 + $1 }

// Trailing closure syntax
let result = [1, 2, 3].map { $0 * 2 }  // [2, 4, 6]

// Multiple trailing closures (Swift 5.3+)
Button {
    print("action")  // action
} label: {
    Text("Click")    // label
}

// No return for Void closures
let log: (String) -> Void = { msg in print(msg) }

Захват значений

Замыкания захватывают переменные из охватывающей области. По умолчанию захватывают по ссылке (изменения захваченной переменной видны). Списки захвата [foo] захватывают по значению (снимок при создании замыкания). Для ссылочных типов (классов) [weak self] или [unowned self] разрывают циклы удержания. Типы-значения (структуры), захваченные по ссылке, всё равно видят обновления, потому что замыкание удерживает коробку. Списки захвата идут перед списком параметров.

swift
// Closures capture surrounding state
func makeIncrementer(amount: Int) -> () -> Int {
    var total = 0
    return {
        total += amount  // captures total and amount
        return total
    }
}

let inc = makeIncrementer(amount: 5)
inc()  // 5
inc()  // 10
// total persists between calls (captured by reference)

// Capture list controls how
class Foo { var x = 0 }
let foo = Foo()
let closure = { [foo] in  // captures foo by value (snapshot)
    print(foo.x)
}
foo.x = 100
closure()  // prints 0 (captured the old value)

// Without capture list, foo is captured by reference
let refClosure = { print(foo.x) }
foo.x = 200
refClosure()  // prints 200

Escaping и Autoclosure

@autoclosure оборачивает выражение в замыкание без аргументов автоматически, поэтому вызывающий пишет выражение напрямую (без фигурных скобок). Это обеспечивает ленивое вычисление — assert() использует это для пропуска условия в релизных сборках. Сочетая с @escaping, можно отложить вычисление. Операторы && и || используют @autoclosure для короткого замыкания. Используйте @autoclosure умеренно — он скрывает, что код отложен, что может запутать читателей.

swift
// @autoclosure: wraps an expression in a closure automatically
func log(_ condition: @autoclosure () -> Bool, _ message: String) {
    if condition() {  // evaluated here, not at call
        print(message)
    }
}
log(2 > 1, "two is greater")  // no { } needed

// assert uses @autoclosure to avoid evaluating in release
assert(isValid, "Invalid state")  // isValid not checked in release

// @autoclosure + @escaping
func deferred(_ block: @autoclosure @escaping () -> Int) {
    DispatchQueue.main.async { print(block()) }
}
deferred(expensiveComputation())  // runs later, on main

// Short-circuit evaluation with @autoclosure
func &&(lhs: @autoclosure () -> Bool, rhs: @autoclosure () -> Bool) -> Bool {
    return lhs() ? rhs() : false  // rhs not evaluated if lhs is false
}

Функции высшего порядка

Функции высшего порядка Swift (map, filter, reduce, flatMap, compactMap) обеспечивают функциональное программирование над коллекциями. map преобразует, filter выбирает, reduce агрегирует, flatMap выравнивает, compactMap удаляет nil. Они возвращают новые коллекции (неизменяемость). Цепочка компонует операции. Для больших наборов данных используйте .lazy для избежания промежуточных массивов. Это основа функционального Swift — предпочитайте их императивным циклам for при преобразовании данных.

swift
let nums = [1, 2, 3, 4, 5, 6]

// map: transform each element
let doubled = nums.map { $0 * 2 }  // [2, 4, 6, 8, 10, 12]

// filter: keep elements matching predicate
let evens = nums.filter { $0 % 2 == 0 }  // [2, 4, 6]

// reduce: combine all into one
let sum = nums.reduce(0, +)  // 21
let product = nums.reduce(1, *)  // 720

// flatMap: transform + flatten
let nested = [[1, 2], [3, 4]]
let flat = nested.flatMap { $0 }  // [1, 2, 3, 4]

// compactMap: transform + filter nils
let strings = ["1", "abc", "3"]
let ints = strings.compactMap { Int($0) }  // [1, 3]

// Chaining (lazy evaluation)
let result = nums
    .filter { $0 % 2 == 0 }
    .map { $0 * $0 }
    .reduce(0, +)  // 4 + 16 + 36 = 56

Замыкания как обработчики завершения

Обработчики завершения были стандартным асинхронным паттерном до async/await (Swift 5.5). Они @escaping (выполняются позже) и часто используют Result для обработки ошибок. Недостаток — вложенные обратные вызовы ('пирамида гибели'). async/await делает это линейным и читаемым. Новый код должен использовать async/await; обработчики завершения остаются для делегирующих API и совместимости с Objective-C. Можно обернуть обработчики завершения withCheckedContinuation для использования с async/await.

swift
// Completion handler pattern (pre-async/await)
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        guard let data = data,
              let user = try? JSONDecoder().decode(User.self, from: data) else {
            completion(.failure(DecodeError()))
            return
        }
        completion(.success(user))
    }.resume()
}

// Usage (pyramid of doom)
fetchUser(id: 42) { result in
    switch result {
    case .success(let user):
        fetchAvatar(user) { avatarResult in
            // nested...
        }
    case .failure(let error):
        print(error)
    }
}

// Modern: async/await replaces this
func fetchUserAsync(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}
16

Обёртки свойств

Определение обёртки свойства

Обёртки свойств (@propertyWrapper) инкапсулируют переиспользуемое поведение свойств. Свойство wrappedValue — это то, с чем взаимодействуют пользователи. init получает начальное значение и любые пользовательские аргументы. SwiftUI широко использует обёртки свойств: @State, @Binding, @Published, @AppStorage. Определяйте свои для проверки, кэширования, логирования или значений по умолчанию. Обёртка — структура/класс, управляющий хранилищем.

swift
@propertyWrapper
struct Clamped<Value: Comparable> {
    var value: Value
    let range: ClosedRange<Value>

    var wrappedValue: Value {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }

    init(wrappedValue: Value, _ range: ClosedRange<Value>) {
        self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
        self.range = range
    }
}

// Usage
struct Slider {
    @Clamped(0...100) var progress: Int = 50
}
var s = Slider()
s.progress = 150  // clamped to 100
s.progress = -10  // clamped to 0
print(s.progress)  // 0

Спроецированное значение

projectedValue (доступ через $) — вторичный интерфейс к свойству. SwiftUI использует это широко: $ @State даёт Binding для дочерних представлений, $ @Published даёт Publisher. Определяйте projectedValue, когда обёртка должна раскрыть что-то сверх обёрнутого значения (привязка, publisher, сама обёртка). Синтаксис префикса $ делает это эргономичным. Не всем обёрткам нужен projectedValue — это опционально.

swift
@propertyWrapper
struct Observable<T> {
    var value: T
    var wrappedValue: T {
        get { value }
        set { value = newValue; onChange() }
    }
    var onChange: () -> Void = {}

    // projectedValue: accessed via $ (like @State's binding)
    var projectedValue: Observable<T> { self }
}

struct View {
    @Observable var count = 0
    // count: the wrapped value (Int)
    // $count: the projected value (Observable<Int>)
}

// SwiftUI's @State projects a Binding
struct Counter: View {
    @State var value = 0
    var body: some View {
        Stepper("Value: \(value)", value: $value)  // Binding<Int>
    }
}

Встроенные обёртки свойств

SwiftUI предоставляет множество обёрток свойств: @State (локальное состояние), @Binding (состояние родителя), @ObservedObject/@StateObject (внешняя модель), @EnvironmentObject (внедрённая модель), @AppStorage (UserDefaults), @SceneStorage (восстановление сцены), @FocusState (фокус клавиатуры), @ScaledMetric (Dynamic Type), @Namespace (анимации). Каждая управляет хранилищем и жизненным циклом. @Published из Combine запускает обновления UI. Знание, когда что использовать — ключ к SwiftUI.

swift
import SwiftUI
import Combine

struct ContentView: View {
    @State var count = 0  // local view state

    @Binding var externalValue: Int  // parent-owned

    @EnvironmentObject var model: AppModel  // injected

    @AppStorage("username") var username = ""  // UserDefaults

    @SceneStorage("draft") var draft = ""  // per-scene storage

    @FocusState var isFocused: Bool  // focus management

    @ScaledMetric var size = 16  // Dynamic Type scaling

    @Namespace var animationNamespace  // matched geometry

    // Combine
    @Published var data = [Item]()  // in ObservableObject
}

// @AppStorage auto-syncs with UserDefaults
// @SceneStorage restores state per scene (iPad multitasking)

Пользовательские обёртки для проверки

Обёртки свойств хороши для сквозных задач. NonEmpty проверяет при установке. Cached вычисляет лениво и мемоизирует. Другие идеи: Logged (логирует изменения), Trimmed (обрезает пробелы), Formatted (форматирует при get), Clamped (ограничения диапазона), UserDefaults-backed. Обёртки уменьшают шаблонность — логика пишется один раз и применяется через @. Они компонуются: @Logged @Clamped(0...100) var value. Держите обёртки сфокусированными на одной задаче.

swift
@propertyWrapper
struct NonEmpty {
    private var value: String = ""
    var wrappedValue: String {
        get { value }
        set {
            guard !newValue.isEmpty else {
                print("Warning: cannot set to empty")
                return
            }
            value = newValue
        }
    }
    init(wrappedValue: String) {
        self.wrappedValue = wrappedValue
    }
}

@propertyWrapper
struct Cached<T> {
    private var cached: T?
    private var generator: () -> T
    var wrappedValue: T {
        mutating get {
            if cached == nil { cached = generator() }
            return cached!
        }
    }
    init(wrappedValue: @autoclosure @escaping () -> T) {
        self.generator = wrappedValue
    }
}

struct Config {
    @NonEmpty var name: String = "default"
    @Cached var expensiveValue: Int = computeExpensive()
}

Обёртки свойств в протоколах и обобщённых типах

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

swift
// Apply to protocol requirements
protocol Configurable {
    @NonEmpty var name: String { get set }
}

// Generic property wrapper
@propertyWrapper
struct Validated<T> {
    private var value: T?
    let validator: (T) -> Bool
    var wrappedValue: T? {
        get { value }
        set {
            if let v = newValue, validator(v) { value = v }
        }
    }
    init(wrappedValue: T?, validator: @escaping (T) -> Bool) {
        self.validator = validator
        self.wrappedValue = wrappedValue
    }
}

struct Form {
    @Validated(validator: { $0.count >= 8 }) var password: String? = nil
}

// Limitation: property wrappers in structs can't be
// initialized from another instance easily (copy issues).
// They work best as stored properties, not computed.
17

Фреймворк Combine

Издатели и подписчики

Combine — реактивный фреймворк Apple (как RxSwift). Издатели испускают значения; подписчики их получают. sink создаёт подписчика с замыканиями. assign привязывает вывод к свойству. Операторы (map, filter, reduce) преобразуют издателей декларативно. Combine декларативен — описываете конвейер, значения протекают через него. Используется для асинхронных операций, привязок UI и обработки событий. Современный Swift предпочитает async/await, но Combine остаётся для сложных конвейеров.

swift
import Combine

// Publisher: emits values over time
let publisher = [1, 2, 3].publisher

// Subscriber: receives values
publisher.sink { value in
    print(value)  // 1, 2, 3
}

// With completion
publisher.sink(receiveCompletion: { completion in
    print("done: \(completion)")
}, receiveValue: { value in
    print(value)
})

// Assign to a property
class ViewModel {
    var data: String = ""
}
let vm = ViewModel()
Just("hello").assign(to: &vm.data)

// Operators transform publishers
[1, 2, 3, 4].publisher
    .map { $0 * 2 }
    .filter { $0 > 4 }
    .sink { print($0) }  // 6, 8

@Published и ObservableObject

@Published оборачивает свойство и испускает новое значение подписчикам при каждом изменении. Префикс $ предоставляет доступ к publisher. Это соединяет Combine со SwiftUI — @Published в ObservableObject запускает обновления представления. Подписки отменяются при освобождении AnyCancellable, поэтому храните их (например, в Set). Используйте Combine для дебаунса, троттлинга, объединения нескольких асинхронных источников — то, что async/await обрабатывает менее элегантно.

swift
class UserStore: ObservableObject {
    @Published var name: String = ""  // publishes on change
    @Published var age: Int = 0

    // $name is a Publisher<String, Never>
    func demo() {
        $name.sink { print("name changed to \($0)") }
        name = "Alice"  // prints "name changed to Alice"
    }
}

// Combine with SwiftUI
struct ProfileView: View {
    @ObservedObject var store: UserStore
    var body: some View {
        Text(store.name)  // updates when name changes
    }
}

// Cancellable: store subscriptions to keep them alive
class Service {
    var cancellables = Set<AnyCancellable>()
    let store = UserStore()

    init {
        store.$name
            .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
            .sink { name in saveToServer(name) }
            .store(in: &cancellables)  // keep alive
    }
}

Операторы

Операторы Combine преобразуют, фильтруют, комбинируют и управляют временем издателей. map/scan преобразуют; filter/селекторы выбирают; merge/zip/combineLatest комбинируют несколько потоков; debounce/throttle управляют временем. Операторы возвращают новых издателей (неизменяемость, цепочки). debounce ждёт тихого периода (поиск-по-мере-ввода); throttle ограничивает частоту (нажатия кнопок). collect собирает все значения в массив. Это обеспечивает декларативные реактивные конвейеры.

swift
let publisher = [1, 2, 3, 4, 5].publisher

// Transforming
publisher.map { $0 * 2 }  // [2, 4, 6, 8, 10]
publisher.scan(0, +)  // [1, 3, 6, 10, 15] (running sum)

// Filtering
publisher.filter { $0 % 2 == 0 }  // [2, 4]
publisher.removeDuplicates()  // consecutive dupes
publisher.first()  // just 1

// Combining
let p1 = [1, 2].publisher
let p2 = [3, 4].publisher
p1.merge(with: p2)  // interleaved: 1, 2, 3, 4 (or any order)
p1.zip(p2)  // pairs: (1,3), (2,4)
p1.combineLatest(p2)  // latest of each

// Timing
publisher.delay(for: 1, scheduler: RunLoop.main)
publisher.throttle(for: 1, scheduler: .main, latest: true)
publisher.debounce(for: 0.5, scheduler: .main)
publisher.collect()  // gather all into [1,2,3,4,5]

Субъекты (ручная публикация)

Субъекты — изменяемые издатели, которым можно отправлять значения вручную — мост между императивным и реактивным кодом. PassthroughSubject вещает без хранения (поток событий). CurrentValueSubject хранит последнее значение (состояние). Новые подписчики CurrentValueSubject немедленно получают текущее значение. Используйте Субъекты для обёртывания делегатов, уведомлений или событий UI в конвейеры Combine. @Published по сути CurrentValueSubject, интегрированный с ObservableObject.

swift
import Combine

// PassthroughSubject: broadcasts to subscribers, no current value
let subject = PassthroughSubject<Int, Never>()
subject.sink { print($0) }  // subscriber 1
subject.send(1)  // prints 1
subject.send(2)  // prints 2

// CurrentValueSubject: holds current value, new subs get it
let current = CurrentValueSubject<Int, Never>(0)
current.sink { print($0) }  // prints 0 (current)
current.send(1)  // prints 1
current.sink { print($0) }  // prints 1 (current)
current.value  // 1

// Use cases:
// - PassthroughSubject: events (button taps, notifications)
// - CurrentValueSubject: state (like @Published but more control)
// - Bridge imperative code to Combine

// Finish a subject
subject.send(completion: .finished)

Обработка ошибок

Издатели Combine имеют тип Failure (Never для не-падающих). catch заменяет ошибку резервным издателем. retry переподписывается при неудаче (хорошо для нестабильных сетей). mapError преобразует тип ошибки. assertNoFailure вызывает сбой при ошибке (используйте когда уверены). Издатели с Failure == Never могут использоваться где угодно; с ошибками требуют обработки. Это делает обработку ошибок явной и компонуемой в реактивных конвейерах.

swift
enum APIError: Error { case network, parse }

// Publishers can fail with an error
let publisher = Fail<Int, APIError>(error: .network)

// catch: replace error with fallback publisher
publisher
    .catch { _ in Just(0) }  // fallback to 0
    .sink { print($0) }  // 0

// retry: retry on failure
URLSession.shared.dataTaskPublisher(for: url)
    .retry(3)  // retry up to 3 times
    .sink(receiveCompletion: { _ in }, receiveValue: { _ in })

// mapError: transform error type
publisher
    .mapError { _ in NSError(domain: "x", code: 1) }

// assertNoFailure: crash if error (when you're sure it won't)
Just(1).assertNoFailure().sink { print($0) }

// Result publisher
let resultPublisher = [1, 2, 3].publisher.setFailureType(to: Error.self)
18

Управление доступом и организация кода

Уровни доступа

Swift имеет пять уровней доступа. private (наименьшая область, в пределах объявления), fileprivate (в пределах файла), internal (в пределах модуля, по умолчанию), public (любой, кто импортирует), open (public + наследуемый/переопределяемый, только классы). Swift 5.9 добавляет package для модулей SPM. Начинайте строго и расширяйте по необходимости. Используйте public для API библиотек, internal для кода приложения, private для деталей реализации. open для классов фреймворков, предназначенных для наследования.

swift
// public: accessible from any module that imports this
public class PublicAPI {
    public func connect() {}
}

// internal (default): accessible within the module
class InternalClass {
    func helper() {}  // internal by default
}

// fileprivate: accessible within the source file
fileprivate func fileHelper() {}

// private: accessible within the enclosing declaration
class Counter {
    private var count = 0  // only Counter can access
    fileprivate func reset() { count = 0 }  // same file
}

// Swift 5.9: package (for Swift Package Manager)
package struct PackageType {
    package var value: Int
}

// Guidelines:
// - Start restrictive (private), open up as needed
// - public for framework API, internal for implementation
// - private for encapsulation within a type

Расширения и организация

Расширения позволяют разделить функциональность типа по файлам (String+Validation.swift, String+Parsing.swift) и добавлять соответствие ретроактивно. Это сохраняет файлы сфокусированными и управляемыми. Соответствие протоколу может быть в отдельном расширении/файле от основного определения типа. Условное соответствие (where Element: ...) добавляет соответствие условно. Используйте расширения для организации: сохраняйте основной тип минимальным, добавляйте функциональность в сфокусированных файлах расширений.

swift
// Split a type across files with extensions
// String+Validation.swift
extension String {
    var isValidEmail: Bool {
        contains("@") && contains(".")
    }
}

// String+Parsing.swift
extension String {
    func toURL() -> URL? { URL(string: self) }
}

// Conform to protocols in extensions
extension String: Comparable {  // already conforms, just showing
    // protocol methods
}

// Conditional conformance
extension Array: MyProtocol where Element: MyProtocol {}

// Protocol conformance in a separate file
// (keeps the main type definition clean)
struct User { let id: Int; let name: String }

extension User: Codable {}  // synthesized
extension User: Equatable {}  // synthesized
extension User: CustomStringConvertible {
    var description: String { "User(\(id), \(name))" }
}

Модули и пакеты

Swift Package Manager (SPM) определяет модули через Package.swift. Каждый target — модуль, internal доступ в пределах target, public пересекает модули. Зависимости — другие пакеты (git URL). Продукты — то, что ваш пакет экспортирует. SPM — современный способ управления кодом Swift (заменяет CocoaPods/Carthage). Xcode интегрирует SPM нативно. Структура: Sources/MyLibrary/ для кода, Tests/MyLibraryTests/ для тестов. Используйте модули для разделения задач и управления доступом.

swift
// Package.swift (Swift Package Manager)
// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyLibrary",
    products: [
        .library(name: "MyLibrary", targets: ["MyLibrary"]),
    ],
    dependencies: [
        .package(url: "https://github.com/.../Logging", from: "1.0.0"),
    ],
    targets: [
        .target(name: "MyLibrary", dependencies: ["Logging"]),
        .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]),
    ]
)

// Importing modules
import Foundation
import MyLibrary  // your module
import Logging    // dependency

// Each target is a module
// internal access is within a target/module
// public access crosses module boundaries

Инициализаторы: designated и convenience

Классы имеют designated инициализаторы (полностью инициализируют, должны вызывать designated суперкласса) и convenience инициализаторы (делегируют другому init в том же классе). Эта двухуровневая система обеспечивает полную инициализацию. Структуры получают memberwise initializer бесплатно. Failable инициализаторы (init?) возвращают nil при неудаче. Required инициализаторы (required init) должны быть реализованы подклассами. Для большинства кода предпочитайте структуры (более простая инициализация) классам.

swift
class Person {
    let name: String
    let age: Int

    // Designated initializer: fully initializes
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

class Employee: Person {
    let employeeId: String

    // Designated init must call super's designated init
    init(name: String, age: Int, employeeId: String) {
        self.employeeId = employeeId
        super.init(name: name, age: age)  // must call super
    }

    // Convenience init must call another init (same class)
    convenience init(name: String) {
        self.init(name: name, age: 0, employeeId: "TEMP")
    }
}

// Structs: memberwise initializer (free)
struct Point { let x, y: Int }
let p = Point(x: 1, y: 2)  // synthesized

// Failable initializers
struct Config {
    let port: Int
    init?(port: Int) {
        guard port > 0 else { return nil }
        self.port = port
    }
}

Опционалы: глубокое погружение

Опционалы — механизм null-безопасности Swift, перечисление, которое либо .some(value), либо .none (nil). Нужно развернуть для доступа к значению. Опциональная цепочка (?.) безопасно навигирует; nil-coalescing (??) предоставляет значение по умолчанию; if let/guard let безопасно разворачивают. Избегайте ! (принудительное разворачивание) если не уверены — вызывает сбой при nil. Опционалы заставляют явно обрабатывать отсутствие, устраняя исключения нулевых ссылок, распространённые в других языках.

swift
// Optional: enum with .some(Value) and .none
let maybeInt: Int? = 42
let nilInt: Int? = nil

// Optional chaining (short-circuits on nil)
let name: String? = user?.profile?.name  // nil if any link is nil

// Nil-coalescing (default value)
let value = maybeInt ?? 0  // 42
let value2 = nilInt ?? 0   // 0

// Forced unwrapping (crashes if nil)
let forced = maybeInt!  // 42
// let crash = nilInt!   // RUNTIME CRASH

// Optional binding
if let actual = maybeInt {
    print(actual)  // 42, actual is Int (not optional)
}

// Guard for early exit
func process(_ data: Data?) {
    guard let data = data else { return }
    // data is non-optional here
}

// Optional try
let result = try? parse(json)  // nil on error
let result2 = try! parse(json) // crashes on error

// Multiple binding
if let a = optA, let b = optB, a < b { /* ... */ }
19

Управление памятью (ARC/weak)

Основы ARC

Automatic Reference Counting (ARC) отслеживает сильные ссылки. Когда счётчик достигает нуля, объект освобождается. ARC детерминирован (в отличие от сборки мусора): deinit выполняется немедленно при освобождении последней ссылки. Большую часть времени ARC просто работает. Проблемы возникают с циклами ссылок: два объекта сильно ссылаются друг на друга, предотвращая освобождение. Используйте weak или unowned для разрыва циклов.

swift
class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var reference1: Person?
var reference2: Person?

reference1 = Person(name: "Alice")
// Prints: Alice is being initialized

reference2 = reference1  // Strong reference count: 2

reference1 = nil  // Count: 1
reference2 = nil  // Count: 0, deinit runs
// Prints: Alice is being deinitialized

Слабые ссылки

weak ссылки не удерживают объект живым. При освобождении объекта weak ссылки автоматически становятся nil. weak должен быть опциональным (var tenant: Person?). Используйте weak, когда у объекта более короткое время жизни (арендатор может покинуть квартиру). Классический пример — паттерн делегирования: делегат weak для избежания циклов. weak имеет небольшие накладные расходы (регистрируется в среде выполнения для установки nil).

swift
class Person {
    var name: String
    var apartment: Apartment?  // Strong (person owns apartment)
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class Apartment {
    var unit: String
    weak var tenant: Person?  // Weak (breaks cycle)
    init(unit: String) { self.unit = unit }
    deinit { print("Apartment \(unit) deinit") }
}

var alice: Person? = Person(name: "Alice")
var apt: Apartment? = Apartment(unit: "4A")

alice!.apartment = apt
apt!.tenant = alice  // Weak reference, no cycle

alice = nil  // Alice deinit (apartment's tenant becomes nil)
apt = nil    // Apartment deinit

// weak must be optional (becomes nil when deallocated)

Unowned ссылки

unowned ссылки, как и weak, не удерживают объект живым. В отличие от weak, unowned не опциональный и не становится nil. Доступ к unowned ссылке после освобождения вызывает сбой. Используйте unowned, когда можете гарантировать, что объект переживает ссылку (например, кредитная карта не может существовать без клиента). unowned имеет меньше накладных расходов, чем weak. Выбор: weak для неопределённых времён жизни, unowned для гарантированных.

swift
class Customer {
    let name: String
    var card: CreditCard?  // Strong
    init(name: String) { self.name = name }
    deinit { print("\(name) deinit") }
}

class CreditCard {
    let number: UInt
    unowned let customer: Customer  // Non-optional, no cycle
    init(number: UInt, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("Card \(number) deinit") }
}

var john: Customer? = Customer(name: "John")
john!.card = CreditCard(number: 1234, customer: john!)

john = nil
// Customer deinit, then CreditCard deinit

// unowned: non-optional, assumed to always have a value
// CRASH if accessed after deallocation
// Use when the referenced object has same or longer lifetime

Замыкания и списки захвата

Замыкания захватывают ссылки сильно по умолчанию, создавая циклы при сохранении замыкания на self. Списки захвата ([weak self] или [unowned self]) разрывают цикл. weak self требует опциональной обработки (guard let self = self). Захватывайте конкретные значения (let name = self.name), чтобы избежать захвата self целиком. Замыкания, переданные функциям (не сохраняемые), не создают циклов. Всегда используйте списки захвата для сохраняемых замыканий, ссылающихся на self.

swift
class ViewController {
    var name = "MyVC"
    var onComplete: (() -> Void)?

    // BAD: Strong reference cycle
    func setupBad() {
        onComplete = {
            print("\(self.name) completed")  // Captures self strongly
        }
    }

    // GOOD: Capture list breaks cycle
    func setupGood() {
        onComplete = { [weak self] in
            guard let self = self else { return }
            print("\(self.name) completed")
        }
    }

    // unowned for guaranteed lifetime
    func setupUnowned() {
        onComplete = { [unowned self] in
            print("\(self.name) completed")
        }
    }

    // Capture specific values
    func setupCapture() {
        let name = self.name  // Capture value, not self
        onComplete = {
            print("\(name) completed")
        }
    }

    deinit { print("VC deinit") }
}

Обнаружение и исправление утечек

Распространённые источники утечек: делегаты, сохранённые сильно (используйте weak), Timer удерживающий target (invalidate в deinit), наблюдатели NotificationCenter (удалите в deinit). Xcode Memory Graph Debugger визуализирует графы объектов и подсвечивает утечки. Запустите с MallocStackLogging для получения трассировок выделения. Проверяйте deinit, устанавливая ссылки в nil и проверяя, что deinit выполняется. Instruments (инструмент Leaks) обнаруживает утечки во время выполнения. Всегда парируйте setup с teardown.

swift
// Xcode memory graph debugger: visualize reference cycles
// Product > Debug > View Memory Graph Hierarchy

// Common leak patterns:
// 1. Delegate stored as strong
class View {
    weak var delegate: ViewDelegate?  // MUST be weak
}

// 2. Timer retains target
class TimerHolder {
    var timer: Timer?
    func start() {
        // BAD: timer retains self via #selector
        timer = Timer.scheduledTimer(timeInterval: 1,
            target: self, selector: #selector(fire),
            userInfo: nil, repeats: true)
    }
    @objc func fire() {}
    deinit { timer?.invalidate() }  // MUST invalidate
}

// 3. NotificationCenter observer
class Observer {
    init() {
        NotificationCenter.default.addObserver(
            self, selector: #selector(handle),
            name: .someNotification, object: nil)
    }
    @objc func handle() {}
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
}

// Use Xcode's Debug Memory Graph to find leaks
20

Актёры и async/await

Основы async/await

async/await (Swift 5.5+) делает асинхронный код выглядящим синхронным. async помечает функции, которые могут приостанавливаться; await помечает точки приостановки. Task мостит синхронный к асинхронному контексту. async let запускает параллельную задачу; await собирает результат. Используйте async let для параллельного выполнения, обычный await для последовательного. Ошибки распространяются через throws/try. Компилятор требует await в точках приостановки. async/await заменяет обработчики завершения и Combine для многих случаев.

swift
// Async function
func fetchUser(id: Int) async throws -> User {
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Calling async functions
func loadProfile() async {
    do {
        let user = try await fetchUser(id: 1)
        print("Loaded: \(user.name)")
    } catch {
        print("Error: \(error)")
    }
}

// Task: bridge async to sync context
Task {
    await loadProfile()
}

// Concurrent execution
func loadMultiple() async {
    async let user1 = fetchUser(id: 1)
    async let user2 = fetchUser(id: 2)
    async let user3 = fetchUser(id: 3)

    let users = try await [user1, user2, user3]
    print("Loaded \(users.count) users")
}

// Sequenced vs concurrent
let sequential = try await fetchUser(id: 1)  // Waits
let concurrent = try await fetchUser(id: 2)  // Then waits again

Актёры

Актёры (Swift 5.5+) — ссылочные типы с сериализованным доступом, предотвращающие гонки данных. Только один метод выполняется на актёре за раз — ручная блокировка не нужна. Методы актёра должны вызываться с await (могут приостанавливаться). Свойства изолированы и не могут быть доступны напрямую извне. Актёры — безопасный выбор по умолчанию для разделяемого изменяемого состояния в параллельном коде. Используйте актёры вместо классов с блокировками для большинства параллельного состояния.

swift
// Actor: reference type with serialized access
actor BankAccount {
    private var balance: Decimal

    init(balance: Decimal) {
        self.balance = balance
    }

    func deposit(_ amount: Decimal) {
        balance += amount
    }

    func withdraw(_ amount: Decimal) -> Bool {
        guard balance >= amount else { return false }
        balance -= amount
        return true
    }

    func getBalance() -> Decimal { balance }
}

// Usage (must be async)
let account = BankAccount(balance: 100)
Task {
    await account.deposit(50)
    let balance = await account.getBalance()
    print("Balance: \(balance)")  // 150
}

// Actors prevent data races automatically
// Only one method runs at a time per actor instance
// No need for locks or queues

Изоляция актёров

Изоляция актёров защищает состояние. nonisolated члены могут быть доступны без await (для чистых значений или методов, не затрагивающих изолированное состояние). @MainActor — глобальный актёр для изоляции основного потока (обновления UI). @globalActor создаёт пользовательские глобальные актёры. Пересечение границ актёров требует await. Компилятор проверяет изоляцию во время компиляции, предотвращая гонки данных. Используйте @MainActor для ViewModel и кода UI; пользовательские актёры для доменно-специфической изоляции.

swift
actor Counter {
    private var count = 0

    // Isolated method (default)
    func increment() { count += 1 }

    // Nonisolated: can be called without await
    nonisolated var description: String { "Counter" }

    // nonisolated function
    nonisolated func id() -> String { "counter-\(ObjectIdentifier(self))" }
}

// Global actor: @MainActor runs on main thread
@MainActor
class ViewModel: ObservableObject {
    @Published var data: [Item] = []

    func loadData() async {
        // Always runs on main thread
        data = try await fetchItems()
    }
}

// Mark specific functions
class Service {
    @MainActor
    func updateUI(_ result: Result) {
        // UI updates on main thread
    }
}

// globalActor lets you apply to types, functions, properties
@globalActor
actor MyActor {
    static let shared = MyActor()
}

Структурированный параллелизм

Группы задач обеспечивают динамические параллельные вычисления. addTask порождает дочерние задачи; for await group собирает результаты по мере завершения. Группы задач обеспечивают структурированный параллелизм: все дочерние завершаются до продолжения родителя. Отмена распространяется от родителя к дочерним автоматически. Проверяйте Task.isCancelled в длительных задачах. withThrowingTaskGroup распространяет ошибки, отменяя сиблингов при throw. Используйте группы задач для паттернов fan-out/fan-in.

swift
// TaskGroup: dynamic parallel computation
func fetchAllUsers(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask { try await fetchUser(id: id) }
        }

        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

// Task tree: parent-child relationship
func process() async {
    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask {
                await processItem(i)
            }
        }
        // All tasks complete before group returns
    }
    print("All done")
}

// Cancellation
func longRunning() async {
    await withTaskGroup(of: Void.self) { group in
        group.addTask {
            for i in 1...100 {
                if Task.isCancelled { return }
                await doWork(i)
            }
        }
    }
}

// Cancel from outside
let task = Task { await longRunning() }
task.cancel()

AsyncSequence и потоки

AsyncSequence — асинхронная версия Sequence, обеспечивающая циклы for await. AsyncStream мостит callback-based или delegate-based API к async/await. yield испускает значения, finish завершает. onTermination очищает ресурсы. AsyncThrowingStream поддерживает ошибки. Используйте AsyncSequence для пагинированных API, данных в реальном времени или любого потока значений во времени. Стандартная библиотека предоставляет .lines на URL для построчного чтения файлов. AsyncSequence интегрируется с отменой задач.

swift
// AsyncSequence: iterate asynchronously
struct Counter: AsyncSequence {
    struct AsyncIterator: AsyncIteratorProtocol {
        var current: Int
        let max: Int
        mutating func next() async -> Int? {
            guard current <= max else { return nil }
            defer { current += 1 }
            try? await Task.sleep(nanoseconds: 100_000_000)
            return current
        }
    }

    let start: Int
    let max: Int

    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(current: start, max: max)
    }
}

// Usage
for await num in Counter(start: 1, max: 5) {
    print(num)  // 1 2 3 4 5 (with delays)
}

// AsyncStream: bridge callbacks to async
func urlLines(_ url: URL) -> AsyncThrowingStream<String, Error> {
    AsyncThrowingStream { continuation in
        let task = Task {
            do {
                for try await line in url.lines {
                    continuation.yield(line)
                }
                continuation.finish()
            } catch {
                continuation.finish(throwing: error)
            }
        }
        continuation.onTermination = { _ in task.cancel() }
    }
}
21

Тестирование

Основы XCTest

XCTest — стандартный фреймворк тестирования. XCTestCase группирует связанные тесты. setUp/tearDown выполняются до/после каждого теста. Утверждения: XCTAssertEqual, XCTAssertNil, XCTAssertThrowsError, XCTAssertTrue. @testable import получает доступ к внутренним символам. Тесты по умолчанию выполняются в основном потоке. Используйте XCTAssertNotNil для опционалов. Запускайте тесты через Cmd+U. Xcode показывает результаты тестов встроенно. Пишите тесты рядом с кодом для быстрой обратной связи.

swift
import XCTest
@testable import MyApp

class UserTests: XCTestCase {
    var user: User!

    override func setUp() {
        super.setUp()
        user = User(name: "Alice", age: 30)
    }

    override func tearDown() {
        user = nil
        super.tearDown()
    }

    func testName() {
        XCTAssertEqual(user.name, "Alice")
    }

    func testAgeAfterBirthday() {
        user.haveBirthday()
        XCTAssertEqual(user.age, 31)
    }

    func testValidation() {
        XCTAssertThrowsError(try User(name: "", age: -1)) { error in
            guard let error = error as? UserError else {
                XCTFail("Wrong error type")
                return
            }
            XCTAssertEqual(error, .invalidName)
        }
    }

    func testOptional() {
        XCTAssertNotNil(user.email)
        XCTAssertNil(user.address)
    }
}

// Run: Cmd+U in Xcode

Асинхронное тестирование

Swift 5.5+ поддерживает асинхронные тестовые методы напрямую (async throws func test...). Используйте try await внутри тестов. Для старого кода используйте XCTestExpectation: создайте expectation, fulfill() в обратном вызове, wait(for:timeout:). Устанавливайте разумные таймауты для обнаружения зависаний. Для тестирования асинхронных последовательностей используйте циклы for await. Имитируйте сетевые вызовы через URLProtocol или внедрение зависимостей. Тестируйте как успешные, так и неудачные пути. Асинхронные тесты по умолчанию выполняются параллельно — используйте трейт .serialized если важен порядок.

swift
class APITests: XCTestCase {
    func testFetchUser() async throws {
        let user = try await fetchUser(id: 1)
        XCTAssertEqual(user.name, "Alice")
    }

    func testFetchFailure() async {
        do {
            _ = try await fetchUser(id: -1)
            XCTFail("Should have thrown")
        } catch {
            // Expected
        }
    }

    // Old style with expectations (still works)
    func testAsyncWithExpectation() {
        let expectation = XCTestExpectation(description: "Fetch completes")

        fetchUserAsync { result in
            switch result {
            case .success(let user):
                XCTAssertEqual(user.name, "Alice")
            case .failure:
                XCTFail("Should succeed")
            }
            expectation.fulfill()
        }

        wait(for: [expectation], timeout: 5)
    }
}

Мокирование и заглушки

Внедрение зависимостей обеспечивает тестирование: определите протоколы для зависимостей, внедряйте моки в тестах. MockNetworkService возвращает предустановленные результаты. Тестируйте случаи успеха и неудачи, устанавливая mock.fetchUserResult. Этот паттерн развязывает тесты с сетью, делая их быстрыми и детерминированными. Для сложных моков используйте библиотеки вроде Cuckoo или Mockingbird. Тестируйте публичный API ваших типов, а не детали реализации. Моки должны быть простыми и сфокусированными.

swift
// Protocol for dependency injection
protocol NetworkService {
    func fetchUser(id: Int) async throws -> User
}

// Production implementation
class APIService: NetworkService {
    func fetchUser(id: Int) async throws -> User {
        // Real network call
    }
}

// Mock for testing
class MockNetworkService: NetworkService {
    var fetchUserResult: Result<User, Error>?

    func fetchUser(id: Int) async throws -> User {
        switch fetchUserResult {
        case .success(let user): return user
        case .failure(let error): throw error
        case .none: throw URLError(.badURL)
        }
    }
}

class ViewModelTests: XCTestCase {
    func testLoadUser() async throws {
        let mock = MockNetworkService()
        mock.fetchUserResult = .success(User(name: "Alice", age: 30))

        let vm = ViewModel(service: mock)
        await vm.load()

        XCTAssertEqual(vm.userName, "Alice")
    }
}

UI-тестирование

UI-тесты автоматизируют пользовательские взаимодействия через идентификаторы доступности. XCUIApplication запускает приложение. Находите элементы по идентификатору доступности (устанавливается в коде через .accessibilityIdentifier). Действия: tap, typeText, swipeLeft/Right. Утверждения: waitForExistence, exists, hittable. UI-тесты медленнее модульных, но ловят интеграционные ошибки. Устанавливайте идентификаторы доступности в SwiftUI через .accessibilityIdentifier("Email"). Запускайте UI-тесты на нескольких устройствах/симуляторах для покрытия.

swift
import XCTest

class AppUITests: XCTestCase {
    override func setUpWithError() throws {
        continueAfterFailure = false
        let app = XCUIApplication()
        app.launch()
    }

    func testLoginFlow() {
        let app = XCUIApplication()

        let emailField = app.textFields["Email"]
        let passwordField = app.secureTextFields["Password"]
        let loginButton = app.buttons["Log In"]

        emailField.tap()
        emailField.typeText("[email protected]")

        passwordField.tap()
        passwordField.typeText("password123")

        loginButton.tap()

        // Verify navigation
        XCTAssertTrue(app.staticTexts["Welcome, Alice!"].waitForExistence(timeout: 5))
    }

    func testSwipeAndTap() {
        let cell = app.cells["Item 5"]
        cell.swipeLeft()
        app.buttons["Delete"].tap()
        XCTAssertFalse(cell.exists)
    }
}

Производительность и покрытие кода

measure оценивает производительность кода, сравнивая с базовыми показателями. Устанавливайте базовые показатели в Xcode; тесты падают, если регрессия превышает порог. measureMetrics отслеживает конкретные метрики (пиковая память, CPU). Включите покрытие кода в схеме, чтобы видеть непротестированные пути. Стремитесь к покрытию 80%+ бизнес-логики. Снапшот-тестирование (swift-snapshot-testing) захватывает UI для визуальной регрессии. Профилируйте с Instruments (Time Profiler, Allocations) для глубокого анализа. Тесты производительности ловят регрессии на раннем этапе.

swift
class PerformanceTests: XCTestCase {
    func testSortingPerformance() {
        let data = (1...10000).shuffled()

        measure {
            _ = data.sorted()
        }
    }

    func testMemoryUsage() {
        measureMetrics([XCTPerformanceMetric.peakMemory]) {
            _ = (0..<10000).map { _ in [Int](repeating: 0, count: 100) }
        }
    }

    // Baseline comparison
    func testWithBaselines() {
        let options = XCTMeasureOptions()
        options.defaultOptions.measurementUnits = .seconds

        measure(options) {
            // Code to benchmark
        }
    }
}

// Code coverage: enable in Edit Scheme > Test > Options
// View coverage: Cmd+Shift+Y (Report Navigator) > Coverage

// Snapshot testing (third-party)
// func testAppearance() {
//     assertSnapshot(of: view, as: .image)
// }
22

Параллелизм Swift

async/await

async/await (Swift 5.5+) упрощает асинхронный код. async помечает функции, которые могут приостанавливаться. await помечает точки приостановки. Task создаёт асинхронный контекст. Гораздо чище, чем обработчики завершения. Ошибки распространяются через try.

swift
func fetchData() async throws -> String {
    let (data, _) = try await URLSession.shared.data(from: url)
    return String(data: data, encoding: .utf8) ?? ""
}
// Call from async context
Task {
    let result = try await fetchData()
    print(result)
}

Актёры

actor (Swift 5.5+) — ссылочный тип с автоматным взаимным исключением. Только одна задача обращается к состоянию за раз. Методы неявно async при вызове извне. Заменяет блокировки и очереди диспетчеризации для разделяемого изменяемого состояния. Потокобезопасен по конструкции.

swift
actor Counter {
    private var count = 0
    func increment() {
        count += 1
    }
    func getValue() -> Int {
        count
    }
}
// Usage
Task {
    let counter = Counter()
    await counter.increment()
    let value = await counter.getValue()
}

Асинхронная последовательность

AsyncSequence — асинхронный эквивалент Sequence. for await перебирает асинхронно. Полезно для потоковой передачи данных (сеть, строки файлов). Реализуйте AsyncIteratorProtocol для создания пользовательских асинхронных последовательностей. Компилятор обрабатывает приостановку и отмену.

swift
for try await item in asyncSequence {
    print(item)
}
// Custom AsyncSequence
struct Counter: AsyncSequence {
    struct AsyncIterator: AsyncIteratorProtocol {
        var current = 0
        mutating func next() async -> Int? {
            current += 1
            return current <= 5 ? current : nil
        }
    }
    func makeAsyncIterator() -> AsyncIterator { AsyncIterator() }
}

Группы задач

Группы задач выполняют несколько задач параллельно и собирают результаты. addTask добавляет дочернюю задачу. for await перебирает результаты по мере завершения. Все задачи должны завершиться до возврата группы. Структурированный параллелизм: отмена распространяется к дочерним. Результаты собираются безопасно.

swift
let results = await withTaskGroup(of: Int.self) { group in
    for i in 1...5 {
        group.addTask { i * i }
    }
    var sum = 0
    for await result in group {
        sum += result
    }
    return sum
}

Продолжение

withCheckedContinuation мостит API обработчиков завершения к async/await. resume(returning:) возобновляет асинхронную функцию. Должен вызываться ровно один раз. withCheckedThrowingContinuation поддерживает ошибки. Полезно для принятия async/await с существующими API.

swift
func fetchWithCompletion(_ completion: @escaping (String) -> Void) {
    // Legacy completion handler API
    completion("data")
}
// Wrap in async
func fetchAsync() async -> String {
    await withCheckedContinuation { continuation in
        fetchWithCompletion { result in
            continuation.resume(returning: result)
        }
    }
}
23

SwiftUI: глубокое погружение

Модификаторы представлений

Модификаторы оборачивают представления для изменения внешнего вида или поведения. Порядок важен: более поздние модификаторы оборачивают более ранние. .padding перед .background помещает отступ внутрь фона. Модификаторы возвращают новые экземпляры View. Цепочкой их для сложного стиля.

swift
Text("Hello")
    .font(.title)
    .foregroundColor(.blue)
    .padding()
    .background(Color.gray)
    .cornerRadius(8)
    .shadow(radius: 4)

Списки

List отображает прокручиваемые строки. ForEach генерирует строки из данных. onDelete включает свайп для удаления. Другие модификаторы: onMove, onInsert. Идентифицируемые данные (свойство id) требуются для ForEach. Стили списка: .plain, .insetGrouped, .sidebar.

swift
List {
    ForEach(items) { item in
        HStack {
            Text(item.name)
            Spacer()
            Text("\(item.price)")
        }
    }
    .onDelete { indexSet in
        items.remove(atOffsets: indexSet)
    }
}
.listStyle(.insetGrouped)

Навигация

NavigationStack управляет стеком представлений. NavigationLink помещает назначение. navigationTitle устанавливает заголовок. toolbar добавляет кнопки навигационной панели. Для iOS 16+. Используйте NavigationView для старых версий. Листы и оповещения используют модификаторы .sheet и .alert.

swift
NavigationStack {
    List(items) { item in
        NavigationLink(item.name) {
            DetailView(item: item)
        }
    }
    .navigationTitle("Items")
    .toolbar {
        ToolbarItem(placement: .navigationBarTrailing) {
            Button("Add") { addItem() }
        }
    }
}

Управление состоянием

@State для локального состояния представления (типы-значения). @Binding для переданного состояния. @StateObject для собственных наблюдаемых объектов (создаются один раз). @ObservedObject для внешних наблюдаемых объектов. @EnvironmentObject для состояния приложения. Изменения @State запускают обновления представления.

swift
struct CounterView: View {
    @State private var count = 0
    @Binding var selected: Int
    @StateObject var viewModel = ViewModel()
    var body: some View {
        Button("\(count)") { count += 1 }
    }
}

Анимации

withAnimation анимирует изменения состояния. .spring, .easeInOut — кривые анимации. .animation(value:) анимирует при изменении значения. .scaleEffect, .opacity, .offset анимируемы. Неявные анимации используют модификатор .animation. Явные — withAnimation.

swift
struct AnimatedView: View {
    @State private var scale: CGFloat = 1.0
    var body: some View {
        Button("Tap") {
            withAnimation(.spring(response: 0.3)) {
                scale = scale == 1 ? 1.5 : 1
            }
        }
        .scaleEffect(scale)
        .animation(.easeInOut(duration: 0.3), value: scale)
    }
}
24

Фреймворк Combine

Издатели и подписчики

Combine — реактивный фреймворк Apple. Издатели испускают значения со временем. Подписчики их получают. sink создаёт подписчика. Just испускает одно значение. PassthroughSubject — ручной издатель. Cancellable должны удерживаться, иначе подписка отменяется. Похож на RxSwift.

swift
import Combine

let publisher = Just(42)
let cancellable = publisher.sink { value in
    print("Received: \(value)")
}
// Just emits one value then finishes
// PassthroughSubject: manual values
let subject = PassthroughSubject<Int, Never>()
subject.send(1)
subject.send(2)

Операторы

Операторы преобразуют вывод издателя. map преобразует значения. filter выбирает значения. debounce задерживает до тишины. throttle ограничивает частоту. combineLatest объединяет потоки. flatMap компонует издателей. Операторы ленивы: ничего не происходит, пока sink не подпишется.

swift
let publisher = (1...10).publisher
    .map { $0 * 2 }
    .filter { $0 > 10 }
    .sink { print($0) }  // 12, 14, 16, 18, 20

// Debounce
let debounced = subject
    .debounce(for: .seconds(0.5), scheduler: RunLoop.main)
    .sink { print($0) }

@Published

@Published представляет свойство как издателя Combine. Префикс $ получает доступ к издателю. Изменения испускают новые значения. Работает с ObservableObject для SwiftUI. Издатель испускает текущее значение при подписке. Полезно для реактивных обновлений UI без ручных уведомлений.

swift
class ViewModel: ObservableObject {
    @Published var count = 0
}
let vm = ViewModel()
let cancellable = vm.$count.sink { newCount in
    print("Count changed: \(newCount)")
}
vm.count = 1  // Prints "Count changed: 1"
vm.count = 2  // Prints "Count changed: 2"

Future

Future оборачивает API обработчика завершения как издателя. promise(.success) испускает значение. promise(.failure) испускает ошибку. Future испускает ровно один раз. Полезно для мостинга callback API к Combine. Замыкание выполняется энергично, если не обёрнуто через .delay или deferred.

swift
func fetchUser() -> Future<User, Error> {
    Future { promise in
        URLSession.shared.dataTask(with: url) { data, _, error in
            if let error = error { promise(.failure(error)) }
            else { promise(.success(parse(data!))) }
        }.resume()
    }
}
// Usage
fetchUser().sink(receiveCompletion: { _ in }, receiveValue: { user in print(user) })

Обработка ошибок

Fail немедленно испускает ошибку. catch заменяет упавшего издателя другим. retry переподписывается при неудаче. assertNoFailure вызывает сбой при ошибке (для отладки). Событие completion сигнализирует об отсутствии новых значений. Ошибки распространяются вниз по потоку, если не перехвачены.

swift
let publisher = Fail<Int, MyError>(error: .notFound)
    .catch { error in
        Just(0)  // Fallback value
    }
    .sink { print($0) }  // 0

// retry on failure
let retried = requestPublisher
    .retry(3)
    .sink(receiveCompletion: { _ in }, receiveValue: { _ in })
25

Управление памятью

Основы ARC

ARC (Automatic Reference Counting) отслеживает ссылки. Когда счётчик достигает нуля, deinit выполняется и память освобождается. Сильные ссылки увеличивают счётчик. Циклы ссылок предотвращают освобождение. ARC детерминирован (в отличие от GC). deinit выполняется синхронно при удалении последней ссылки.

swift
class Person {
    let name: String
    init(name: String) { self.name = name }
    deinit { print("\(name) is being deinitialized") }
}
var p1: Person? = Person(name: "Alice")
p1 = nil  // Prints "Alice is being deinitialized"

Слабые ссылки

weak ссылки не увеличивают счётчик удержания. Они автоматически устанавливаются в nil при освобождении объекта. Должны быть optional var. Используются для делегатов и наблюдателей для разрыва циклов. weak ссылки обнуляются: безопасны для доступа после удаления объекта. Самое частое исправление циклов удержания.

swift
class View {
    weak var delegate: Delegate?
}
class Delegate {
    var view: View?  // strong
}
// weak: does not keep object alive, auto-set to nil
// Use for delegate patterns to avoid cycles

Unowned ссылки

unowned ссылки не увеличивают счётчик удержания. В отличие от weak, они не опциональны и не обнуляются. Доступ после освобождения вызывает сбой. Используйте, когда объект переживает или умирает вместе со ссылающимся. Быстрее weak (без проверки nil). Часто в замыканиях, захватывающих self, когда self переживает замыкание.

swift
class Customer {
    var card: CreditCard?
}
class CreditCard {
    unowned let customer: Customer
    init(customer: Customer) { self.customer = customer }
}
// unowned: does not keep alive, but not optional
// CRASHES if accessed after dealloc
// Use when the other object has same or shorter lifetime

Циклы удержания

Циклы удержания возникают, когда объекты ссылаются друг на друга сильно. Ни один не освобождается, утекает память. Исправьте через weak или unowned. Часто в делегатах, наблюдателях и замыканиях. Замыкания захватывают self сильно по умолчанию. [weak self] или [unowned self] разрывает цикл. Используйте Memory Graph Debugger для поиска циклов.

swift
// BAD: retain cycle
class Node {
    var next: Node?
    var prev: Node?  // strong -> cycle
}
// GOOD: break with weak
class Node {
    var next: Node?
    weak var prev: Node?
}
// Closures: use [weak self]
{ [weak self] in self?.update() }

Замыкания и захват

Замыкания захватывают переменные по ссылке. Сохраняемые замыкания (escaping) создают циклы удержания, если захватывают self. [weak self] делает захват опциональным и слабым. @escaping помечает замыкания, переживающие вызов функции. Не-escaping замыкания (по умолчанию) не могут создавать циклы. Компилятор предупреждает о потенциальных циклах.

swift
class Timer {
    var handler: (() -> Void)?
    func start() {
        handler = { [weak self] in
            self?.tick()  // weak to avoid cycle
        }
    }
    func tick() { /* ... */ }
}
// Escaping closure: stored for later
// @escaping marks escaping closures
26

Обработка ошибок: глубокое погружение

Пользовательские ошибки

Пользовательские ошибки соответствуют Error. LocalizedError предоставляет errorDescription. Ассоциированные значения несут контекст. Перечисления — идиоматичный способ определения ошибок. Каждый случай представляет отдельную неудачу. Исчерпываемость switch обеспечивает обработку всех ошибок. Соответствуйте CustomStringConvertible для отладочного вывода.

swift
enum APIError: Error, LocalizedError {
    case invalidURL
    case unauthorized
    case serverError(Int)
    case decodingFailed(Error)

    var errorDescription: String? {
        switch self {
        case .invalidURL: return "Invalid URL"
        case .unauthorized: return "Unauthorized"
        case .serverError(let code): return "Server error: \(code)"
        case .decodingFailed(let err): return "Decode failed: \(err)"
        }
    }
}

Тип Result

Result — типизированное перечисление успех/неудача. Полезно для синхронных операций, которые могут завершиться неудачей. map преобразует значение успеха. flatMap компонует операции. get() вызывает throw для преобразования в try/catch. Result предпочтительнее функций, генерирующих ошибки, для сохранённых или переданных результатов. Сочетает обработку ошибок со значимой семантикой.

swift
func fetchUser(id: Int) -> Result<User, APIError> {
    guard id > 0 else { return .failure(.invalidURL) }
    return .success(User(name: "Alice"))
}
// Usage
switch fetchUser(id: 1) {
case .success(let user): print(user)
case .failure(let error): print(error)
}
// map and flatMap
let result = fetchUser(id: 1).map { $0.name }

Rethrows

rethrows распространяет ошибки из параметра-замыкания. Если замыкание не генерирует ошибки, функция тоже не генерирует. Это избегает принуждения try для вызывающих, передающих не-генерирующие замыкания. Используется map, filter и другими функциями высшего порядка. Сама функция не может генерировать ошибки независимо от замыкания.

swift
func withLock<T>(_ lock: NSLock, _ body: () throws -> T) rethrows -> T {
    lock.lock()
    defer { lock.unlock() }
    return try body()
}
// Only throws if body throws
let x = withLock(lock) { 5 }  // no try needed
let y = try withLock(lock) { try throwingFunc() }

Распространение ошибок

try распространяет ошибки вызывающему. try? преобразует ошибки в nil (возвращает Optional). try! вызывает сбой при ошибке (используйте когда уверены). Ошибки поднимаются по стеку вызовов, пока не перехвачены. Функции, генерирующие ошибки, должны быть помечены throws. Компилятор обеспечивает маркеры try, предотвращая необработанные ошибки. defer выполняется независимо от сгенерированных ошибок.

swift
func process() throws {
    let data = try fetchData()  // may throw
    let user = try parse(data)  // may throw
    save(user)                  // does not throw
}
// try?: converts to optional, nil on error
let user = try? fetchUser()
// try!: asserts no error, crashes if thrown
let user = try! fetchUser()

Очистка через defer

defer планирует очистку при выходе из области. Выполняется независимо от способа выхода (return, throw или провал). Несколько defer выполняются в порядке LIFO. Полезно для закрытия файлов, освобождения блокировок, освобождения ресурсов. defer не может throw или break/continue. Замыкание захватывает переменные по ссылке.

swift
func processFile() throws {
    let file = open("file.txt")
    defer { close(file) }  // runs on scope exit

    let data = try read(file)
    // ... process ...
    // close() runs here even if read throws
}
// Multiple defers run in reverse order (LIFO)
defer { print("1") }
defer { print("2") }  // prints 2 then 1

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.