/swift-composable-architecture

構成、テスト、人間工学を考慮し、一貫性のある理解しやすい方法でアプリケーションを構築するためのライブラリ。

Primary LanguageSwiftMIT LicenseMIT

The Composable Architecture

CI Slack

The Composable Architecture(略してTCA)は、コンポジション、テスト、人間工学を考慮し、一貫性のあります理解しやすい方法でアプリケーションを構築しますためのライブラリです。 SwiftUI、UIKit、その他、あらゆるAppleプラットフォーム(iOS、macOS、tvOS、watchOS)で使用できます。

What is the Composable Architecture?

このライブラリは、様々な目的や複雑さのアプリケーションを構築するために使用できる、いくつかのコアツールを提供します。 アプリケーションを構築する際に日々遭遇する多くの問題を解決するために、以下のような説得力のあるストーリーを提供しています:

  • State management
    単純な値型を使ってアプリケーションの状態を管理し、多くの画面にわたって状態を共有する方法。

  • Composition
    大規模な機能をどのように小さな構成要素に分解しますか、 モジュールに分離し、簡単につなぎ合わせて特徴を形成します。

  • Side effects
    アプリケーションの特定の部分を、可能な限りテスト可能で理解しやすい方法で外部と対話させる。

  • Testing
    アーキテクチャで構築されました機能をテストしますだけでなく、統合テストをどのように書くか。 また、エンドツーエンドのテストを書いて、副作用がアプリケーションにどのような影響を与えますかを理解しますためにエンドツーエンドのテストを書きます。 これによって、ビジネス・ロジックが以下のような方法で実行されていますことを強く保証します。 ビジネス・ロジックが期待通りに動作していますことを強く保証できます。

  • Ergonomics
    上記のすべてを、できるだけ概念や可動部の少ないシンプルなAPIで実現するにはどうすればいいか。

Learn More

コンポーザブル・アーキテクチャーは、ポイントフリーの多くのエピソードを経て設計されました。 Point-Free、関数型プログラミングとSwift言語を探求しますビデオシリーズ、 Brandon Williams]mbrandonwStephen Celisによってホストされています。

すべてのエピソードはこちらで見えます。 multiparttourでアーキテクチャをゼロから見えます。

video poster image

Examples

Screen shots of example applications

このリポジトリには、一般的な問題や複雑な問題をどのように解決しますかを示す、たくさんのサンプルが含まれています。 コンポーザブル・アーキテクチャで一般的で複雑な問題を解決します方法を示す、たくさんの例が含まれています。 このディレクトリをチェックアウトして、それらを全て見てください:

もっと充実したものをお探しですか? isowordsのソースコードをチェックしましょう。 これはSwiftUIとComposable Architectureで構築されましたiOSの単語検索ゲームです。

Basic Usage

Note

For a step-by-step interactive tutorial, be sure to check out Meet the Composable Architecture.

To build a feature using the Composable Architecture you define some types and values that model your domain:

  • State: その機能がロジックを実行し、UIをレンダリングするために必要なデータを記述する型。
  • Action: ユーザーアクション、通知、イベントソースなど、機能内で起こりうるすべてのアクションを表すタイプ。
  • Reducer: アクションが与えられたときに、アプリの現在の状態を次の状態に進化させる方法を記述する関数。 に進化させる方法を記述する関数です。リデューサーは、APIリクエストのような実行すべきエフェクトを返す役割も担っています。 これは Effect 値を返すことで行うことができます。
  • Store: あなたの機能を実際に動かすランタイム。すべてのユーザーアクションをストアに送信します。 ストアがリデューサーとエフェクトを実行し、ストアの状態変化を観察してUIを更新できるようにします。 ストアの状態変化を観察し、UIを更新することができます。

こうしますことの利点は、機能のテスト可能性を即座に解き放つことができますことです。 大規模で複雑な機能を小さなドメインに分割し、それらをつなぎ合わせることができます。

基本的な例として、数字を表示しますUIと、数字を増減させる「+」と「-」ボタンを考えてみよう。 と「-」ボタンが表示されますUIを考えてみよう。面白くしますために、次のようなボタンもありますとします。 をタップしますと、その数字に関しますランダムな事実を取得しますAPIリクエストが行われ、ビューに表示されます。

この機能を実装しますために、この機能のドメインとビヘイビアを格納します新しい型を作成します。 この型は @Reducer マクロでアノテーションされます:

import ComposableArchitecture

@Reducer
struct Feature {
}

ここでは、Featureの状態を表す型を定義する必要があります。 これにはオプションで示される要素を表す文字列を指定します:

@Reducer
struct Feature {
  @ObservableState
  struct State: Equatable {
    var count = 0
    var numberFact: String?
  }
}

Note

We've applied the @ObservableState macro to State in order to take advantage of the observation tools in the library.

また、その機能のアクションの型も定義します必要があります。 デクリメント・ボタン、インクリメント・ボタン、要素ボタンのタップなど、明らかなアクションがあります。 しかし、ファクトAPIリクエストからのレスポンスを受信したときに発生するアクションのように、明白ではありませんものもあります:

@Reducer
struct Feature {
  @ObservableState
  struct State: Equatable { /* ... */ }
  enum Action {
    case decrementButtonTapped
    case incrementButtonTapped
    case numberFactButtonTapped
    case numberFactResponse(String)
  }
}

そして、body プロパティを実装します。このプロパティは機能の実際のロジックと振る舞いを構成します役割を持ちます。 この中で Reduce リデューサを使って、現在の状態を次の状態に変更します方法や、実行します必要がありますエフェクトを記述できます。 アクションによってはエフェクトを実行します必要がありませんものもあり、その場合は .none を返します:

@Reducer
struct Feature {
  @ObservableState
  struct State: Equatable { /* ... */ }
  enum Action { /* ... */ }

  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      case .decrementButtonTapped:
        state.count -= 1
        return .none

      case .incrementButtonTapped:
        state.count += 1
        return .none

      case .numberFactButtonTapped:
        return .run { [count = state.count] send in
          let (data, _) = try await URLSession.shared.data(
            from: URL(string: "http://numbersapi.com/\(count)/trivia")!
          )
          await send(
            .numberFactResponse(String(decoding: data, as: UTF8.self))
          )
        }

      case let .numberFactResponse(fact):
        state.numberFact = fact
        return .none
      }
    }
  }
}

And then finally we define the view that displays the feature. It holds onto a StoreOf<Feature> so that it can observe all changes to the state and re-render, and we can send all user actions to the store so that state changes:

struct FeatureView: View {
  let store: StoreOf<Feature>

  var body: some View {
    Form {
      Section {
        Text("\(store.count)")
        Button("Decrement") { store.send(.decrementButtonTapped) }
        Button("Increment") { store.send(.incrementButtonTapped) }
      }

      Section {
        Button("Number fact") { store.send(.numberFactButtonTapped) }
      }
      
      if let fact = store.numberFact {
        Text(fact)
      }
    }
  }
}

It is also straightforward to have a UIKit controller driven off of this store. You can observe state changes in the store in viewDidLoad, and then populate the UI components with data from the store. The code is a bit longer than the SwiftUI version, so we have collapsed it here:

Click to expand!
class FeatureViewController: UIViewController {
  let store: StoreOf<Feature>

  init(store: StoreOf<Feature>) {
    self.store = store
    super.init(nibName: nil, bundle: nil)
  }

  required init?(coder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    let countLabel = UILabel()
    let decrementButton = UIButton()
    let incrementButton = UIButton()
    let factLabel = UILabel()
    
    // Omitted: Add subviews and set up constraints...
    
    observe { [weak self] in
      guard let self 
      else { return }
      
      countLabel.text = "\(self.store.text)"
      factLabel.text = self.store.numberFact
    }
  }

  @objc private func incrementButtonTapped() {
    self.store.send(.incrementButtonTapped)
  }
  @objc private func decrementButtonTapped() {
    self.store.send(.decrementButtonTapped)
  }
  @objc private func factButtonTapped() {
    self.store.send(.numberFactButtonTapped)
  }
}

Once we are ready to display this view, for example in the app's entry point, we can construct a store. This can be done by specifying the initial state to start the application in, as well as the reducer that will power the application:

import ComposableArchitecture

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      FeatureView(
        store: Store(initialState: Feature.State()) {
          Feature()
        }
      )
    }
  }
}

And that is enough to get something on the screen to play around with. It's definitely a few more steps than if you were to do this in a vanilla SwiftUI way, but there are a few benefits. It gives us a consistent manner to apply state mutations, instead of scattering logic in some observable objects and in various action closures of UI components. It also gives us a concise way of expressing side effects. And we can immediately test this logic, including the effects, without doing much additional work.

Testing

Note

For more in-depth information on testing, see the dedicated testing article.

To test use a TestStore, which can be created with the same information as the Store, but it does extra work to allow you to assert how your feature evolves as actions are sent:

@MainActor
func testFeature() async {
  let store = TestStore(initialState: Feature.State()) {
    Feature()
  }
}

Once the test store is created we can use it to make an assertion of an entire user flow of steps. Each step of the way we need to prove that state changed how we expect. For example, we can simulate the user flow of tapping on the increment and decrement buttons:

// Test that tapping on the increment/decrement buttons changes the count
await store.send(.incrementButtonTapped) {
  $0.count = 1
}
await store.send(.decrementButtonTapped) {
  $0.count = 0
}

Further, if a step causes an effect to be executed, which feeds data back into the store, we must assert on that. For example, if we simulate the user tapping on the fact button we expect to receive a fact response back with the fact, which then causes the numberFact state to be populated:

await store.send(.numberFactButtonTapped)

await store.receive(\.numberFactResponse) {
  $0.numberFact = ???
}

However, how do we know what fact is going to be sent back to us?

Currently our reducer is using an effect that reaches out into the real world to hit an API server, and that means we have no way to control its behavior. We are at the whims of our internet connectivity and the availability of the API server in order to write this test.

It would be better for this dependency to be passed to the reducer so that we can use a live dependency when running the application on a device, but use a mocked dependency for tests. We can do this by adding a property to the Feature reducer:

@Reducer
struct Feature {
  let numberFact: (Int) async throws -> String
  // ...
}

Then we can use it in the reduce implementation:

case .numberFactButtonTapped:
  return .run { [count = state.count] send in 
    let fact = try await self.numberFact(count)
    await send(.numberFactResponse(fact))
  }

And in the entry point of the application we can provide a version of the dependency that actually interacts with the real world API server:

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      FeatureView(
        store: Store(initialState: Feature.State()) {
          Feature(
            numberFact: { number in
              let (data, _) = try await URLSession.shared.data(
                from: URL(string: "http://numbersapi.com/\(number)")!
              )
              return String(decoding: data, as: UTF8.self)
            }
          )
        }
      )
    }
  }
}

But in tests we can use a mock dependency that immediately returns a deterministic, predictable fact:

@MainActor
func testFeature() async {
  let store = TestStore(initialState: Feature.State()) {
    Feature(numberFact: { "\($0) is a good number Brent" })
  }
}

With that little bit of upfront work we can finish the test by simulating the user tapping on the fact button, and thenreceiving the response from the dependency to present the fact:

await store.send(.numberFactButtonTapped)

await store.receive(\.numberFactResponse) {
  $0.numberFact = "0 is a good number Brent"
}

We can also improve the ergonomics of using the numberFact dependency in our application. Over time the application may evolve into many features, and some of those features may also want access to numberFact, and explicitly passing it through all layers can get annoying. There is a process you can follow to “register” dependencies with the library, making them instantly available to any layer in the application.

Note

For more in-depth information on dependency management, see the dedicated dependencies article.

We can start by wrapping the number fact functionality in a new type:

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

And then registering that type with the dependency management system by conforming the client to the DependencyKey protocol, which requires you to specify the live value to use when running the application in simulators or devices:

extension NumberFactClient: DependencyKey {
  static let liveValue = Self(
    fetch: { number in
      let (data, _) = try await URLSession.shared
        .data(from: URL(string: "http://numbersapi.com/\(number)")!
      )
      return String(decoding: data, as: UTF8.self)
    }
  )
}

extension DependencyValues {
  var numberFact: NumberFactClient {
    get { self[NumberFactClient.self] }
    set { self[NumberFactClient.self] = newValue }
  }
}

With that little bit of upfront work done you can instantly start making use of the dependency in any feature by using the @Dependency property wrapper:

 @Reducer
 struct Feature {
-  let numberFact: (Int) async throws -> String
+  @Dependency(\.numberFact) var numberFact-  try await self.numberFact(count)
+  try await self.numberFact.fetch(count)
 }

This code works exactly as it did before, but you no longer have to explicitly pass the dependency when constructing the feature's reducer. When running the app in previews, the simulator or on a device, the live dependency will be provided to the reducer, and in tests the test dependency will be provided.

This means the entry point to the application no longer needs to construct dependencies:

@main
struct MyApp: App {
  var body: some Scene {
    WindowGroup {
      FeatureView(
        store: Store(initialState: Feature.State()) {
          Feature()
        }
      )
    }
  }
}

And the test store can be constructed without specifying any dependencies, but you can still override any dependency you need to for the purpose of the test:

let store = TestStore(initialState: Feature.State()) {
  Feature()
} withDependencies: {
  $0.numberFact.fetch = { "\($0) is a good number Brent" }
}

// ...

That is the basics of building and testing a feature in the Composable Architecture. There are a lot more things to be explored, such as composition, modularity, adaptability, and complex effects. The Examples directory has a bunch of projects to explore to see more advanced usages.

Documentation

The documentation for releases and main are available here:

Other versions

There are a number of articles in the documentation that you may find helpful as you become more comfortable with the library:

Community

If you want to discuss the Composable Architecture or have a question about how to use it to solve a particular problem, there are a number of places you can discuss with fellow Point-Free enthusiasts:

Installation

You can add ComposableArchitecture to an Xcode project by adding it as a package dependency.

  1. From the File menu, select Add Package Dependencies...
  2. Enter "https://github.com/pointfreeco/swift-composable-architecture" into the package repository URL text field
  3. Depending on how your project is structured:
    • If you have a single application target that needs access to the library, then add ComposableArchitecture directly to your application.
    • If you want to use this library from multiple Xcode targets, or mix Xcode targets and SPM targets, you must create a shared framework that depends on ComposableArchitecture and then depend on that framework in all of your targets. For an example of this, check out the Tic-Tac-Toe demo application, which splits lots of features into modules and consumes the static library in this fashion using the tic-tac-toe Swift package.

Companion libraries

The Composable Architecture is built with extensibility in mind, and there are a number of community-supported libraries available to enhance your applications:

If you'd like to contribute a library, please open a PR with a link to it!

Translations

The following translations of this README have been contributed by members of the community:

If you'd like to contribute a translation, please open a PR with a link to a Gist!

FAQ

  • How does the Composable Architecture compare to Elm, Redux, and others?

    Expand to see answer The Composable Architecture (TCA) is built on a foundation of ideas popularized by the Elm Architecture (TEA) and Redux, but made to feel at home in the Swift language and on Apple's platforms.

    In some ways TCA is a little more opinionated than the other libraries. For example, Redux is not prescriptive with how one executes side effects, but TCA requires all side effects to be modeled in the Effect type and returned from the reducer.

    In other ways TCA is a little more lax than the other libraries. For example, Elm controls what kinds of effects can be created via the Cmd type, but TCA allows an escape hatch to any kind of effect since Effect wraps around an async operation.

    And then there are certain things that TCA prioritizes highly that are not points of focus for Redux, Elm, or most other libraries. For example, composition is very important aspect of TCA, which is the process of breaking down large features into smaller units that can be glued together. This is accomplished with reducer builders and operators like Scope, and it aids in handling complex features as well as modularization for a better-isolated code base and improved compile times.

Credits and thanks

The following people gave feedback on the library at its early stages and helped make the library what it is today:

Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doležal, Eimantas, Matthew Johnson, George Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt, Kyle Sherman, Petr Šíma, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, and all of the Point-Free subscribers 😁.

Special thanks to Chris Liscio who helped us work through many strange SwiftUI quirks and helped refine the final API.

And thanks to Shai Mishali and the CombineCommunity project, from which we took their implementation of Publishers.Create, which we use in Effect to help bridge delegate and callback-based APIs, making it much easier to interface with 3rd party frameworks.

Other libraries

The Composable Architecture was built on a foundation of ideas started by other libraries, in particular Elm and Redux.

There are also many architecture libraries in the Swift and iOS community. Each one of these has their own set of priorities and trade-offs that differ from the Composable Architecture.

License

This library is released under the MIT license. See LICENSE for details.