Composable Architecture (TCA, singkatnya) adalah perpustakaan untuk membangun aplikasi dengan cara yang konsisten dan mudah dipahami, dengan mempertimbangkan komposisi, pengujian, dan ergonomi. Ini dapat digunakan di SwiftUI, UIKit, dan lainnya, dan di platform Apple apa pun (iOS, macOS, visionOS, tvOS, dan watchOS).
Apa itu Arsitektur Composable?
Pelajari lebih lanjut
Contoh
Penggunaan dasar
Dokumentasi
Masyarakat
Instalasi
Terjemahan
Pustaka ini menyediakan beberapa alat inti yang dapat digunakan untuk membangun aplikasi dengan berbagai tujuan dan kompleksitas. Ini memberikan cerita menarik yang dapat Anda ikuti untuk memecahkan banyak masalah yang Anda temui sehari-hari saat membangun aplikasi, seperti:
Manajemen negara
Cara mengelola status aplikasi Anda menggunakan tipe nilai sederhana, dan berbagi status di banyak layar sehingga mutasi di satu layar dapat langsung diamati di layar lain.
Komposisi
Cara memecah fitur-fitur besar menjadi komponen-komponen lebih kecil yang dapat diekstraksi ke modul-modulnya sendiri yang terisolasi dan dengan mudah direkatkan kembali untuk membentuk fitur tersebut.
Efek samping
Bagaimana membiarkan bagian-bagian tertentu dari aplikasi berkomunikasi dengan dunia luar dengan cara yang paling dapat diuji dan dimengerti.
Pengujian
Caranya tidak hanya menguji fitur yang dibangun dalam arsitektur, tetapi juga menulis pengujian integrasi untuk fitur yang telah terdiri dari banyak bagian, dan menulis pengujian end-to-end untuk memahami bagaimana efek samping memengaruhi aplikasi Anda. Hal ini memungkinkan Anda memberikan jaminan kuat bahwa logika bisnis Anda berjalan sesuai harapan.
Ergonomi
Cara mencapai semua hal di atas dalam API sederhana dengan konsep dan bagian bergerak sesedikit mungkin.
Arsitektur Composable dirancang selama banyak episode di Point-Free, serial video yang mengeksplorasi pemrograman fungsional dan bahasa Swift, dipandu oleh Brandon Williams dan Stephen Celis.
Anda dapat menonton semua episodenya di sini, serta tur arsitektur multi-bagian yang berdedikasi dari awal.
Repo ini dilengkapi dengan banyak contoh untuk menunjukkan cara memecahkan masalah umum dan kompleks dengan Arsitektur Composable. Kunjungi direktori ini untuk melihat semuanya, termasuk:
Studi Kasus
Memulai
Efek
Navigasi
Pereduksi tingkat tinggi
Komponen yang dapat digunakan kembali
Manajer lokasi
Manajer gerak
Mencari
Pengenalan Ucapan
aplikasi SyncUp
Tic-Tac-Toe
Semua
Memo suara
Mencari sesuatu yang lebih substansial? Lihat kode sumber untuk isowords, permainan pencarian kata iOS yang dibangun di SwiftUI dan Arsitektur Composable.
Catatan
Untuk tutorial interaktif langkah demi langkah, pastikan untuk membaca Temui Arsitektur yang Dapat Dikomposisi.
Untuk membuat fitur menggunakan Arsitektur Composable, Anda menentukan beberapa jenis dan nilai yang memodelkan domain Anda:
Status : Jenis yang mendeskripsikan data yang dibutuhkan fitur Anda untuk menjalankan logikanya dan merender UI-nya.
Tindakan : Jenis yang mewakili semua tindakan yang dapat terjadi di fitur Anda, seperti tindakan pengguna, notifikasi, sumber peristiwa, dan lainnya.
Peredam : Fungsi yang menjelaskan cara mengembangkan status aplikasi saat ini ke status berikutnya jika diberi tindakan. Peredam juga bertanggung jawab untuk mengembalikan efek apa pun yang harus dijalankan, seperti permintaan API, yang dapat dilakukan dengan mengembalikan nilai Effect
.
Store : Waktu proses yang benar-benar menggerakkan fitur Anda. Anda mengirimkan semua tindakan pengguna ke toko sehingga toko dapat menjalankan peredam dan efek, dan Anda dapat mengamati perubahan status di toko sehingga Anda dapat memperbarui UI.
Manfaat melakukan hal ini adalah Anda akan langsung membuka kemampuan pengujian fitur Anda, dan Anda akan dapat memecah fitur yang besar dan kompleks menjadi domain yang lebih kecil yang dapat direkatkan.
Sebagai contoh dasar, pertimbangkan UI yang menampilkan angka beserta tombol "+" dan "−" yang menambah dan mengurangi angka tersebut. Yang menarik, misalkan ada juga tombol yang ketika diketuk akan membuat permintaan API untuk mengambil fakta acak tentang nomor tersebut dan menampilkannya dalam tampilan.
Untuk mengimplementasikan fitur ini, kami membuat tipe baru yang akan menampung domain dan perilaku fitur tersebut, dan akan dianotasi dengan makro @Reducer
:
impor Fitur ComposableArchitecture@Reducerstruct {}
Di sini kita perlu mendefinisikan tipe status fitur, yang terdiri dari bilangan bulat untuk hitungan saat ini, serta string opsional yang mewakili fakta yang disajikan:
@Reducerstruct Feature { @ObservableState struct Status: Equatable { var count = 0 var numberFact: String? }}
Catatan
Kami telah menerapkan makro @ObservableState
ke State
untuk memanfaatkan alat observasi di perpustakaan.
Kita juga perlu menentukan tipe tindakan fitur tersebut. Ada tindakan yang jelas, seperti mengetuk tombol pengurangan, tombol penambahan, atau tombol fakta. Namun ada juga beberapa yang agak tidak jelas, seperti tindakan yang terjadi saat kita menerima respons dari permintaan API fakta:
@Reducerstruct Feature { @ObservableState struct Status: Equatable { /* ... */ } enum Tindakan { case decrementButtonTapped case incrementButtonTapped case numberFactButtonTapped case numberFactResponse(String) }}
Lalu kita mengimplementasikan properti body
, yang bertanggung jawab untuk menyusun logika dan perilaku aktual untuk fitur tersebut. Di dalamnya kita dapat menggunakan Reducer Reduce
untuk menjelaskan cara mengubah keadaan saat ini ke keadaan berikutnya, dan efek apa yang perlu dijalankan. Beberapa tindakan tidak perlu menjalankan efek, dan tindakan tersebut dapat mengembalikan .none
untuk menyatakan hal tersebut:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: some Reducer{ Kurangi { status, tindakan dalam beralih tindakan { kasus .decrementButtonTapped: state.count -= 1 kembalikan .none case .incremenButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] kirim masuk let (data, _) = coba tunggu URLSession.shared.data( dari: URL(string: "http://numbersapi.com/(count)/trivia")! ) menunggu kirim( .numberFactResponse(String(decoding: data, sebagai: UTF8.self)) ) } case let .numberFactResponse(fakta): state.numberFact = fakta kembali .none } } }}
Dan terakhir kita menentukan tampilan yang menampilkan fitur tersebut. Ia menyimpan StoreOf
sehingga dapat mengamati semua perubahan pada status dan merender ulang, dan kami dapat mengirimkan semua tindakan pengguna ke penyimpanan sehingga status berubah:
struct FeatureView: Lihat { biarkan menyimpan: StoreOfvar tubuh: beberapa Lihat { Formulir { Bagian { Teks("(store.count)") Tombol("Pengurangan") { toko.kirim(.decrementButtonTapped) } Tombol( "Peningkatan") { store.send(.incrementButtonTapped) } } Bagian { Tombol("Nomor fakta") { store.send(.numberFactButtonTapped) } } jika biarkan fakta = store.numberFact { Teks(fakta) } } }}
Juga mudah untuk mengeluarkan pengontrol UIKit dari toko ini. Anda dapat mengamati perubahan status di penyimpanan di viewDidLoad
, lalu mengisi komponen UI dengan data dari penyimpanan. Kode ini sedikit lebih panjang dari versi SwiftUI, jadi kami telah menciutkannya di sini:
class FeatureViewController: UIViewController { biarkan menyimpan: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, bundle: nil) } diperlukan init?(coder: NSCoder) { fatalError("init(coder:) belum diterapkan") } override func viewDidLoad() { super.viewDidLoad() biarkan countLabel = UILabel() biarkan decrementButton = UIButton() biarkan incrementButton = UIButton() biarkan factLabel = UILabel() // Dihilangkan: Tambahkan subview dan atur batasan... amati { [diri yang lemah] di jaga biarkan diri lain { kembali } countLabel.text = "(self.store.text)" factLabel.text = self.store.numberFact } } @objc private func incrementButtonTapped() { self.store.send(.inriceButtonTapped) } @objc private func decrementButtonTapped() { self.store.send(.decrementButtonTapped) } @objc private func factButtonTapped() { self.store.send(.numberFactButtonTapped) }}
Setelah kita siap menampilkan tampilan ini, misalnya di titik masuk aplikasi, kita dapat membuat toko. Hal ini dapat dilakukan dengan menentukan keadaan awal untuk memulai aplikasi, serta peredam yang akan memberi daya pada aplikasi:
import ComposableArchitecture@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( toko: Toko(negara awal: Fitur.Negara()) { Fitur() } ) } }}
Dan itu cukup untuk menampilkan sesuatu di layar untuk dimainkan. Ini jelas merupakan beberapa langkah lebih banyak dibandingkan jika Anda melakukan ini dengan cara vanilla SwiftUI, tetapi ada beberapa manfaatnya. Ini memberi kita cara yang konsisten untuk menerapkan mutasi keadaan, alih-alih menyebarkan logika di beberapa objek yang dapat diamati dan dalam berbagai tindakan penutupan komponen UI. Ini juga memberi kita cara ringkas untuk mengungkapkan efek samping. Dan kita bisa langsung menguji logika ini, termasuk efeknya, tanpa melakukan banyak pekerjaan tambahan.
Catatan
Untuk informasi lebih mendalam tentang pengujian, lihat artikel pengujian khusus.
Untuk mengujinya, gunakan TestStore
, yang dapat dibuat dengan informasi yang sama seperti Store
, namun melakukan pekerjaan ekstra untuk memungkinkan Anda menegaskan bagaimana fitur Anda berkembang seiring dengan tindakan yang dikirim:
@Testfunc basics() async { biarkan toko = TestStore(initialState: Feature.State()) { Feature() }}
Setelah penyimpanan pengujian dibuat, kita dapat menggunakannya untuk membuat pernyataan tentang seluruh alur langkah pengguna. Setiap langkah yang kita perlukan untuk membuktikan bahwa keadaan berubah sesuai harapan kita. Misalnya, kita dapat menyimulasikan alur pengguna saat mengetuk tombol kenaikan dan penurunan:
// Uji apakah mengetuk tombol kenaikan/penurunan akan mengubah countawait store.send(.incremenButtonTapped) { $0.count = 1}menunggu toko.kirim(.decrementButtonTapped) { $0.hitungan = 0}
Selanjutnya, jika suatu langkah menyebabkan efek dieksekusi, yang mengembalikan data ke penyimpanan, kita harus menegaskan hal itu. Misalnya, jika kita menyimulasikan pengguna yang mengetuk tombol fakta, kita berharap menerima respons fakta kembali dengan fakta, yang kemudian menyebabkan status numberFact
terisi:
tunggu toko.kirim(.numberFactButtonTapped)tunggu toko.terima(.numberFactResponse) { $0.angkaFakta = ???}
Namun, bagaimana kita tahu fakta apa yang akan dikirimkan kembali kepada kita?
Saat ini peredam kami menggunakan efek yang menjangkau dunia nyata untuk menyerang server API, dan itu berarti kami tidak memiliki cara untuk mengontrol perilakunya. Kami bergantung pada konektivitas internet dan ketersediaan server API untuk menulis tes ini.
Akan lebih baik jika ketergantungan ini diteruskan ke peredam sehingga kita dapat menggunakan ketergantungan langsung saat menjalankan aplikasi pada perangkat, tetapi menggunakan ketergantungan tiruan untuk pengujian. Kita dapat melakukan ini dengan menambahkan properti ke peredam Feature
:
@Reducerstruct Feature { biarkan numberFact: (Int) async throw -> String // ...}
Kemudian kita bisa menggunakannya dalam implementasi reduce
:
kasus .numberFactButtonTapped: return .run { [count = state.count] kirim biarkan fakta = coba tunggu mandiri.numberFact(hitungan) tunggu kirim(.numberFactResponse(fakta)) }
Dan di titik masuk aplikasi kami dapat menyediakan versi ketergantungan yang benar-benar berinteraksi dengan server API dunia nyata:
@mainstruct Aplikasi Saya: Aplikasi { var body: beberapa Adegan { WindowGroup { FeatureView( toko: Toko(Status awal: Fitur.Status()) { Fitur( numberFact: { nomor masuk (data, _) = coba tunggu URLSession.shared.data( dari: URL(string: "http://numbersapi.com/(number)")! ) return String(decoding: data, sebagai: UTF8.self) } ) } ) } }}
Namun dalam pengujian kita dapat menggunakan ketergantungan tiruan yang segera mengembalikan fakta yang deterministik dan dapat diprediksi:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0) adalah angka yang bagus Brent" }) }}
Dengan sedikit upaya awal tersebut, kita dapat menyelesaikan pengujian dengan menyimulasikan pengguna yang mengetuk tombol fakta, lalu menerima respons dari ketergantungan untuk menyajikan fakta:
tunggu toko.kirim(.numberFactButtonTapped)tunggu toko.terima(.numberFactResponse) { $0.numberFact = "0 adalah angka yang bagus, Brent"}
Kita juga dapat meningkatkan ergonomi penggunaan ketergantungan numberFact
dalam aplikasi kita. Seiring waktu, aplikasi dapat berkembang menjadi banyak fitur, dan beberapa fitur tersebut mungkin juga memerlukan akses ke numberFact
, dan meneruskannya secara eksplisit ke semua lapisan dapat mengganggu. Ada proses yang dapat Anda ikuti untuk “mendaftarkan” dependensi ke perpustakaan, menjadikannya langsung tersedia untuk lapisan mana pun dalam aplikasi.
Catatan
Untuk informasi lebih mendalam tentang manajemen ketergantungan, lihat artikel dependensi khusus.
Kita bisa mulai dengan menggabungkan fungsionalitas fakta angka dalam tipe baru:
struct NumberFactClient { var ambil: (Int) lemparan async -> String}
Dan kemudian mendaftarkan tipe tersebut ke sistem manajemen ketergantungan dengan menyesuaikan klien dengan protokol DependencyKey
, yang mengharuskan Anda menentukan nilai langsung yang akan digunakan saat menjalankan aplikasi di simulator atau perangkat:
ekstensi NumberFactClient: DependencyKey { static biarkan liveValue = Self( ambil: { nomor di let (data, _) = coba tunggu URLSession.shared .data(dari: URL(string: "http://numbersapi.com/(number)")! ) return String(decoding: data, sebagai: UTF8.self) } )}ekstensi DependencyValues { var numberFact: NumberFactClient { dapatkan { self[NumberFactClient.self] } set { mandiri[NumberFactClient.self] = Nilai baru } }}
Dengan sedikit pekerjaan awal yang dilakukan, Anda dapat langsung mulai memanfaatkan ketergantungan pada fitur apa pun dengan menggunakan pembungkus properti @Dependency
:
@Peredam Fitur struct {- biarkan numberFact: (Int) async throw -> String+ @Dependency(.numberFact) var numberFact …- coba tunggu self.numberFact(count)+ coba tunggu self.numberFact.fetch(count) }
Kode ini berfungsi persis seperti sebelumnya, tetapi Anda tidak lagi harus meneruskan ketergantungan secara eksplisit saat membuat peredam fitur. Saat menjalankan aplikasi di pratinjau, simulator, atau di perangkat, ketergantungan langsung akan diberikan ke peredam, dan dalam pengujian, ketergantungan pengujian akan diberikan.
Ini berarti titik masuk ke aplikasi tidak perlu lagi membuat dependensi:
@mainstruct Aplikasi Saya: Aplikasi { var body: beberapa Adegan { WindowGroup { FeatureView( toko: Toko(negara awal: Fitur.Negara()) { Fitur() } ) } }}
Dan penyimpanan pengujian dapat dibuat tanpa menentukan dependensi apa pun, namun Anda masih dapat mengganti dependensi apa pun yang Anda perlukan untuk tujuan pengujian:
biarkan toko = TestStore(initialState: Feature.State()) { Feature()} withDependencies: { $0.numberFact.fetch = { "($0) adalah angka Brent yang bagus" }}// ...
Itulah dasar-dasar pembuatan dan pengujian fitur dalam Arsitektur Composable. Masih banyak lagi hal yang bisa dieksplorasi, seperti komposisi, modularitas, kemampuan beradaptasi, dan efek kompleks. Direktori Contoh memiliki banyak proyek untuk dijelajahi guna melihat penggunaan lebih lanjut.
Dokumentasi untuk rilis dan main
tersedia di sini:
main
1.17.0 (panduan migrasi)
1.16.0 (panduan migrasi)
1.15.0 (panduan migrasi)
1.14.0 (panduan migrasi)
1.13.0 (panduan migrasi)
1.12.0 (panduan migrasi)
1.11.0 (panduan migrasi)
1.10.0 (panduan migrasi)
1.9.0 (panduan migrasi)
1.8.0 (panduan migrasi)
1.7.0 (panduan migrasi)
1.6.0 (panduan migrasi)
1.5.0 (panduan migrasi)
1.4.0 (panduan migrasi)
1.3.0
1.2.0
1.1.0
1.0.0
0.59.0
0.58.0
0.57.0
Ada sejumlah artikel dalam dokumentasi yang mungkin berguna bagi Anda saat Anda semakin terbiasa dengan perpustakaan:
Memulai
Ketergantungan
Pengujian
Navigasi
Status berbagi
Pertunjukan
Konkurensi
Binding
Jika Anda ingin mendiskusikan Arsitektur Composable atau memiliki pertanyaan tentang cara menggunakannya untuk menyelesaikan masalah tertentu, ada beberapa tempat yang bisa Anda diskusikan dengan sesama penggemar Point-Free:
Untuk diskusi jangka panjang, kami merekomendasikan tab diskusi di repo ini.
Untuk obrolan santai, kami merekomendasikan slack Komunitas Bebas Poin.
Anda dapat menambahkan ComposableArchitecture ke proyek Xcode dengan menambahkannya sebagai dependensi paket.
Dari menu File , pilih Tambahkan Ketergantungan Paket...
Masukkan "https://github.com/pointfreeco/swift-composable-architecture" ke dalam kolom teks URL repositori paket
Bergantung pada bagaimana proyek Anda disusun:
Jika Anda memiliki satu target aplikasi yang memerlukan akses ke perpustakaan, tambahkan ComposableArchitecture langsung ke aplikasi Anda.
Jika Anda ingin menggunakan pustaka ini dari beberapa target Xcode, atau menggabungkan target Xcode dan target SPM, Anda harus membuat kerangka kerja bersama yang bergantung pada ComposableArchitecture , lalu bergantung pada kerangka tersebut di semua target Anda. Sebagai contohnya, lihat aplikasi demo Tic-Tac-Toe, yang membagi banyak fitur menjadi modul dan menggunakan perpustakaan statis dengan cara ini menggunakan paket Swift tic-tac-toe .
Arsitektur Composable dibangun dengan mempertimbangkan ekstensibilitas, dan terdapat sejumlah perpustakaan yang didukung komunitas yang tersedia untuk menyempurnakan aplikasi Anda:
Ekstra Arsitektur yang Dapat Dikomposisi: Pustaka pendamping untuk Arsitektur yang Dapat Dikomposisi.
TCAComposer: Kerangka kerja makro untuk menghasilkan kode pelat ketel dalam Arsitektur Composable.
TCACoordinators: Pola koordinator dalam Arsitektur Composable.
Jika Anda ingin menyumbangkan perpustakaan, silakan buka PR dengan tautan ke perpustakaan tersebut!
Terjemahan README berikut ini telah disumbangkan oleh anggota komunitas:
Arab
Perancis
Hindi
Indonesia
Italia
Jepang
Korea
Polandia
Portugis
Rusia
Cina Sederhana
Spanyol
Ukraina
Jika Anda ingin menyumbangkan terjemahan, silakan buka PR dengan tautan ke Intisari!
Kami memiliki artikel khusus untuk semua pertanyaan dan komentar yang paling sering diajukan orang mengenai perpustakaan.
Orang-orang berikut memberikan masukan mengenai perpustakaan pada tahap awal dan membantu menjadikan perpustakaan seperti sekarang ini:
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, dan semua pelanggan Point-Free?.
Terima kasih khusus kepada Chris Liscio yang membantu kami mengatasi banyak kebiasaan aneh SwiftUI dan membantu menyempurnakan API akhir.
Dan terima kasih kepada Shai Mishali dan proyek CombineCommunity, tempat kami mengambil penerapan Publishers.Create
, yang kami gunakan di Effect
untuk membantu menjembatani delegasi dan API berbasis panggilan balik, sehingga lebih mudah untuk berinteraksi dengan kerangka kerja pihak ketiga.
Arsitektur Composable dibangun di atas landasan ide yang dimulai oleh perpustakaan lain, khususnya Elm dan Redux.
Ada juga banyak perpustakaan arsitektur di komunitas Swift dan iOS. Masing-masing memiliki prioritas dan trade-off tersendiri yang berbeda dari Arsitektur Composable.
tulang rusuk
Lingkaran
Cepat kembali
Alur kerja
Kit Reaktor
Umpan Balik Rx
Mobius.swift
Fluksor
Kit Arsitektur yang Dijanjikan
Perpustakaan ini dirilis di bawah lisensi MIT. Lihat LISENSI untuk detailnya.