0Pricing
Swift Academy · Урок

Эффекты и зависимости

Обрабатывайте побочные эффекты и внедряйте зависимости.

«Эффекты и зависимости» — бесплатный урок Swift Academy на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Swift Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Swift Academy содержит 4 уроков всего.

Зачем нужны эффекты

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

Именно такое разделение делает компоненты пригодными для тестирования.

import ComposableArchitecture

// Reducer stays pure; side effects are values it returns.
// Returning .none means no further work.

Возврат .none

Если действие только изменяет состояние и не требует дальнейшей работы, редуктор возвращает .none. Это самый распространённый эффект, означающий отсутствие асинхронной работы.

Вы уже видели его: это базовый вариант, к которому возвращается каждая ветвь.

import ComposableArchitecture

Reduce { state, action in
    switch action {
    case .incrementTapped:
        state.count += 1
        return .none
    }
}

Effect.run для асинхронной работы

Чтобы выполнить асинхронную работу, верните .run. Его замыкание получает дескриптор send, с помощью которого результаты передаются обратно в систему как новые действия.

Здесь действие fetch загружает данные и после завершения отправляет действие ответа.

import ComposableArchitecture

Reduce { state, action in
    switch action {
    case .reloadTapped:
        return .run { send in
            let value = try await loadNumber()
            await send(.responseReceived(value))
        }
    case let .responseReceived(value):
        state.count = value
        return .none
    }
}

Возврат результатов в виде действий

Эффект не может напрямую изменять состояние; вместо этого он отправляет действия через send. Затем редуктор обрабатывает эти действия и обновляет состояние. Благодаря этому циклу все изменения остаются внутри редуктора.

Таким образом, сетевой ответ превращается в действие ответа, которое редуктор сохраняет.

import ComposableArchitecture

.run { send in
    let result = try await fetch()
    await send(.dataLoaded(result))
}
// .dataLoaded is then handled by the reducer to set state.

Обработка ошибок в эффектах

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

Необязательное замыкание catch у .run получает возникшую ошибку.

import ComposableArchitecture

.run { send in
    let value = try await loadNumber()
    await send(.responseReceived(value))
} catch: { error, send in
    await send(.loadFailed)
}

Почему важны зависимости

Если редуктор напрямую вызывает сетевой клиент или считывает дату, его становится трудно тестировать и контролировать. TCA решает эту проблему с помощью зависимостей — внедряемых служб, к которым редуктор обращается через @Dependency.

В тестах их можно заменить предсказуемыми имитациями.

import ComposableArchitecture

// Instead of calling APIClient.shared directly,
// inject it as a dependency so tests can replace it.

Обёртка свойства @Dependency

Внутри редуктора зависимость объявляют с помощью @Dependency, указывая её ключевой путь в значениях зависимостей. Библиотека предоставляет встроенные зависимости, например clock и генератор идентификаторов UUID.

Здесь редуктор получает зависимость continuousClock.

import ComposableArchitecture

@Reducer
struct TimerFeature {
    @ObservableState
    struct State: Equatable { var seconds = 0 }
    enum Action { case start, tick }

    @Dependency(\.continuousClock) var clock

    var body: some ReducerOf<Self> {
        Reduce { state, action in .none }
    }
}

Использование зависимости в эффекте

После объявления зависимость можно использовать внутри .run. Поскольку clock внедряется извне, в тестах можно передать тестовую реализацию clock, которая продвигает время по требованию.

Здесь таймер срабатывает каждую секунду с использованием внедрённой зависимости clock.

import ComposableArchitecture

var body: some ReducerOf<Self> {
    Reduce { state, action in
        switch action {
        case .start:
            return .run { send in
                for await _ in self.clock.timer(interval: .seconds(1)) {
                    await send(.tick)
                }
            }
        case .tick:
            state.seconds += 1
            return .none
        }
    }
}

Определение собственной зависимости

Пользовательские зависимости регистрируют, сопоставляя значение с DependencyKey и предоставляя liveValue. Затем в DependencyValues добавляют вычисляемое свойство, чтобы зависимость получила ключевой путь.

Здесь клиент программного интерфейса становится полноценной зависимостью.

import ComposableArchitecture

struct NumberClient {
    var fetch: () async throws -> Int
}

extension NumberClient: DependencyKey {
    static let liveValue = NumberClient {
        try await loadNumber()
    }
}

extension DependencyValues {
    var numberClient: NumberClient {
        get { self[NumberClient.self] }
        set { self[NumberClient.self] = newValue }
    }
}

Внедрение пользовательской зависимости

После добавления ключевого пути редуктор объявляет клиент с помощью @Dependency(\.numberClient) и вызывает его внутри эффектов. В рабочей среде используется активное значение, а в тестах его можно переопределить.

Здесь действие перезагрузки получает данные через внедрённый клиент.

import ComposableArchitecture

@Reducer
struct NumberFeature {
    @ObservableState
    struct State: Equatable { var value = 0 }
    enum Action { case reload, response(Int) }

    @Dependency(\.numberClient) var numberClient

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .reload:
                return .run { send in
                    let n = try await numberClient.fetch()
                    await send(.response(n))
                }
            case let .response(n):
                state.value = n
                return .none
            }
        }
    }
}

Отмена эффектов

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

Здесь таймер можно остановить по требованию.

import ComposableArchitecture

enum CancelID { case timer }

// Start a cancellable effect:
.run { send in /* ... */ }
    .cancellable(id: CancelID.timer)

// Stop it from another action:
.cancel(id: CancelID.timer)

Быстрая проверка: эффекты и результаты

Вспомните, как эффект передаёт свой результат обратно редьюсеру.

Итоги: эффекты и зависимости

Вы узнали, как TCA обрабатывает побочные эффекты и внедрение зависимостей:

  • Редьюсеры остаются чистыми и возвращают значения Effect; .none означает отсутствие работы.
  • .run выполняет асинхронную работу и передаёт результаты обратно через await send(.action).
  • Ошибки обрабатываются в замыкании catch эффекта.
  • @Dependency внедряет такие службы, как часы или собственные клиенты, объявленные через DependencyKey и DependencyValues.
  • Длительные эффекты можно сделать cancellable по идентификатору.

Далее вы научитесь объединять функциональности и исчерпывающе проверять их.

import ComposableArchitecture

// Recap: pure reducer + Effect.run for async work
// + @Dependency for testable, injectable services.

Часто задаваемые вопросы

Урок «Эффекты и зависимости» бесплатный?

Да — полный текст урока «Эффекты и зависимости» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Swift Academy, подпишись на CoddyKit PRO. Курс Swift Academy содержит 4 уроков всего.

Чему я научусь в уроке «Эффекты и зависимости»?

Обрабатывайте побочные эффекты и внедряйте зависимости. Ты практикуешь Swift Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать Swift Academy?

Предыдущий опыт не требуется. Swift Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.

Сколько времени занимает урок «Эффекты и зависимости»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке Swift Academy?

Да. Каждый урок Swift Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Состояние, действие и редьюсер
  2. Хранилище и интеграция со SwiftUI
  3. Эффекты и зависимости
  4. Композиция и тестирование возможностей
← Назад к Swift Academy