컴포저블 아키텍처(TCA, 줄여서)는 구성, 테스트 및 인체 공학을 염두에 두고 일관되고 이해하기 쉬운 방식으로 애플리케이션을 구축하기 위한 라이브러리입니다. SwiftUI, UIKit 등 및 모든 Apple 플랫폼(iOS, macOS, VisionOS, tvOS 및 watchOS)에서 사용할 수 있습니다.
컴포저블 아키텍처란 무엇입니까?
자세히 알아보기
예
기본 사용법
선적 서류 비치
지역 사회
설치
번역
이 라이브러리는 다양한 목적과 복잡성의 애플리케이션을 구축하는 데 사용할 수 있는 몇 가지 핵심 도구를 제공합니다. 다음과 같이 애플리케이션을 구축할 때 일상적으로 직면하는 많은 문제를 해결하기 위해 따를 수 있는 설득력 있는 이야기를 제공합니다.
상태 관리
간단한 값 유형을 사용하여 애플리케이션 상태를 관리하고, 한 화면의 변형을 다른 화면에서 즉시 관찰할 수 있도록 여러 화면에서 상태를 공유하는 방법입니다.
구성
큰 기능을 자체 격리된 모듈로 추출할 수 있고 쉽게 다시 접착하여 기능을 형성할 수 있는 작은 구성 요소로 나누는 방법.
부작용
가능한 가장 테스트 가능하고 이해하기 쉬운 방식으로 애플리케이션의 특정 부분이 외부 세계와 통신하도록 하는 방법입니다.
테스트
아키텍처에 내장된 기능을 테스트하는 방법뿐만 아니라 여러 부분으로 구성된 기능에 대한 통합 테스트를 작성하고, 부작용이 애플리케이션에 어떤 영향을 미치는지 이해하기 위한 엔드투엔드 테스트를 작성하는 방법입니다. 이를 통해 비즈니스 로직이 예상한 방식으로 실행된다는 것을 강력하게 보장할 수 있습니다.
인간 공학
가능한 적은 개념과 움직이는 부분을 사용하여 간단한 API에서 위의 모든 작업을 수행하는 방법입니다.
컴포저블 아키텍처는 Brandon Williams와 Stephen Celis가 진행하는 함수형 프로그래밍과 Swift 언어를 탐구하는 비디오 시리즈인 Point-Free의 여러 에피소드를 통해 설계되었습니다.
여기에서 모든 에피소드를 볼 수 있을 뿐만 아니라 처음부터 건축물에 대한 전용 다중 투어도 볼 수 있습니다.
이 저장소에는 컴포저블 아키텍처의 일반적이고 복잡한 문제를 해결하는 방법을 보여주는 많은 예가 포함되어 있습니다. 다음을 포함한 모든 항목을 보려면 이 디렉토리를 확인하세요.
사례 연구
시작하기
효과
항해
고차 감속기
재사용 가능한 구성 요소
위치관리자
모션 매니저
찾다
음성 인식
SyncUps 앱
틱택토
할일
음성 메모
더 실질적인 것을 찾고 계십니까? SwiftUI 및 컴포저블 아키텍처로 구축된 iOS 단어 검색 게임인 isowords의 소스 코드를 확인하세요.
메모
단계별 대화형 튜토리얼을 보려면 컴포저블 아키텍처 만나보기를 확인하세요.
컴포저블 아키텍처를 사용하여 기능을 빌드하려면 도메인을 모델링하는 몇 가지 유형과 값을 정의합니다.
State : 기능이 논리를 수행하고 UI를 렌더링하는 데 필요한 데이터를 설명하는 유형입니다.
Action : 사용자 작업, 알림, 이벤트 소스 등 기능에서 발생할 수 있는 모든 작업을 나타내는 유형입니다.
Reducer : 앱의 현재 상태를 특정 작업에 따라 다음 상태로 발전시키는 방법을 설명하는 함수입니다. 또한 리듀서는 Effect
값을 반환하여 수행할 수 있는 API 요청과 같이 실행해야 하는 모든 효과를 반환하는 역할도 담당합니다.
Store : 실제로 기능을 구동하는 런타임입니다. 스토어가 리듀서와 효과를 실행할 수 있도록 모든 사용자 작업을 스토어로 보내고, UI를 업데이트할 수 있도록 스토어에서 상태 변경을 관찰할 수 있습니다.
이렇게 하면 기능의 테스트 가능성이 즉시 해제되고, 크고 복잡한 기능을 서로 연결할 수 있는 더 작은 도메인으로 나눌 수 있다는 이점이 있습니다.
기본적인 예로, 숫자를 증가 및 감소시키는 "+" 및 "-" 버튼과 함께 숫자를 표시하는 UI를 생각해 보세요. 상황을 흥미롭게 만들기 위해 탭했을 때 해당 숫자에 대한 임의의 사실을 가져오고 이를 뷰에 표시하는 API 요청을 하는 버튼도 있다고 가정해 보겠습니다.
이 기능을 구현하기 위해 우리는 기능의 도메인과 동작을 수용할 새로운 유형을 생성하고 @Reducer
매크로로 주석을 달 것입니다.
ComposableArchitecture@Reducerstruct 기능 가져오기 {}
여기서는 현재 개수에 대한 정수와 표시되는 사실을 나타내는 선택적 문자열로 구성된 기능 상태에 대한 유형을 정의해야 합니다.
@Reducerstruct Feature { @ObservableState struct State: Equatable { var count = 0 var numberFact: String? }}
메모
라이브러리의 관찰 도구를 활용하기 위해 @ObservableState
매크로를 State
에 적용했습니다.
또한 기능의 작업 유형을 정의해야 합니다. 감소 버튼, 증가 버튼 또는 사실 버튼을 탭하는 것과 같은 명백한 동작이 있습니다. 그러나 사실 API 요청으로부터 응답을 받을 때 발생하는 작업과 같이 약간 명확하지 않은 작업도 있습니다.
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { case decrementButtonTapped case incrementButtonTapped case numberFactButtonTapped case numberFactResponse(String) }}
그런 다음 기능에 대한 실제 논리와 동작을 구성하는 body
속성을 구현합니다. 여기서는 Reduce
감속기를 사용하여 현재 상태를 다음 상태로 변경하는 방법과 실행해야 하는 효과를 설명할 수 있습니다. 일부 작업은 효과를 실행할 필요가 없으며 이를 나타내기 위해 .none
반환할 수 있습니다.
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: some Reducer{ Reduce { state, action in 스위치 작업 { 케이스 .decrementButtonTapped: state.count -= 1 .none 케이스 반환 .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] send in let (data, _) = try wait URLSession.shared.data( 출처: URL(문자열: "http://numbersapi.com/(count)/trivia")! ) send( .numberFactResponse(String(decoding: data, as: UTF8.self)) ) } 케이스 let .numberFactResponse(fact): state.numberFact = 사실 반환 .none } } }}
그런 다음 마지막으로 기능을 표시하는 뷰를 정의합니다. 상태에 대한 모든 변경 사항을 관찰하고 다시 렌더링할 수 있도록 StoreOf
보유하고 있으며 상태가 변경되도록 모든 사용자 작업을 저장소에 보낼 수 있습니다.
struct FeatureView: View { let store: StoreOfvar body: some View { Form { Section { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "증분") { store.send(.incrementButtonTapped) } } 섹션 { Button("숫자 사실") { store.send(.numberFactButtonTapped) } } if let 사실 = store.numberFact { Text(fact) } } }}
이 저장소에서 UIKit 컨트롤러를 구동하는 것도 간단합니다. viewDidLoad
에서 저장소의 상태 변경을 관찰한 다음 저장소의 데이터로 UI 구성요소를 채울 수 있습니다. 코드는 SwiftUI 버전보다 약간 길기 때문에 여기에서 코드를 축소했습니다.
class FeatureViewController: UIViewController { let store: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, Bundle: nil) } 필수 init?(coder: NSCoder) { fatalError("init(coder:)가 구현되지 않았습니다.") } override func viewDidLoad() { super.viewDidLoad() let countLabel = UILabel() let decrementButton = UIButton() let incrementButton = UIButton() letfactLabel = UILabel() // 생략됨: 하위 뷰 추가 및 제약 조건 설정... 관찰 { [약한 자기] in 가드는 스스로를 놔둔다 그렇지 않으면 {반환 } 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 사실ButtonTapped() { self.store.send(.numberFactButtonTapped) }}
예를 들어 앱의 진입점에 이 뷰를 표시할 준비가 되면 스토어를 구성할 수 있습니다. 이는 애플리케이션을 시작할 초기 상태와 애플리케이션을 구동할 감속기를 지정하여 수행할 수 있습니다.
import ComposableArchitecture@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( 저장소: Store(initialState: Feature.State()) { Feature() } ) } }}
그리고 그것은 화면에 뭔가를 가지고 놀기에 충분합니다. 바닐라 SwiftUI 방식으로 이 작업을 수행하는 것보다 확실히 몇 가지 단계가 더 필요하지만 몇 가지 이점이 있습니다. 이는 일부 관찰 가능한 객체와 UI 구성요소의 다양한 액션 클로저에 로직을 분산시키는 대신 상태 변이를 적용하는 일관된 방식을 제공합니다. 또한 부작용을 간결하게 표현하는 방법도 제공합니다. 그리고 많은 추가 작업을 수행하지 않고도 효과를 포함한 이 논리를 즉시 테스트할 수 있습니다.
메모
테스트에 대한 자세한 내용은 전용 테스트 기사를 참조하세요.
TestStore
를 사용하여 테스트하려면 Store
와 동일한 정보로 생성할 수 있지만 작업이 전송될 때 기능이 어떻게 발전하는지 확인할 수 있도록 추가 작업을 수행합니다.
@Testfunc 기본() async { let store = TestStore(initialState: Feature.State()) { Feature() }}
테스트 저장소가 생성되면 이를 사용하여 전체 사용자 단계 흐름을 어설션할 수 있습니다. 각 단계에서 우리는 상태가 우리가 기대하는 방식으로 바뀌었다는 것을 증명해야 합니다. 예를 들어, 증가 및 감소 버튼을 탭하는 사용자 흐름을 시뮬레이션할 수 있습니다.
// 증가/감소 버튼을 탭하면 countawait가 변경되는지 테스트합니다. store.send(.incrementButtonTapped) { $0.count = 1}store.send(.decrementButtonTapped)를 기다립니다. { $0.count = 0}
또한 단계에서 효과가 실행되어 저장소에 데이터가 다시 공급되는 경우 이를 주장해야 합니다. 예를 들어, 사용자가 팩트 버튼을 탭하는 것을 시뮬레이션하는 경우 팩트와 함께 팩트 응답을 다시 수신할 것으로 예상되며, 이로 인해 numberFact
상태가 채워집니다.
wait store.send(.numberFactButtonTapped)await store.receive(.numberFactResponse) { $0.numberFact = ???}
그러나 어떤 사실이 우리에게 다시 전송될지 어떻게 알 수 있습니까?
현재 우리의 리듀서는 API 서버에 도달하기 위해 현실 세계에 도달하는 효과를 사용하고 있으며 이는 우리가 그 동작을 제어할 방법이 없다는 것을 의미합니다. 우리는 이 테스트를 작성하기 위해 인터넷 연결과 API 서버의 가용성에 따라 변덕스럽습니다.
장치에서 애플리케이션을 실행할 때 라이브 종속성을 사용할 수 있지만 테스트에는 모의 종속성을 사용할 수 있도록 이 종속성을 리듀서에 전달하는 것이 더 나을 것입니다. Feature
감속기에 속성을 추가하여 이를 수행할 수 있습니다.
@Reducerstruct Feature { let numberFact: (Int) async throws -> 문자열 // ...}
그런 다음 이를 reduce
구현에 사용할 수 있습니다.
케이스 .numberFactButtonTapped: return .run { [count = state.count] 보내기 사실을 보자 = wait self.numberFact(count) wait send(.numberFactResponse(fact)) }
그리고 애플리케이션의 진입점에서 실제 API 서버와 실제로 상호 작용하는 종속성 버전을 제공할 수 있습니다.
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( 저장소: Store(initialState: Feature.State()) { 기능( numberFact: { let(data, _)의 숫자 = URLSession.shared.data( 출처: URL(문자열: "http://numbersapi.com/(숫자)")! ) return String(디코딩: 데이터, as: UTF8.self) } ) } ) } }}
그러나 테스트에서는 결정적이고 예측 가능한 사실을 즉시 반환하는 모의 종속성을 사용할 수 있습니다.
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0)는 좋은 숫자입니다. 브렌트" }) }}
약간의 사전 작업을 통해 사용자가 사실 버튼을 탭하는 것을 시뮬레이션한 다음 종속성으로부터 응답을 받아 사실을 제시함으로써 테스트를 완료할 수 있습니다.
wait store.send(.numberFactButtonTapped)await store.receive(.numberFactResponse) { $0.numberFact = "0은 좋은 숫자 브렌트입니다"}
또한 애플리케이션에서 numberFact
종속성을 사용하여 인체공학적 측면을 개선할 수도 있습니다. 시간이 지남에 따라 애플리케이션은 많은 기능으로 발전할 수 있으며, 이러한 기능 중 일부는 numberFact
에 대한 액세스를 원할 수도 있으며 모든 레이어를 명시적으로 전달하면 성가실 수 있습니다. 라이브러리에 종속성을 "등록"하여 애플리케이션의 모든 계층에서 즉시 사용할 수 있도록 하는 프로세스가 있습니다.
메모
종속성 관리에 대한 자세한 내용은 전용 종속성 문서를 참조하세요.
숫자 사실 기능을 새로운 유형으로 래핑하는 것부터 시작할 수 있습니다.
struct NumberFactClient { var fetch: (Int) 비동기 발생 -> 문자열}
그런 다음 클라이언트를 종속성 관리 시스템에 등록하면 DependencyKey
나 장치에서 애플리케이션을 실행할 때 사용할 라이브 값을 지정해야 합니다.
확장 번호FactClient: 종속성키 { static let liveValue = Self( 가져오기: { let (data, _)의 숫자 = URLSession.shared .data(from: URL(string: "http://numbersapi.com/(number)")! ) return String(decoding: data, as: UTF8.self) } )}extension 종속성 값 { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = 새로운 값 } }}
약간의 사전 작업이 완료되면 @Dependency
속성 래퍼를 사용하여 모든 기능에서 종속성을 즉시 사용할 수 있습니다.
@감속기 struct Feature {- numberFact: (Int) async throws -> String+ @Dependency(.numberFact) var numberFact ...- self.numberFact(count)를 기다려 보세요. + self.numberFact.fetch(count)를 기다려 보세요. }
이 코드는 이전과 동일하게 작동하지만 기능의 리듀서를 구성할 때 더 이상 종속성을 명시적으로 전달할 필요가 없습니다. 미리 보기, 시뮬레이터 또는 장치에서 앱을 실행할 때 라이브 종속성이 감속기에 제공되고 테스트에서는 테스트 종속성이 제공됩니다.
이는 애플리케이션의 진입점이 더 이상 종속성을 구성할 필요가 없음을 의미합니다.
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( 저장소: Store(initialState: Feature.State()) { Feature() } ) } }}
종속성을 지정하지 않고도 테스트 저장소를 생성할 수 있지만 테스트 목적에 필요한 종속성은 여전히 재정의할 수 있습니다.
let store = TestStore(initialState: Feature.State()) { Feature()} withDependency: { $0.numberFact.fetch = { "($0)은 좋은 숫자 브렌트입니다." }}// ...
이것이 컴포저블 아키텍처에서 기능을 빌드하고 테스트하는 기본 사항입니다. 구성, 모듈성, 적응성, 복잡한 효과 등 탐구할 사항이 더 많습니다 . 예제 디렉토리에는 더 많은 고급 사용법을 확인하기 위해 탐색할 수 있는 여러 프로젝트가 있습니다.
릴리스 및 main
문서는 다음에서 확인할 수 있습니다.
main
1.17.0(마이그레이션 가이드)
1.16.0(마이그레이션 가이드)
1.15.0(마이그레이션 가이드)
1.14.0(마이그레이션 가이드)
1.13.0(마이그레이션 가이드)
1.12.0(마이그레이션 가이드)
1.11.0(마이그레이션 가이드)
1.10.0(마이그레이션 가이드)
1.9.0(마이그레이션 가이드)
1.8.0(마이그레이션 가이드)
1.7.0(마이그레이션 가이드)
1.6.0(마이그레이션 가이드)
1.5.0(마이그레이션 가이드)
1.4.0(마이그레이션 가이드)
1.3.0
1.2.0
1.1.0
1.0.0
0.59.0
0.58.0
0.57.0
라이브러리에 익숙해지면 도움이 될 수 있는 문서가 많이 있습니다.
시작하기
종속성
테스트
항해
공유 상태
성능
동시성
바인딩
컴포저블 아키텍처에 대해 논의하고 싶거나 이를 사용하여 특정 문제를 해결하는 방법에 대한 질문이 있는 경우 동료 Point-Free 애호가와 논의할 수 있는 여러 장소가 있습니다.
긴 토론의 경우 이 저장소의 토론 탭을 권장합니다.
캐주얼한 채팅을 원하시면 Point-Free Community Slack을 추천합니다.
ComposableArchitecture를 패키지 종속성으로 추가하여 Xcode 프로젝트에 추가할 수 있습니다.
파일 메뉴에서 패키지 종속성 추가...를 선택합니다.
패키지 저장소 URL 텍스트 필드에 "https://github.com/pointfreeco/swift-composable-architecture"를 입력합니다.
프로젝트 구성 방식에 따라 다음과 같습니다.
라이브러리에 액세스해야 하는 단일 애플리케이션 대상이 있는 경우 ComposableArchitecture를 애플리케이션에 직접 추가하세요.
여러 Xcode 대상에서 이 라이브러리를 사용하거나 Xcode 대상과 SPM 대상을 혼합하려면 ComposableArchitecture 에 의존하는 공유 프레임워크를 만든 다음 모든 대상에서 해당 프레임워크에 의존해야 합니다. 이에 대한 예를 보려면 Tic-Tac-Toe 데모 애플리케이션을 확인하세요. 이 애플리케이션은 많은 기능을 모듈로 분할하고 tic-tac-toe Swift 패키지를 사용하여 이러한 방식으로 정적 라이브러리를 사용합니다.
컴포저블 아키텍처는 확장성을 염두에 두고 구축되었으며, 애플리케이션을 향상시키는 데 사용할 수 있는 커뮤니티 지원 라이브러리가 많이 있습니다.
컴포저블 아키텍처 추가 기능: 컴포저블 아키텍처의 동반 라이브러리입니다.
TCAComposer: 컴포저블 아키텍처에서 상용구 코드를 생성하기 위한 매크로 프레임워크입니다.
TCACoordinators: 컴포저블 아키텍처의 코디네이터 패턴입니다.
도서관에 기여하고 싶다면 링크가 포함된 PR을 열어주세요!
이 README의 다음 번역은 커뮤니티 구성원이 제공했습니다.
아라비아 말
프랑스 국민
힌디 어
인도네시아 인
이탈리아 사람
일본어
한국인
광택
포르투갈 인
러시아인
중국어 간체
스페인 사람
우크라이나 말
번역에 기여하고 싶다면 Gist 링크가 포함된 PR을 열어주세요!
우리는 도서관에 관해 사람들이 가장 자주 묻는 질문과 의견을 모두 담은 전용 기사를 보유하고 있습니다.
다음 사람들은 도서관 초기 단계에 대한 피드백을 제공하고 오늘날의 도서관을 만드는 데 도움을 주었습니다.
폴 콜튼, 칸 데데오글루, 맷 디프하우스, 요세프 돌레잘, 에이만타스, 매튜 존슨, 조지 카이마카스, 니키타 레오노프, 크리스토퍼 리스시오, 제프리 맥코, 알레한드로 마르티네즈, 샤이 미샬리, 윌리스 플러머, 사이먼-피에르 로이, 저스틴 프라이스, 스벤 A. 슈미트 , 카일 셔먼, 페트르 시마, 야스데프 싱, 맥심 Smirnov, Ryan Stone, Daniel Hollis Tavares 및 모든 Point-Free 가입자 ?.
많은 이상한 SwiftUI 문제를 해결하고 최종 API를 개선하는 데 도움을 준 Chris Liscio에게 특별히 감사드립니다.
그리고 Shai Mishali와 CombineCommunity 프로젝트에서 Publishers.Create
구현을 가져왔습니다. Effect
에서 이를 사용하여 대리자 및 콜백 기반 API를 연결함으로써 타사 프레임워크와 인터페이스하기가 훨씬 쉬워졌습니다.
컴포저블 아키텍처는 다른 라이브러리, 특히 Elm 및 Redux에서 시작된 아이디어를 기반으로 구축되었습니다.
Swift 및 iOS 커뮤니티에도 많은 아키텍처 라이브러리가 있습니다. 이들 각각에는 컴포저블 아키텍처와 다른 고유한 우선순위와 장단점이 있습니다.
갈비 살
고리
리스위프트
작업흐름
리액터 키트
수신피드백
Mobius.swift
플럭서
약속된 아키텍처 키트
이 라이브러리는 MIT 라이센스에 따라 배포됩니다. 자세한 내용은 라이센스를 참조하세요.