logo
hero image

The Composable Architecture

Redux ❤️ Swift = TCA
17 March 2023 · 8 Minutes

Apa itu TCA ?

The Composable Architecture (TCA) adalah sebuah library Swift untuk membangun aplikasi dengan cara yang konsisten dan mudah dipahami, dengan mempertimbangkan Composition, Testing, dan Ergonomics.

TCA bisa digunakan di SwiftUI maupun UIKit, dan semua platform Apple seperti iOS, macOS, tvOS, dan watchOS.

Jika kamu pernah menggunakan library Redux sebelumnya, secara konsep, TCA ini sebenarnya hampir sama dengan Redux!

Mengapa menggunakan TCA ?

TCA menyelesaikan beberapa masalah yang mungkin sering kita hadapi dalam membangun aplikasi iOS, seperti:

State Management

State Management adalah tentang bagaimana kita memanajemen state pada aplikasi kita, dan membagikannya ke banyak halaman pada aplikasi kita, jadi jika kita mengubah state pada suatu halaman, maka halaman lain yang menggunakan state tersebut juga akan berubah.

Composition

Composition adalah tentang bagaimana kita memecah fitur yang besar menjadi komponen-komponen kecil, modul-modul yang terisolasi, dan mudah untuk digabungkan kembali menjadi satu fitur yang utuh.

Side Effects

Side Effects adalah tentang bagaimana aplikasi kita berinteraksi dengan dunia luar seperti database atau API. nah dengan TCA, kita bisa memanajemen Side Effect agar mudah untuk ditest dan dipahami.

Testing

TCA memungkinkan kita untuk membuat Unit, Integration, bahkan End-to-End (E2E) tests untuk memastikan bahwa aplikasi kita berjalan dengan baik.

Ergonomics

Ergonomics adalah tentang bagaimana kita bisa menyelesaikan semua hal di atas dengan cara semudah mungkin.


6 komponen TCA

Diagram TCADiagram TCA

Berikut adalah 5 komponen yang ada dalam library the Composable Architecture:

State

State adalah sebuah type yang mendeskripsikan data dari aplikasi kita seperti data count, username, dll.

Berikut adalah contoh untuk membuat State pada TCA:

State
icon
1struct State {
2 var count: Int = 0
3}

Action

Action adalah sebuah type yang merepresentasikan semua aksi atau event yang terjadi pada aplikasi kita seperti jika button diklik, fetching data ke API, dll.

Berikut adalah contoh untuk membuat Action pada TCA:

Action
icon
1enum Action: Equatable {
2 case incrementButtonTapped
3 case decrementButtonTapped
4}

Reducer

Reducer adalah sebuah function yang bertugas untuk mengubah State kita berdasarkan tipe Action yang dikirim. Reducer juga bertugas untuk me-return Effect jika diperlukan.

Berikut adalah contoh untuk membuat Reducer pada TCA:

Reducer
icon
1struct CounterReducer: ReducerProtocol {
2 struct State: Equatable {
3 var count = 0
4 }
5
6 enum Action: Equatable {
7 case incrementButtonTapped
8 case decrementButtonTapped
9 }
10
12 func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
13 switch action {
14 case .decrementButtonTapped:
15 state.count -= 1
16 return .none
17
18 case .incrementButtonTapped:
19 state.count += 1
20 return .none
21 }
22 }
24}

.none artinya tidak ada Effect yang ingin dijalankan pada action tersebut.

Effect

Effect adalah cara kita berinteraksi dengan dunia luar seperti database dan API. Di dalam Effect, kita juga bisa mengirimkan Action jika diperlukan.

Ada 2 cara untuk membuat Effect:

  1. Menggunakan Swift Structured Concurreny

  1. Menggunakan framework Combine Jika kita menggunakan Combine, maka kita bisa membuat Effect dengan menggunakan operator-operator yang ada di Combine, kemudian kita lakukan type erasure menjadi EffectPublisher menggunakan method eraseToEffect atau catchToEffect.

Berikut adalah contoh untuk membuat Effect pada TCA:

Effect
icon
1func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
2 switch action {
3 case .decrementButtonTapped:
4 state.count -= 1
6 return .fireAndForget {
7 print("State count decremented by -1")
8 }
10 }
11}

Dependency

Dependency adalah segala hal yang kita gunakan dari dunia luar yang tidak bisa kita kontrol. Contohnya seperti API Request, menyimpan data ke Database, dll.

TCA memungkinkan kita untuk mengontrol Dependency pada aplikasi kita, jadi kita bisa melakukan mocking ketika menulis Unit Tests.

Untuk membuat Dependency, kita tinggal melakukan extension pada DependencyValues.

Berikut adalah contoh untuk membuat Dependency pada TCA:

Dependency
icon
1private enum UserDefaultsKey: DependencyKey {
2 static let liveValue = UserDefaults.standard
3
4 static let testValue = {
5 let userDefaults = UserDefaults()
6 userDefaults.removePersistentDomain(forName: "test")
7 return userDefaults
8 }()
9}
10
11extension DependencyValues {
12 var userDefaults: UserDefaults {
13 get { self[UserDefaultsKey.self] }
14 set { self[UserDefaultsKey.self] = newValue }
15 }
16}

liveValue adalah value yang akan digunakan pada saat aplikasi kita dijalankan, sedangkan mockValue adalah value yang akan digunakan pada saat testing. selain itu, ada juga previewValue yang akan digunakan pada SwiftUI Preview.

Untuk menggunakan Dependency yang telah kita buat di atas, caranya seperti ini:

CounterReducer
icon
1struct CounterReducer: ReducerProtocol {
3 @Dependency(\.userDefaults) var userDefaults
4 ...
5 func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
6 switch action {
7 case .decrementButtonTapped:
8 state.count -= 1
9 return .fireAndForget { [state] in
10 print("State count decremented by -1")
12 self.userDefaults.setValue(state.count, forKey: "count")
13 }
14 ...

Store

Store adalah hasil kombinasi dari semua komponen di atas, Jika kita akan mengirim Action, kita kirim ke Store, atau jika kita ingin meng-observe perubahan State, juga kita observe ke Store.

Berikut adalah contoh untuk membuat Store pada TCA:

Store
icon
1let store = Store(
2 initialState: CounterReducer.State(),
3 reducer: CounterReducer()
4)

Instalasi TCA

Kita bisa menginstal the Composable Architecture dengan cara:

  1. Klik menu "File", kemudian pilih "Add Packages"

  2. Lalu masukkan "https://github.com/pointfreeco/swift-composable-architecture"

  3. Pilih versi "0.52.0"

Menggunakan TCA di SwiftUI

Misalnya kita punya CounterReducer seperti ini:

CounterReducer.swift
icon
1import Foundation
2import ComposableArchitecture
3
4struct CounterReducer: ReducerProtocol {
5 @Dependency(\.userDefaults) var userDefaults
6
7 struct State: Equatable {
8 var count = 0
9 }
10
11 enum Action: Equatable {
12 case initializeState
13 case incrementButtonTapped
14 case decrementButtonTapped
15 }
16
17 func reduce(into state: inout State, action: Action) -> EffectTask<Action> {
18 switch action {
19 case .initializeState:
20 state.count = self.userDefaults.integer(forKey: "count")
21 return .none
22
23 case .decrementButtonTapped:
24 state.count -= 1
25 return .fireAndForget { [state] in
26 print("State count decremented by -1")
27 self.userDefaults.setValue(state.count, forKey: "count")
28 }
29
30 case .incrementButtonTapped:
31 state.count += 1
32 return .fireAndForget { [state] in
33 print("State count incremented by 1")
34 self.userDefaults.setValue(state.count, forKey: "count")
35 }
36 }
37 }
38}
39
40
41// MARK: Dependencies
42private enum UserDefaultsKey: DependencyKey {
43 static let liveValue = UserDefaults.standard
44
45 static let testValue = {
46 let userDefaults = UserDefaults()
47 userDefaults.removePersistentDomain(forName: "test")
48 return userDefaults
49 }()
50}
51
52extension DependencyValues {
53 var userDefaults: UserDefaults {
54 get { self[UserDefaultsKey.self] }
55 set { self[UserDefaultsKey.self] = newValue }
56 }
57}

Dan berikut adalah contoh code untuk menggunakan CounterReducer di SwiftUI:

ContentView.swift
icon
1import SwiftUI
2import ComposableArchitecture
3
4struct ContentView: View {
5 let store = Store(
6 initialState: CounterReducer.State(),
7 reducer: CounterReducer()
8 )
9
10 var body: some View {
11 WithViewStore(store) { viewStore in
12 HStack {
13 Button("-") { viewStore.send(.decrementButtonTapped) }
14 Text("\(viewStore.count)")
15 Button("+") { viewStore.send(.incrementButtonTapped) }
16 }
17 .onAppear {
18 viewStore.send(.initializeState)
19 }
20 }
21 }
22}

Kita tidak bisa menggunakan variabel store secara langsung, namun perlu membungkus view kita menggunakan WithViewStore agar jika state count berubah, view kita juga akan berubah secara otomatis.

Menggunakan TCA di UIKit

Untuk menggunakan TCA di UIKit, caranya seperti ini:

ViewController.swift
icon
1import UIKit
2import Combine
3import ComposableArchitecture
4
5class ViewController: UIViewController {
6 let viewStore: ViewStoreOf<CounterReducer> = ViewStore(
7 Store(
8 initialState: CounterReducer.State(),
9 reducer: CounterReducer()
10 )
11 )
12 var cancellables: Set<AnyCancellable> = []
13
14 let countLabel = UILabel()
15 let decrementButton: UIButton = {
16 let button = UIButton()
17 button.setTitle("-", for: .normal)
18 return button
19 }()
20 let incrementButton: UIButton = {
21 let button = UIButton()
22 button.setTitle("+", for: .normal)
23 return button
24 }()
25 let stackView: UIStackView = {
26 let stackView = UIStackView()
27 stackView.axis = .horizontal
28 return stackView
29 }()
30
31
32 override func loadView() {
33 super.loadView()
34
35 setupUI()
36 setupConstraints()
37 setupHandlers()
38 }
39
40 override func viewDidLoad() {
41 super.viewDidLoad()
42
43 viewStore.send(.initializeState)
44 }
45
46 private func setupUI() {
47 stackView.addArrangedSubview(decrementButton)
48 stackView.addArrangedSubview(countLabel)
49 stackView.addArrangedSubview(incrementButton)
50 self.view.addSubview(stackView)
51 }
52
53 private func setupConstraints() {
54 stackView.translatesAutoresizingMaskIntoConstraints = false
55 NSLayoutConstraint.activate([
56 stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
57 stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)
58 ])
59 }
60
61 private func setupHandlers() {
62 self.decrementButton.addTarget(self, action: #selector(decrementButtonTapped), for: .touchUpInside)
63 self.incrementButton.addTarget(self, action: #selector(incrementButtonTapped), for: .touchUpInside)
64
65 self.viewStore.publisher
66 .map { "\($0.count)" }
67 .assign(to: \.text, on: countLabel)
68 .store(in: &self.cancellables)
69 }
70
71 @objc private func decrementButtonTapped() {
72 self.viewStore.send(.decrementButtonTapped)
73 }
74
75 @objc private func incrementButtonTapped() {
76 self.viewStore.send(.incrementButtonTapped)
77 }
78}

Kita memerlukan framework Combine agar jika terjadi perubahan pada state count, maka label countLabel juga akan ikut berubah.

icon
1self.viewStore.publisher
2 .map { "\($0.count)" }
3 .assign(to: \.text, on: countLabel)
4 .store(in: &self.cancellables)

Testing

Untuk melakukan Testing di TCA itu sangat mudah, caranya seperti ini:

Testing
icon
1import XCTest
2import ComposableArchitecture
3@testable import MyApp
4
5final class MyAppTests: XCTestCase {
6 func test_decrementButtonTapped() {
7 let sut = TestStore(
8 initialState: CounterReducer.State(),
9 reducer: CounterReducer()
10 )
11
12 sut.send(.decrementButtonTapped) {
13 $0.count = -1
14 }
15 }
16
17 func test_incrementButtonTapped() {
18 let sut = TestStore(
19 initialState: CounterReducer.State(),
20 reducer: CounterReducer()
21 )
22
23 sut.send(.incrementButtonTapped) {
24 $0.count = 1
25 }
26 }
27}

Trailing closure pada method send berisi kondisi apa saja yang akan menyebabkan test kita berhasil.

Dan apabila test kita gagal, pesan errornya juga sangat readable, ini salah satu yang saya suka dari TCA ❤️

Test FailedTest Failed


Jika kamu tertarik dan ingin mempelajari lebih dalam mengenai TCA, seperti cara Sharing State, Scoping, melakukan Binding, integrasi dengan WebSocket, dll. Kamu bisa membaca dokumentasinya di sini: https://github.com/pointfreeco/swift-composable-architecture atau melihat studi kasus penggunaan TCA di sini: https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples/CaseStudies

Oke mungkin itu saja yang bisa saya bagikan kali ini, kalau kamu merasa artikel ini bermanfaat silakan Like & Share artikel ini ke teman-teman kamu atau jika kamu punya pertanyaan, tulis aja di kolom komentar, Thank you! 😁 🙏

iOS Development
Swift
SwiftUI
UIKit
The Composable Architecture

Written by :
Alfin Syahruddin
Developer · Stock Trader · Libertarian · Freethinker

Always open to new ideas. 🕊️

Loading...

Related articles

Articles that you might want to read.

hero image
CI/CD aplikasi iOS dengan Fastlane dan Github Actions

Otomatisasi build, testing, screenshot, dan deployment aplikasi iOS ke Testflight & AppStore.

24 September 2023 · 9 Minutes
iOS Development
CI/CD
Fastlane
Github Actions
hero image
Membuat Form dinamis pada React JS

Tanpa memanipulasi Javascript DOM

22 October 2019 · 6 Minutes
React
Web Development
hero image
CI/CD aplikasi iOS dengan Xcode Cloud

Otomatisasi build, testing, dan deployment aplikasi iOS ke Testflight & AppStore.

27 January 2024 · 8 Minutes
iOS Development
CI/CD
Xcode Cloud
All rights reserved © Alfin Syahruddin · 2019
RSS Feed