Компонуемая архитектура (сокращенно TCA) — это библиотека для последовательного и понятного создания приложений с учетом композиции, тестирования и эргономики. Его можно использовать в SwiftUI, UIKit и других платформах Apple (iOS, macOS, VisionOS, tvOS и watchOS).
Что такое компонуемая архитектура?
Узнать больше
Примеры
Основное использование
Документация
Сообщество
Установка
Переводы
Эта библиотека предоставляет несколько основных инструментов, которые можно использовать для создания приложений различного назначения и сложности. В нем представлены убедительные истории, которые вы можете использовать для решения многих проблем, с которыми вы сталкиваетесь изо дня в день при создании приложений, таких как:
Государственное управление
Как управлять состоянием вашего приложения с помощью простых типов значений и делиться состоянием на нескольких экранах, чтобы изменения на одном экране можно было немедленно наблюдать на другом экране.
Состав
Как разбить большие функции на более мелкие компоненты, которые можно извлечь в отдельные изолированные модули и легко склеить обратно, чтобы сформировать функцию.
Побочные эффекты
Как позволить определенным частям приложения взаимодействовать с внешним миром наиболее тестируемым и понятным способом.
Тестирование
Как не только тестировать встроенную в архитектуру функцию, но и писать интеграционные тесты для функций, состоящих из множества частей, а также писать сквозные тесты, чтобы понять, как побочные эффекты влияют на ваше приложение. Это позволяет вам дать надежные гарантии того, что ваша бизнес-логика работает так, как вы ожидаете.
Эргономика
Как реализовать все вышеперечисленное с помощью простого API с минимальным количеством концепций и движущихся частей.
Компонуемая архитектура разрабатывалась на протяжении многих эпизодов Point-Free, серии видео, посвященной функциональному программированию и языку Swift, которую вели Брэндон Уильямс и Стивен Селис.
Здесь можно посмотреть все выпуски, а также специальную многочастную экскурсию по архитектуре с нуля.
В этом репозитории содержится множество примеров, демонстрирующих, как решать распространенные и сложные проблемы с помощью составной архитектуры. Посетите этот каталог, чтобы увидеть их все, в том числе:
Тематические исследования
Начиная
Эффекты
Навигация
Редукторы высшего порядка
Многоразовые компоненты
Менеджер местоположения
Менеджер движения
Поиск
Распознавание речи
Приложение SyncUps
Крестики-нолики
Тодос
Голосовые заметки
Ищете что-то более существенное? Ознакомьтесь с исходным кодом isowords, игры для поиска слов для iOS, созданной на SwiftUI и Composable Architecture.
Примечание
Чтобы получить пошаговое интерактивное руководство, обязательно ознакомьтесь с разделом «Знакомство с составной архитектурой».
Чтобы создать функцию с использованием составной архитектуры, вы определяете некоторые типы и значения, которые моделируют ваш домен:
Состояние : тип, описывающий данные, необходимые вашей функции для выполнения своей логики и отображения пользовательского интерфейса.
Действие : тип, который представляет все действия, которые могут произойти с вашей функцией, например действия пользователя, уведомления, источники событий и т. д.
Редуктор : функция, которая описывает, как преобразовать текущее состояние приложения в следующее состояние при выполнении действия. Редюсер также отвечает за возврат любых эффектов, которые должны быть запущены, например, запросов API, что можно сделать, вернув значение Effect
.
Store : среда выполнения, которая фактически управляет вашей функцией. Вы отправляете все действия пользователя в хранилище, чтобы хранилище могло запускать редуктор и эффекты, и вы могли наблюдать за изменениями состояния в хранилище, чтобы иметь возможность обновить пользовательский интерфейс.
Преимущество этого в том, что вы мгновенно разблокируете возможность тестирования своей функции и сможете разбить большие и сложные функции на более мелкие области, которые можно будет склеить вместе.
В качестве базового примера рассмотрим пользовательский интерфейс, в котором отображается число вместе с кнопками «+» и «-», которые увеличивают и уменьшают число. Чтобы было интереснее, предположим, что есть также кнопка, которая при нажатии отправляет запрос API на получение случайного факта об этом числе и отображает его в представлении.
Чтобы реализовать эту функцию, мы создаем новый тип, который будет содержать домен и поведение функции, и он будет помечен макросом @Reducer
:
импортировать функцию ComposableArchitecture@Reducerstruct {}
Здесь нам нужно определить тип состояния функции, который состоит из целого числа для текущего счетчика, а также необязательной строки, которая представляет представляемый факт:
@Reducerstruct Feature { @ObservableState struct State: Equatable { var count = 0 var NumberFact: String? }}
Примечание
Мы применили макрос @ObservableState
к State
, чтобы воспользоваться преимуществами инструментов наблюдения в библиотеке.
Нам также необходимо определить тип действий функции. Существуют очевидные действия, такие как нажатие кнопки уменьшения, кнопки увеличения или кнопки факта. Но есть и несколько неочевидные, например действие, которое происходит, когда мы получаем ответ на запрос API фактов:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { Case decrementButtonTapped Case IncrementButtonTapped Case NumberFactButtonTapped Case NumberFactResponse(String) }}
Затем мы реализуем свойство body
, которое отвечает за формирование фактической логики и поведения функции. В нем мы можем использовать Reduce
Редуктор, чтобы описать, как изменить текущее состояние на следующее состояние и какие эффекты необходимо выполнить. Некоторым действиям не обязательно выполнять эффекты, и они могут возвращать .none
чтобы представить это:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: some Редюсер{ уменьшает { состояние, действие в действие переключения {case.decrementButtonTapped: state.count -= 1 return .none case .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] отправить let (data, _) = попробовать дождаться URLSession.shared.data( откуда: URL (строка: «http://numbersapi.com/(count)/trivia»)! ) await send( .numberFactResponse(String(decoding: data, as: UTF8.self)) ) } case let .numberFactResponse(факт): state.numberFact = возврат факта .none } } }}
И затем, наконец, мы определяем представление, отображающее функцию. Он удерживает StoreOf
, чтобы отслеживать все изменения состояния и выполнять повторную отрисовку, а мы можем отправлять все действия пользователя в хранилище, чтобы состояние менялось:
struct FeatureView: View { let store: StoreOfvar body: some View { Form { Раздел { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "Increment") { store.send(.incrementButtonTapped) } } Раздел { Button("Числовой факт") { store.send(.numberFactButtonTapped) } } если пусть факт = store.numberFact { Text(факт) } } }}
Также легко отключить контроллер UIKit от этого хранилища. Вы можете наблюдать за изменениями состояния в хранилище в viewDidLoad
, а затем заполнять компоненты пользовательского интерфейса данными из хранилища. Код немного длиннее, чем версия SwiftUI, поэтому мы свернули его здесь:
класс FeatureViewController: UIViewController { let store: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, Bundle: nil) } требуется init?(coder: NSCoder) { FatalError("init(coder:) не реализован") } override func viewDidLoad() { super.viewDidLoad(), пусть countLabel = UILabel(), пусть decrementButton = UIButton(), пусть IncrementButton = UIButton(), пусть factLabel = UILabel() // Пропущено: добавить подпредставления и настроить ограничения... наблюдать {[слабое я] в охранник пусть себя еще {возврат} countLabel.text = "(self.store.text)" factLabel.text = self.store.numberFact } } @objc Private funccrementButtonTapped() { self.store.send(.incrementButtonTapped) } @objc Private func decrementButtonTapped() { self.store.send(.decrementButtonTapped) } @objc Private функция factButtonTapped() { self.store.send(.numberFactButtonTapped) }}
Когда мы будем готовы отобразить это представление, например, в точке входа приложения, мы сможем создать хранилище. Это можно сделать, указав начальное состояние для запуска приложения, а также редуктор, который будет обеспечивать работу приложения:
import ComposableArchitecture@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( store: Store(initialState: Feature.State()) { Feature() } ) } }}
И этого достаточно, чтобы на экране появилось что-то, с чем можно поиграть. Это определенно на несколько шагов больше, чем если бы вы делали это стандартным способом SwiftUI, но есть несколько преимуществ. Это дает нам последовательный способ применения мутаций состояний вместо разбрасывания логики по некоторым наблюдаемым объектам и различным замыканиям действий компонентов пользовательского интерфейса. Это также дает нам краткий способ выражения побочных эффектов. И мы можем сразу протестировать эту логику, включая эффекты, не выполняя особой дополнительной работы.
Примечание
Более подробную информацию о тестировании можно найти в специальной статье о тестировании.
Для тестирования используйте TestStore
, который можно создать с той же информацией, что и Store
, но он выполняет дополнительную работу, позволяющую вам определить, как ваша функция развивается по мере отправки действий:
@Testfunc Basics() async { let store = TestStore(initialState: Feature.State()) { Feature() }}
После создания тестового хранилища мы можем использовать его для подтверждения всего потока действий пользователя. На каждом этапе пути нам нужно доказывать, что состояние изменилось так, как мы ожидаем. Например, мы можем смоделировать процесс нажатия пользователем кнопок увеличения и уменьшения:
// Проверяем, что нажатие кнопок увеличения/уменьшения изменяет счетчик ожидания store.send(.incrementButtonTapped) { $0.count = 1}await store.send(.decrementButtonTapped) { $0.count = 0}
Кроме того, если шаг вызывает выполнение эффекта, который возвращает данные в хранилище, мы должны утверждать это. Например, если мы имитируем нажатие пользователем кнопки факта, мы ожидаем получить ответный ответ с фактом, что затем приведет к заполнению состояния numberFact
:
await store.send(.numberFactButtonTapped)await store.receive(.numberFactResponse) { $0.numberFact = ???}
Однако как мы узнаем, какой факт будет отправлен нам обратно?
В настоящее время наш редуктор использует эффект, который распространяется на реальный мир и воздействует на сервер API, а это означает, что у нас нет возможности контролировать его поведение. Для написания этого теста мы находимся в зависимости от нашего подключения к Интернету и доступности сервера API.
Было бы лучше передать эту зависимость в редуктор, чтобы мы могли использовать живую зависимость при запуске приложения на устройстве, но использовать имитированную зависимость для тестов. Мы можем сделать это, добавив свойство в редуктор Feature
:
@Reducerstruct Feature { let numberFact: (Int) асинхронные броски -> String // ...}
Затем мы можем использовать его в реализации reduce
:
случай .numberFactButtonTapped: return .run { [count = state.count] отправить let fact = попробуйте await self.numberFact(count) await send(.numberFactResponse(fact)) }
И в точке входа приложения мы можем предоставить версию зависимости, которая фактически взаимодействует с реальным API-сервером:
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( store: Store(initialState: Feature.State()) { Feature( NumberFact: { число в let (data, _) = попробуйте дождаться URLSession.shared.data( откуда: URL-адрес (строка: «http://numbersapi.com/(номер)»)! ) return String(decoding: data, as: UTF8.self) } ) } ) } }}
Но в тестах мы можем использовать фиктивную зависимость, которая немедленно возвращает детерминированный, предсказуемый факт:
@Testfunc Basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0) — хорошее число Brent" }) }}
С помощью этой небольшой предварительной работы мы можем завершить тест, имитируя нажатие пользователем кнопки факта, а затем получение ответа от зависимости, чтобы представить факт:
await store.send(.numberFactButtonTapped)await store.receive(.numberFactResponse) { $0.numberFact = "0 — хорошее число Brent"}
Мы также можем улучшить эргономику использования зависимости numberFact
в нашем приложении. Со временем приложение может развиться во множество функций, и некоторым из этих функций также может потребоваться доступ к numberFact
, и явная передача его через все уровни может раздражать. Существует процесс, с помощью которого вы можете «зарегистрировать» зависимости в библиотеке, сделав их мгновенно доступными для любого уровня приложения.
Примечание
Более подробную информацию об управлении зависимостями см. в специальной статье о зависимостях.
Мы можем начать с обертывания функциональности числового факта в новый тип:
struct NumberFactClient { var fetch: (Int) асинхронные броски -> String}
А затем зарегистрировать этот тип в системе управления зависимостями, согласовав клиент с протоколом DependencyKey
, который требует от вас указать активное значение, которое будет использоваться при запуске приложения в симуляторах или устройствах:
расширение NumberFactClient: DependencyKey { static let liveValue = Self( fetch: { число в let (data, _) = попробуйте дождаться URLSession.shared .data(from: URL(строка: "http://numbersapi.com/(number)")!) return String(decoding: data, как: UTF8.self) } )}extension DependencyValues { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = newValue } }}
Проделав эту небольшую предварительную работу, вы можете мгновенно начать использовать зависимость в любой функции, используя оболочку свойства @Dependency
:
@Редусер struct Feature {- let NumberFact: (Int) асинхронные броски -> String+ @Dependency(.numberFact) var NumberFact …- попробуйте await self.numberFact(count)+ попробуйте await self.numberFact.fetch(count) }
Этот код работает точно так же, как и раньше, но вам больше не нужно явно передавать зависимость при создании редуктора функции. При запуске приложения в предварительной версии, симуляторе или на устройстве редюсеру будет предоставлена живая зависимость, а в тестах — тестовая зависимость.
Это означает, что точке входа в приложение больше не нужно создавать зависимости:
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( store: Store(initialState: Feature.State()) { Feature() } ) } }}
Тестовое хранилище можно создать без указания каких-либо зависимостей, но вы все равно можете переопределить любую зависимость, необходимую для целей теста:
let store = TestStore(initialState: Feature.State()) { Feature()} withDependologies: { $0.numberFact.fetch = { "($0) — хорошее число Brent" }}// ...
Это основы создания и тестирования функции в составной архитектуре. Предстоит изучить гораздо больше вещей, таких как композиция, модульность, адаптируемость и сложные эффекты. В каталоге «Примеры» есть множество проектов, которые стоит изучить, чтобы увидеть более сложные варианты использования.
Документация по релизам и main
доступна здесь:
main
1.17.0 (руководство по миграции)
1.16.0 (руководство по миграции)
1.15.0 (руководство по миграции)
1.14.0 (руководство по миграции)
1.13.0 (руководство по миграции)
1.12.0 (руководство по миграции)
1.11.0 (руководство по миграции)
1.10.0 (руководство по миграции)
1.9.0 (руководство по миграции)
1.8.0 (руководство по миграции)
1.7.0 (руководство по миграции)
1.6.0 (руководство по миграции)
1.5.0 (руководство по миграции)
1.4.0 (руководство по миграции)
1.3.0
1.2.0
1.1.0
1.0.0
0.59.0
0.58.0
0.57.0
В документации есть ряд статей, которые могут оказаться полезными, когда вы освоитесь с библиотекой:
Начиная
Зависимости
Тестирование
Навигация
Состояние общего доступа
Производительность
Параллелизм
Привязки
Если вы хотите обсудить составную архитектуру или задать вопрос о том, как ее использовать для решения конкретной проблемы, вы можете обсудить ее с другими энтузиастами Point-Free:
Для длительных обсуждений мы рекомендуем вкладку обсуждений в этом репозитории.
Для случайного общения мы рекомендуем Slack Point-Free Community.
Вы можете добавить ComposableArchitecture в проект Xcode, добавив его как зависимость пакета.
В меню «Файл» выберите «Добавить зависимости пакета…».
Введите «https://github.com/pointfreeco/swift-composable-architecture» в текстовое поле URL-адреса репозитория пакетов.
В зависимости от структуры вашего проекта:
Если у вас есть одно целевое приложение, которому требуется доступ к библиотеке, добавьте ComposableArchitecture непосредственно в ваше приложение.
Если вы хотите использовать эту библиотеку из нескольких целей Xcode или смешивать цели Xcode и цели SPM, вам необходимо создать общую платформу, которая зависит от ComposableArchitecture , а затем зависеть от этой платформы во всех ваших целях. В качестве примера можно привести демонстрационное приложение Tic-Tac-Toe, которое разбивает множество функций на модули и использует статическую библиотеку таким образом, используя пакет Swift tic-tac-toe .
Компонуемая архитектура построена с учетом расширяемости, и для улучшения ваших приложений доступен ряд поддерживаемых сообществом библиотек:
Дополнения к составной архитектуре: дополнительная библиотека к составной архитектуре.
TCAComposer: платформа макросов для генерации стандартного кода в составной архитектуре.
TCACoordinators: шаблон координатора в составной архитектуре.
Если вы хотите внести свой вклад в библиотеку, откройте PR со ссылкой на нее!
Следующие переводы этого README были предоставлены членами сообщества:
арабский
Французский
хинди
индонезийский
итальянский
японский
корейский
Польский
португальский
Русский
Упрощенный китайский
испанский
Украинский
Если вы хотите внести свой вклад в перевод, откройте PR со ссылкой на Gist!
У нас есть специальная статья для всех наиболее часто задаваемых вопросов и комментариев, касающихся библиотеки.
Следующие люди оставили отзывы о библиотеке на ранних стадиях ее создания и помогли сделать библиотеку такой, какая она есть сегодня:
Пол Колтон, Каан Дедеоглу, Мэтт Дипхаус, Йозеф Долежал, Эймантас, Мэттью Джонсон, Джордж Каймакас, Никита Леонов, Кристофер Лишио, Джеффри Мако, Алехандро Мартинес, Шай Мишали, Уиллис Пламмер, Симон-Пьер Рой, Джастин Прайс, Свен А. Шмидт , Кайл Шерман, Петр Шима, Ясдев Сингх, Максим Смирнов, Райан Стоун, Дэниел Холлис Таварес и все подписчики Point-Free?
Особая благодарность Крису Лисио, который помог нам разобраться со многими странными особенностями SwiftUI и усовершенствовать окончательный API.
И спасибо Шаю Мишали и проекту JointCommunity, из которого мы взяли реализацию Publishers.Create
, которую мы используем в Effect
, чтобы помочь соединить API-интерфейсы делегирования и обратного вызова, что значительно упрощает взаимодействие со сторонними платформами.
Компонуемая архитектура была построена на основе идей других библиотек, в частности Elm и Redux.
В сообществе Swift и iOS также есть множество архитектурных библиотек. Каждый из них имеет свой собственный набор приоритетов и компромиссов, которые отличаются от составной архитектуры.
РИБы
Петля
РеСвифт
Рабочий процесс
РеакторКит
RxОбратная связь
Мобиус.Свифт
Флюксор
ОбещаннаяАрхитектураКомплект
Эта библиотека выпущена под лицензией MIT. Подробности см. в разделе ЛИЦЕНЗИЯ.