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!
TCA menyelesaikan beberapa masalah yang mungkin sering kita hadapi dalam membangun aplikasi iOS, seperti:
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 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 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.
TCA memungkinkan kita untuk membuat Unit, Integration, bahkan End-to-End (E2E) tests untuk memastikan bahwa aplikasi kita berjalan dengan baik.
Ergonomics adalah tentang bagaimana kita bisa menyelesaikan semua hal di atas dengan cara semudah mungkin.
Berikut adalah 5 komponen yang ada dalam library the Composable Architecture:
State adalah sebuah type yang mendeskripsikan data dari aplikasi kita seperti data count, username, dll.
Berikut adalah contoh untuk membuat State pada TCA:
State1struct State {2 var count: Int = 03}
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:
Action1enum Action: Equatable {2 case incrementButtonTapped3 case decrementButtonTapped4}
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:
Reducer1struct CounterReducer: ReducerProtocol {2 struct State: Equatable {3 var count = 04 }56 enum Action: Equatable {7 case incrementButtonTapped8 case decrementButtonTapped9 }1012 func reduce(into state: inout State, action: Action) -> EffectTask<Action> {13 switch action {14 case .decrementButtonTapped:15 state.count -= 116 return .none1718 case .incrementButtonTapped:19 state.count += 120 return .none21 }22 }24}
.none artinya tidak ada Effect yang ingin dijalankan pada action tersebut.
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:
Menggunakan Swift Structured Concurreny
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.
Baca juga: 3 Pilar Framework Combine
Berikut adalah contoh untuk membuat Effect pada TCA:
Effect1func reduce(into state: inout State, action: Action) -> EffectTask<Action> {2 switch action {3 case .decrementButtonTapped:4 state.count -= 16 return .fireAndForget {7 print("State count decremented by -1")8 }10 }11}
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:
Dependency1private enum UserDefaultsKey: DependencyKey {2 static let liveValue = UserDefaults.standard34 static let testValue = {5 let userDefaults = UserDefaults()6 userDefaults.removePersistentDomain(forName: "test")7 return userDefaults8 }()9}1011extension 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:
CounterReducer1struct CounterReducer: ReducerProtocol {3 @Dependency(\.userDefaults) var userDefaults4 ...5 func reduce(into state: inout State, action: Action) -> EffectTask<Action> {6 switch action {7 case .decrementButtonTapped:8 state.count -= 19 return .fireAndForget { [state] in10 print("State count decremented by -1")12 self.userDefaults.setValue(state.count, forKey: "count")13 }14 ...
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:
Store1let store = Store(2 initialState: CounterReducer.State(),3 reducer: CounterReducer()4)
Kita bisa menginstal the Composable Architecture dengan cara:
Klik menu "File", kemudian pilih "Add Packages"
Lalu masukkan "https://github.com/pointfreeco/swift-composable-architecture"
Pilih versi "0.52.0"
Misalnya kita punya CounterReducer seperti ini:
CounterReducer.swift1import Foundation2import ComposableArchitecture34struct CounterReducer: ReducerProtocol {5 @Dependency(\.userDefaults) var userDefaults67 struct State: Equatable {8 var count = 09 }1011 enum Action: Equatable {12 case initializeState13 case incrementButtonTapped14 case decrementButtonTapped15 }1617 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 .none2223 case .decrementButtonTapped:24 state.count -= 125 return .fireAndForget { [state] in26 print("State count decremented by -1")27 self.userDefaults.setValue(state.count, forKey: "count")28 }2930 case .incrementButtonTapped:31 state.count += 132 return .fireAndForget { [state] in33 print("State count incremented by 1")34 self.userDefaults.setValue(state.count, forKey: "count")35 }36 }37 }38}394041// MARK: Dependencies42private enum UserDefaultsKey: DependencyKey {43 static let liveValue = UserDefaults.standard4445 static let testValue = {46 let userDefaults = UserDefaults()47 userDefaults.removePersistentDomain(forName: "test")48 return userDefaults49 }()50}5152extension 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.swift1import SwiftUI2import ComposableArchitecture34struct ContentView: View {5 let store = Store(6 initialState: CounterReducer.State(),7 reducer: CounterReducer()8 )910 var body: some View {11 WithViewStore(store) { viewStore in12 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.
Untuk menggunakan TCA di UIKit, caranya seperti ini:
ViewController.swift1import UIKit2import Combine3import ComposableArchitecture45class ViewController: UIViewController {6 let viewStore: ViewStoreOf<CounterReducer> = ViewStore(7 Store(8 initialState: CounterReducer.State(),9 reducer: CounterReducer()10 )11 )12 var cancellables: Set<AnyCancellable> = []1314 let countLabel = UILabel()15 let decrementButton: UIButton = {16 let button = UIButton()17 button.setTitle("-", for: .normal)18 return button19 }()20 let incrementButton: UIButton = {21 let button = UIButton()22 button.setTitle("+", for: .normal)23 return button24 }()25 let stackView: UIStackView = {26 let stackView = UIStackView()27 stackView.axis = .horizontal28 return stackView29 }()303132 override func loadView() {33 super.loadView()3435 setupUI()36 setupConstraints()37 setupHandlers()38 }3940 override func viewDidLoad() {41 super.viewDidLoad()4243 viewStore.send(.initializeState)44 }4546 private func setupUI() {47 stackView.addArrangedSubview(decrementButton)48 stackView.addArrangedSubview(countLabel)49 stackView.addArrangedSubview(incrementButton)50 self.view.addSubview(stackView)51 }5253 private func setupConstraints() {54 stackView.translatesAutoresizingMaskIntoConstraints = false55 NSLayoutConstraint.activate([56 stackView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),57 stackView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor)58 ])59 }6061 private func setupHandlers() {62 self.decrementButton.addTarget(self, action: #selector(decrementButtonTapped), for: .touchUpInside)63 self.incrementButton.addTarget(self, action: #selector(incrementButtonTapped), for: .touchUpInside)6465 self.viewStore.publisher66 .map { "\($0.count)" }67 .assign(to: \.text, on: countLabel)68 .store(in: &self.cancellables)69 }7071 @objc private func decrementButtonTapped() {72 self.viewStore.send(.decrementButtonTapped)73 }7475 @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.
1self.viewStore.publisher2 .map { "\($0.count)" }3 .assign(to: \.text, on: countLabel)4 .store(in: &self.cancellables)
Untuk melakukan Testing di TCA itu sangat mudah, caranya seperti ini:
Testing1import XCTest2import ComposableArchitecture3@testable import MyApp45final class MyAppTests: XCTestCase {6 func test_decrementButtonTapped() {7 let sut = TestStore(8 initialState: CounterReducer.State(),9 reducer: CounterReducer()10 )1112 sut.send(.decrementButtonTapped) {13 $0.count = -114 }15 }1617 func test_incrementButtonTapped() {18 let sut = TestStore(19 initialState: CounterReducer.State(),20 reducer: CounterReducer()21 )2223 sut.send(.incrementButtonTapped) {24 $0.count = 125 }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 ❤️
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! 😁 🙏
Always open to new ideas. 🕊️
Articles that you might want to read.
Otomatisasi build, testing, screenshot, dan deployment aplikasi iOS ke Testflight & AppStore.
Tanpa memanipulasi Javascript DOM
Otomatisasi build, testing, dan deployment aplikasi iOS ke Testflight & AppStore.