The Composable Architecture (kısaca TCA), uygulamaları tutarlı ve anlaşılır bir şekilde oluşturmak için tasarlanmış, bileşim, test edilebilirlik ve kullanım kolaylığı düşünülerek geliştirilmiş bir kütüphanedir. SwiftUI, UIKit ve daha fazlasında, herhangi bir Apple platformunda (iOS, macOS, iPadOS, visionOS, tvOS ve watchOS) kullanılabilir.
- The Composable Architecture Nedir?
- Daha Fazla Bilgi
- Örnekler
- Temel Kullanım
- Test etme
- Dokümantasyon
- Topluluk
- Kurulum
Bu kütüphane, çeşitli amaç ve karmaşıklıkta uygulamalar oluşturmak için kullanılabilecek temel araçlar sağlar. Uygulama geliştirirken karşılaştığınız günlük problemleri çözmek için takip edebileceğiniz etkileyici yaklaşımlar sunar:
-
Durum Yönetimi
Uygulamanızın durumunu basit değer tipleriyle yönetmek ve durumu birçok ekran arasında paylaşmak, böylece bir ekrandaki değişikliklerin anında diğer ekranlarda gözlemlenebilmesi. -
Bileşim
Büyük özellikleri, kendi izole modüllerine ayrıştırılabilen ve daha sonra kolayca bir araya getirilebilen küçük bileşenlere ayırmak. -
Yan Etkiler
Uygulamanın belirli bölümlerinin dış dünya ile en test edilebilir ve anlaşılır şekilde iletişim kurmasını sağlamak. -
Test Etme
Sadece mimaride oluşturulmuş bir özelliği test etmek değil, aynı zamanda birçok parçadan oluşan özellikler için entegrasyon testleri yazmak ve yan etkilerin uygulamayı nasıl etkilediğini anlamak için uçtan uca testler yazmak. Bu, iş mantığınızın beklediğiniz şekilde çalıştığına dair güçlü garantiler sağlar. -
Kullanım Kolaylığı
Tüm bunları mümkün olduğunca az kavram ve hareketli parçayla basit bir API'de gerçekleştirmek.
The Composable Architecture, fonksiyonel programlama ve Swift dilini keşfeden bir video serisi olan Point-Free üzerinde birçok bölüm boyunca tasarlandı. Seri, Brandon Williams ve Stephen Celis tarafından sunulmaktadır. Tüm bölümleri buradan izleyebilir, ayrıca mimariye sıfırdan adanmış çok bölümlü bir turu keşfedebilirsiniz.
Bu repo, the Composable Architecture ile yaygın ve karmaşık problemlerin nasıl çözüleceğini gösteren birçok örnek içerir. Tüm örnekleri görmek için bu dizine göz atın:
- Vaka Çalışmaları
- Başlangıç
- Etkiler
- Navigasyon
- Yüksek dereceli indirgeyiciler
- Yeniden kullanılabilir bileşenler
- Konum yöneticisi
- Hareket yöneticisi
- Arama
- Ses Tanıma
- SyncUps uygulaması
- Tic-Tac-Toe
- Yapılacaklar
- Ses notları
Daha kapsamlı bir şey mi arıyorsunuz? SwiftUI ve the Composable Architecture ile oluşturulmuş bir iOS kelime arama oyunu olan isowords kaynak koduna göz atın.
Note
Adım adım interaktif bir eğitim için Meet the Composable Architecture'yı inceleyin.
The Composable Architecture kullanarak bir özellik oluşturmak için, alanınızı modelleyen bazı tipler ve değerler tanımlamanız gerekir:
- Durum (State):Özelliğinizin mantığını gerçekleştirmesi ve UI'ını render etmesi için ihtiyaç duyduğu verileri tanımlayan bir tip.
- Eylem (Action): Özelliğinizde gerçekleşebilecek tüm eylemleri (kullanıcı eylemleri, bildirimler, olay kaynakları vb.) temsil eden bir tip.
- İndirgeyici (Reducer): Bir eylem verildiğinde uygulamanın mevcut durumunu bir sonraki duruma nasıl evriltileceğini açıklayan bir fonksiyon. İndirgeyici ayrıca, API istekleri gibi çalıştırılması gereken etkileri döndürmekten sorumludur.
- Mağaza (Store): Özelliğinizi çalıştıran runtime. Tüm kullanıcı eylemlerini mağazaya gönderirsiniz, böylece mağaza indirgeyiciyi ve etkileri çalıştırabilir. Ayrıca mağazadaki durum değişikliklerini gözlemleyerek UI'ı güncelleyebilirsiniz.
Bunu yapmanın avantajı, özelliğinizin test edilebilirliğini anında kilitlemeniz ve büyük, karmaşık özellikleri bir araya getirilebilen daha küçük alanlara bölebilmenizdir.
Temel bir örnek olarak, bir sayıyı ve bu sayıyı artıran/azaltan "+" ve "−" düğmelerini gösteren bir UI düşünün. İlginç olması için, ayrıca tıklandığında bu sayı hakkında rastgele bir gerçek getirmek için bir API isteği yapan ve görünümde gösteren bir düğme olduğunu varsayalım.
Bu özelliği uygulamak için, özelliğin alanını ve davranışını barındıracak yeni bir tip oluştururuz ve bu tipi @Reducer makrosuyla işaretleriz:
import ComposableArchitecture
@Reducer
struct Feature {
}Burada, özelliğin durumu için bir tip tanımlamamız gerekiyor. Bu tip, mevcut sayı için bir tamsayı ve gösterilen gerçeği temsil eden opsiyonel bir string içerir:
@Reducer
struct Feature {
@ObservableState
struct State: Equatable {
var count = 0
var numberFact: String?
}
}Note
Kütüphanedeki gözlem araçlarından yararlanmak için State'e @ObservableState makrosunu uyguladık.
Ayrıca özelliğin eylemleri için bir tip tanımlamamız gerekiyor. Azaltma, artırma veya gerçek düğmesine basma gibi belirgin eylemlerin yanı sıra, gerçek API isteğinden bir yanıt aldığımızda gerçekleşen eylem gibi daha az belirgin eylemler de vardır:
@Reducer
struct Feature {
@ObservableState
struct State: Equatable { /* ... */ }
enum Action {
case decrementButtonTapped
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(String)
}
}Ardından, özelliğin gerçek mantığını ve davranışını oluşturmaktan sorumlu olan body özelliğini uygularız. Burada mevcut durumu bir sonraki duruma nasıl değiştireceğimizi ve hangi etkilerin çalıştırılması gerektiğini tanımlamak için Reduce indirgeyicisini kullanabiliriz. Bazı eylemler etki çalıştırmaz ve bunlar .none döndürebilir:
@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
}
}
}
}Son olarak, özelliği görüntüleyen view'ı tanımlarız. Bu view, StoreOf<Feature> tutar, böylece durumdaki tüm değişiklikleri gözlemleyebilir ve yeniden render edebilir.
Ayrıca tüm kullanıcı eylemlerini mağazaya göndererek durum değişikliklerini tetikleyebiliriz:
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)
}
}
}
}Bu mağazayı kullanan bir UIKit kontrolcüsü oluşturmak da oldukça basittir. viewDidLoad'de mağazadaki durum değişikliklerini gözlemleyebilir ve UI bileşenlerini mağazadan gelen verilerle doldurabilirsiniz. Kod SwiftUI versiyonundan biraz daha uzun, bu yüzden burada daralttık:
Genişletmek için tıklayın!
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)
}
}Bu görünümü görüntülemeye hazır olduğumuzda, örneğin uygulamanın giriş noktasında, bir mağaza oluşturabiliriz. Bu, uygulamayı başlatmak için başlangıç durumunu ve uygulamayı çalıştıracak indirgeyiciyi belirterek yapılır:
import ComposableArchitecture
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
FeatureView(
store: Store(initialState: Feature.State()) {
Feature()
}
)
}
}
}Bu, ekranda denemek için bir şeyler elde etmek için yeterlidir. Vanilla SwiftUI yaklaşımıyla yapmaktan biraz daha fazla adım gerektirir, ancak birkaç avantajı vardır. Mantığı bazı gözlemlenebilir nesnelere ve UI bileşenlerinin eylem kapanışlarına dağıtmak yerine, durum mutasyonlarını tutarlı bir şekilde uygulamamızı sağlar. Ayrıca yan etkileri özlü bir şekilde ifade etmemize olanak tanır. Ve bu mantığı, ekstra iş yapmadan hemen test edebiliriz.
Note
Test etme hakkında daha derinlemesine bilgi için özel test etme makalesine bakın.
Test etmek için TestStore kullanın. Bu, Store ile aynı bilgilerle oluşturulabilir, ancak eylemler gönderildiğinde özelliğin nasıl evrildiğini doğrulamanız için ekstra iş yapar:
@Test
func basics() async {
let store = TestStore(initialState: Feature.State()) {
Feature()
}
}Test mağazası oluşturulduktan sonra, tüm bir kullanıcı akışının adımlarını iddia etmek için kullanabiliriz. Her adımda, durumun beklediğimiz şekilde değiştiğini kanıtlamamız gerekir. Örneğin, kullanıcının artırma ve azaltma düğmelerine tıklamasını simüle edebiliriz:
// Artırma/azaltma düğmelerine tıklandığında sayının değiştiğini test et
await store.send(.incrementButtonTapped) {
$0.count = 1
}
await store.send(.decrementButtonTapped) {
$0.count = 0
}Ayrıca, bir adım bir etkinin çalıştırılmasına ve mağazaya veri geri beslemesine neden olursa, bunu iddia etmeliyiz. Örneğin, kullanıcının gerçek düğmesine tıkladığını simüle edersek, bir gerçek yanıtı almayı bekleriz ve bu da numberFact durumunun doldurulmasına neden olur:
await store.send(.numberFactButtonTapped)
await store.receive(\.numberFactResponse) {
$0.numberFact = ???
}Ancak, bize hangi gerçeğin gönderileceğini nasıl bileceğiz?
Şu anda indirgeyicimiz, gerçek dünyadaki bir API sunucusuna istek atmak için bir etki kullanıyor ve bu, davranışını kontrol etmemizin bir yolu olmadığı anlamına geliyor. Bu testi yazmak için internet bağlantımıza ve API sunucusunun kullanılabilirliğine bağımlıyız.
Bu bağımlılığın indirgeyiciye iletilmesi daha iyi olur, böylece uygulamayı bir cihazda çalıştırırken canlı bir bağımlılık kullanabilir, testler için sahte bir bağımlılık kullanabiliriz. Bunu Feature indirgeyicisine bir özellik ekleyerek yapabiliriz:
@Reducer
struct Feature {
let numberFact: (Int) async throws -> String
// ...
}Ardından bunu reduce uygulamasında kullanabiliriz:
case .numberFactButtonTapped:
return .run { [count = state.count] send in
let fact = try await self.numberFact(count)
await send(.numberFactResponse(fact))
}Uygulamanın giriş noktasında, gerçek dünya API sunucusu ile etkileşime giren bir bağımlılık sağlayabiliriz:
@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)
}
)
}
)
}
}
}Ancak testlerde hemen belirleyici, öngörülebilir bir gerçek döndüren sahte bir bağımlılık kullanabiliriz:
@Test
func basics() async {
let store = TestStore(initialState: Feature.State()) {
Feature(numberFact: { "\($0) is a good number Brent" })
}
}Bu ön çalışma ile kullanıcının gerçek düğmesine tıklamasını simüleyebilir ve ardından bağımlılıktan gelen yanıtı alarak gerçeği görüntüleyebiliriz:
await store.send(.numberFactButtonTapped)
await store.receive(\.numberFactResponse) {
$0.numberFact = "0 is a good number Brent"
}Ayrıca numberFact bağımlılığını uygulamamızda kullanmanın ergonomisini iyileştirebiliriz. Zamanla uygulama birçok özelliğe evrilebilir ve bu özelliklerden bazıları numberFact'e erişmek isteyebilir. Tüm katmanlar arasında açıkça iletilmesi can sıkıcı olabilir. Bağımlılıkları kütüphaneye "kaydederek" uygulamanın herhangi bir katmanında anında kullanılabilir hale getirebilirsiniz.
Note
Bağımlılık yönetimi hakkında daha derinlemesine bilgi için özel bağımlılıklar makalesine bakın.
NumberFact işlevselliğini yeni bir tipe sararak başlayabiliriz:
struct NumberFactClient {
var fetch: (Int) async throws -> String
}Ardından, DependencyKey protokolüne uygun hale getirerek bağımlılık yönetim sistemine bu türü kaydedebiliriz. Bu, uygulamayı simülatörlerde veya cihazlarda çalıştırırken kullanılacak canlı değeri belirtmenizi gerektirir:
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 }
}
}Bu ön çalışma tamamlandıktan sonra, @Dependency property wrapper'ını kullanarak herhangi bir özellikte bağımlılığı anında kullanmaya başlayabilirsiniz:
@Reducer
struct Feature {
- let numberFact: (Int) async throws -> String
+ @Dependency(\.numberFact) var numberFact
…
- try await self.numberFact(count)
+ try await self.numberFact.fetch(count)
}Bu kod tam olarak daha önceki gibi çalışır, ancak artık indirgeyiciyi oluştururken bağımlılığı açıkça iletmenize gerek yoktur. Uygulamayı önizlemelerde, simülatörde veya bir cihazda çalıştırırken, canlı bağımlılık indirgeyiciye sağlanır. Testlerde ise test bağımlılığı sağlanır.
Bu, uygulamanın giriş noktasının artık bağımlılık oluşturması gerekmediği anlamına gelir:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
FeatureView(
store: Store(initialState: Feature.State()) {
Feature()
}
)
}
}
}Test mağazası herhangi bir bağımlılık belirtmeden oluşturulabilir, ancak test amacıyla herhangi bir bağımlılığı geçersiz kılabilirsiniz:
let store = TestStore(initialState: Feature.State()) {
Feature()
} withDependencies: {
$0.numberFact.fetch = { "\($0) is a good number Brent" }
}
// ...Bu, the Composable Architecture'da bir özellik oluşturmanın ve test etmenin temelleridir. Keşfedilecek daha birçok şey vardır: bileşim, modülerlik, uyarlanabilirlik ve karmaşık etkiler. Örnekler dizini, daha gelişmiş kullanımları görmek için keşfedilebilecek birçok proje içerir.
Yayınlar ve ana dal için dokümantasyon burada mevcuttur:
Diğer versiyonlar
- 1.16.0 (migration guide)
- 1.15.0 (migration guide)
- 1.14.0 (migration guide)
- 1.13.0 (migration guide)
- 1.12.0 (migration guide)
- 1.11.0 (migration guide)
- 1.10.0 (migration guide)
- 1.9.0 (migration guide)
- 1.8.0 (migration guide)
- 1.7.0 (migration guide)
- 1.6.0 (migration guide)
- 1.5.0 (migration guide)
- 1.4.0 (migration guide)
- 1.3.0
- 1.2.0
- 1.1.0
- 1.0.0
- 0.59.0
- 0.58.0
- 0.57.0
Dokümantasyonda, kütüphaneye daha aşina oldukça faydalı bulabileceğiniz bir dizi makale bulunmaktadır:
The Composable Architecture'ı tartışmak veya belirli bir problemi çözmek için nasıl kullanılacağı hakkında bir sorunuz varsa, Point-Free meraklıları ile tartışabileceğiniz birkaç yer var:
- Uzun soluklu tartışmalar için bu deponun tartışmalar sekmesini öneririz.
- Gündelik sohbet için Point-Free Community slack öneririz.
ComposableArchitecture'ı bir Xcode projesine paket bağımlılığı ekleyerek ekleyebilirsiniz.
- File menüsünden Add Package Dependencies... seçeneğini seçin.
- Paket deposu URL metin alanına "https://github.com/pointfreeco/swift-composable-architecture" girin
- Projenizin yapısına bağlı olarak:
- Kütüphaneye erişmesi gereken tek bir uygulama hedefiniz varsa, doğrudan uygulamanıza ComposableArchitecture ekleyin.
- Bu kütüphaneyi birden fazla Xcode hedefinden kullanmak veya Xcode hedefleri ile SPM hedeflerini karıştırmak istiyorsanız, ComposableArchitecture'a bağlı paylaşılan bir çerçeve oluşturmalı ve ardından tüm hedeflerinizde bu çerçeveye bağımlı olmalısınız. Bunun bir örneği için, birçok özelliği modüllere ayıran ve bu statik kütüphaneyi tic-tac-toe Swift paketini kullanarak tüketen Tic-Tac-Toe demo uygulamasına bakın.
The Composable Architecture, genişletilebilirlik göz önünde bulundurularak oluşturulmuştur ve uygulamalarınızı geliştirmek için kullanılabilecek topluluk destekli bir dizi kütüphane bulunmaktadır:
- Composable Architecture Extras: The Composable Architecture'a eşlik eden bir kütüphane.
- TCAComposer: The Composable Architecture'da tekrarlayan kod oluşturmak için bir makro çerçevesi.
- TCACoordinators: The Composable Architecture'da koordinatör deseni.
Bir kütüphane katkıda bulunmak isterseniz, bir link ile PR açın!
Kütüphane ile ilgili en sık sorulan sorular ve yorumlar için özel bir makale var.
Aşağıdaki kişiler kütüphanenin erken aşamalarında geri bildirimde bulundu ve kütüphanenin bugünkü haline gelmesine yardımcı oldu:
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 ve tüm Point-Free aboneleri
Özel teşekkürler, birçok garip SwiftUI tuhaflığını çözmemize ve son API'yi iyileştirmemize yardımcı olan Chris Liscio'ya.
Ayrıca, delegate ve callback tabanlı API'lerle arayüz oluşturmayı kolaylaştırmak için Effect'de kullandığımız Publishers.Create uygulamasını aldığımız Shai Mishali ve CombineCommunity projesine teşekkürler.
The Composable Architecture, Elm ve Redux gibi diğer kütüphanelerden başlayan fikirler üzerine inşa edilmiştir. Swift ve iOS topluluğunda birçok mimari kütüphane bulunmaktadır. Bunların her birinin The Composable Architecture'dan farklı öncelikleri ve dengeleri vardır.
-
Ve daha fazlası
Bu kütüphane MIT lisansı altında yayınlanmıştır. Detaylar için LICENSE dosyasına bakın.
