iOS의 Clean Architecture
Layers
- Presentation Layer는 Domain Layer에 의존
- Data Layer는 Domain Layer에 의존
- Domain Layer는 의존성이 없기 때문에 분리해서 다른 프로젝트에서도 사용 가능
- Presentation: View(SwiftUI/UIKit), ViewModel(Presenter)
- Domain: Entity, Use Case(Interactor), Repository 인터페이스
- Data: Repository 구현, Data Source(remote/local), JSON Data 맵핑
Data 흐름
- View가 ViewModel의 method 호출
- ViewModel이 UseCase 실행
- UseCase가 Repository의 data를 조합
- Repository는 Remote/Local에서 data 받아와 리턴
- View에 Information 표시
Presentation
SwiftUI는 MVVM 패턴의 클린 아키텍처와 어울리지 않는다는 의견도 많이 있습니다. 단방향 흐름을 갖고 있는 TCA나 MV 패턴이 대세가 되어가고 있지만, 그래도 SwiftUI에도 적용하는 방법을 알아보겠습니다.
View
SwiftUI View
struct DefaultView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
List(viewModel.items) { item in
Title(item.title)
}
.task {
await viewModel.didSearch("")
}
}
}
UIKit ViewController
class DefaultViewController: UIViewController, StoryboardInstantiable {
private var viewModel: ViewModel!
static func create() -> DefaultViewController {
let viewController = DefaultViewController.instantiateViewController()
viewController.viewModel = ViewModel()
return viewController
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
}
private func bind(to viewModel: ViewModel) {
viewModel.items.observe(on: self) { [weak self] items in
// items 사용
}
viewModel.error.observe(on: self) { [weak self] error in
// error 사용
}
}
}
ViewModel
SwiftUI ViewModel
class ViewModel: ObservableObject {
private let useCase = DefaultUseCase()
@Published var items = [Item]()
@Published var error = ""
}
extension ViewModel {
func didSearch(query: String) async {
let page = 0
let query = Query(query: query)
await load(query: query, page: page)
}
private func load(query: Query, page: Int) async {
do {
let request = Request(query: query.query, page: page)
self.items = try await useCase.execute(request: request)
} catch {
self.error = error
}
}
}
UIKit ViewModel
class ViewModel {
private let useCase = DefaultUseCase()
let items: Observable<[Item]> = Observable([])
let error: Observable<String> = Observable("")
private func load(query: Query, page: Int) async {
do {
let request = Request(query: query.query, page: page)
self.items = try await useCase.execute(request: request)
} catch {
self.error = error.localizedDescription
}
}
}
final class Observable<Value> {
struct Observer<Value> {
weak var observer: AnyObject?
let block: (Value) -> Void
}
private var observers = [Observer<Value>]()
var value: Value {
didSet { notifyObservers() }
}
init(_ value: Value) {
self.value = value
}
func observe(on observer: AnyObject, observerBlock: @escaping (Value) -> Void) {
observers.append(Observer(observer: observer, block: observerBlock))
observerBlock(self.value)
}
func remove(observer: AnyObject) {
observers = observers.filter { $0.observer !== observer }
}
private func notifyObservers() {
for observer in observers {
observer.block(self.value)
}
}
}
Data 바인딩
- SwiftUI는 SwiftUI Property Wrappers, Observable Macro, Combine ObservableObject 등 사용
- UIKit은 Observable(커스텀), RxSwift, Closure, Delegate 등 사용
Domain
Entity
- Business Logic에 사용될 Data 형태
- Codable Data를 Entity 형태로 맵핑해서 사용
struct Item: Equatable, Identifiable {
let id = UUID()
let title: String
}
UseCase
- Business Logic을 구현하는 곳
- Interactor로 불리기도 함
protocol UseCase {
func execute(request: Request) async throws -> [Item]
}
class DefaultUseCase: UseCase {
private let repository = DefaultRepository()
func execute(request: Request) async throws -> [Item] {
let result = try await repository.fetchList(query: request.query, page: request.page)
// result 활용 비즈니스 로직
return result
}
}
Repository 인터페이스
- Dependency Inversion을 위해 필요
- 간단히 말해, Data Layer가 Domain Layer에 의존하기 위해 필요
protocol Repository {
func fetchList(query: Query, page: Int) async throws -> [Item]
}
Data
Repository 구현부
class DefaultRepository: Repository {
private let networkService = NetworkService()
func fetchList(query: Query, page: Int) async throws -> [Item] {
let request = Request(query: query.query, page: page)
let endpoint = APIEndpoints.get(with: request)
let response: Response = try await networkService.request(with: endpoint)
let result = response.toDomain()
return result
}
}
Network Service
- Remote data source로써 API와 통신하는 역할
- Alamofire 같은 써드파티 라이브러리를 사용해도 됨
- Network Layer 글 참고해서 자체적인 구현도 가능
Local Data
- Local data source로써 CoreData, Realm, Cache 등에 해당
JSON Codable
- JSON을 Codable로 파싱하는 Data 구조체
- Domain에서 사용하기 위해 Entity로 맵핑하는 부분도 구현
struct Request: Encodable {
let query: String
let page: Int
}
struct Response: Decodable {
let page: Int
let titles: [String]
}
extension Response {
func toDomain() -> [Item] {
titles.map { Item(title: $0) }
}
}
Dependency Injection
SwiftUI
- SwiftUI에서는 DI 라이브러리가 굳이 필요할까?
@Environment
,@EnvironmentObject
,@ObservedObject
사용- 테스트 용이성이나 추가적인 확장을 위해 Point-Free의
swift-dependencies
와 같은 라이브러리를 사용하기도 함
UIKit
Constructor Injection
protocol NetworkingProtocol {
func request(with request: URLRequest) async throws -> Response
}
class Repository: RepositoryProtocol {
private let networking: NetworkingProtocol
init(networking: NetworkingProtocol) {
self.networking = networking
}
}
Property Injection / Method Injection
// Property Injection
class Repository: RepositoryProtocol {
var networking: NetworkingProtocol?
}
// Method Injection
class Repository: RepositoryProtocol {
private var networking: NetworkingProtocol?
func set(networking: NetworkingProtocol) {
self.networking = networking
}
}
Interface Injection
protocol NetworkingDependent {
func register(networking: NetworkingProtocol)
}
class RepositoryImpl: RepositoryProtocol, NetworkingDependent {
private var networking: NetworkingProtocol?
func register(networking: NetworkingProtocol) {
self.networking = networking
}
}
IoC Container
- Swinject와 같은 프레임워크
// Constructor Injection
let networking = Networking()
let repository = Repository(networking: networking)
// IoC Container
let container = Container()
container.register(Networking.self) { _ in Networking() }
container.register(Repository.self) { r in
Repository(networking: r.resolve())
}
let repository = container.resolve(Repository.self)!
Hierarchical DI
- Needle와 같은 프레임워크