UIKit에 Clean Swift 적용하기
Clean Swift
- Clean Swift는 Clean Architecture를 iOS 앱 개발에 적용한 것입니다.
- 각 Scene은 기능별로 만들어지고, 아래의 컴포넌트로 이뤄져 있습니다.
VIP와 Models, Router, Worker 정도로 이뤄져 있습니다.
- ViewController(+ View)
- Interactor
- Presenter
- Models
- Router
- Worker
- Configurator
Views
- ViewController는 Interactor에 Request하고 Presenter로부터 Response를 전달 받습니다.
- 전환이 필요한 경우에는 Router와 통신하기도 합니다.
ViewController의 역할은 두 가지 있습니다:
- 사용자의 액션을 받습니다.
- 데이터를 화면에 보여줍니다.
ViewController은 Display Logic 프로토콜을 가지고 있습니다:
- 해당 프로토콜은 화면에 데이터를 바인딩 해주는 역할을 합니다.
protocol MunziDisplayLogic: AnyObject {
func displayWithoutData()
func displayWithData(viewModel: MunziModel.Air.ViewModel)
}
final class MunziViewController: UIViewController {
var interactor: (MunziBusinessLogic & MunziDataStore)?
var router: (NSObjectProtocol & MunziRoutingLogic & MunziDataPassing)?
...
required init?(coder: NSCoder) {
super.init(coder: coder)
setup()
}
override func viewDidLoad() {
super.viewDidLoad()
interactor?.fetch()
}
...
private func setup() {
let viewController = self
let interactor = MunziInteractor(networkWorker: NetworkWorker())
let presenter = MunziPresenter()
let router = MunziRouter()
viewController.interactor = interactor
viewController.router = router
interactor.presenter = presenter
presenter.viewController = viewController
router.viewController = viewController
router.dataStore = interactor
}
}
extension MunziViewController: MunziDisplayLogic {
func reloadWithoutData() {
DispatchQueue.main.async { [weak self] in
// UI 관련 코드
}
}
func reloadWithData(viewModel: MunziModel.Air.ViewModel) {
DispatchQueue.main.async { [weak self] in
// UI 관련 코드
}
}
}
Interactor
- Interactor는 중재자의 역할을 합니다.
Interactor의 역할에는 3가지가 있습니다:
- ViewController와 통신해서 Worker에 필요한 Request를 받아옵니다.
- Worker에서 일을 진행하기 전에 모든 값이 제대로 전송 되었는지 확인합니다.
- Worker로부터 Response를 받으면 Presenter로 전달합니다.
Interactor는 두 가지 포로토콜을 가지고 있습니다:
- Business Logic 프로토콜에는 ViewController에서 사용하고 싶은 Interactor의 함수가 있습니다.
- Data Store 프로토콜에는 상태 프로퍼티들이 있고, Router와 ViewController 사이에 통신하는데 사용됩니다.
protocol MunziBusinessLogic {
func fetch()
...
}
protocol MunziDataStore {
var location: LocationModel { get set }
...
}
final class MunziInteractor: MunziDataStore {
var presenter: MunziPresentationLogic?
var networkWorker: NetworkProtocol
var location = LocationModel()
...
}
extension MunziInteractor: MunziBusinessLogic {
func fetch() {
Task {
presenter?.presentWithoutData()
let result = await networkWorker.fetch()
...
presenter?.presentWithData(response: air)
}
}
...
}
Presenter
Presenter의 역할은 두 가지 있습니다:
- Interactor에서 받은 Response를 ViewModel로 포맷해서 ViewController로 전달합니다.
- 데이터가 사용자에게 표시되는 방법을 결정합니다.
Presenter의 프로토콜에서는:
- ViewController에서 선언된 델리게이트 메서드를 호출하여, 이를 통해 ViewModel를 전달합니다.
protocol MunziPresentationLogic {
func presentWithoutData()
func presentWithData(response: MunziModel.Air.Response)
}
final class MunziPresenter: MunziPresentationLogic {
weak var viewController: MunziDisplayLogic?
func presentWithoutData() {
viewController?.reloadWithoutData()
}
func presentWithData(response: MunziModel.Air.Response) {
...
viewController?.displayWithData(viewModel: viewModel)
}
}
Model
각 Model은 Request, Response, ViewModel을 포함합니다:
- Request: API request (ViewController → Interactor)
- Response: API response (Interactor → Presenter)
- ViewModel: UI를 구성하는데 필요한 데이터들 (Presenter → ViewController)
enum MunziModel {
struct AirModel: Decodable {
let response: Body
struct Body: Decodable {
let body: Content
}
struct Content: Decodable {
let items: [Item]
}
struct Item: Decodable {
let pm10Value: String
let pm25Value: String
}
}
enum Air {
struct Request {}
struct Response {
let pm10: Double
let pm25: Double
}
struct ViewModel {
let pm10: Double
let pm25: Double
}
}
}
Router
- Router는 ViewController 간의 전환과 데이터 전달을 처리합니다.
Router는 두 가지 포로토콜을 갖고 있습니다:
- Routing Logic 프로토콜은 화면 전환에 사용되는 함수를 갖고 있습니다.
- Data Passing 프로토콜은 ViewController 사이에 전달해야 하는 데이터를 갖고 있습니다.
protocol MunziRoutingLogic {
func routeToSettings()
}
protocol MunziDataPassing {
var dataStore: MunziDataStore? { get }
}
final class MunziRouter: NSObject, MunziRoutingLogic, MunziDataPassing {
weak var viewController: MunziViewController?
var dataStore: MunziDataStore?
func routeToSettings() {
let destinationVC = SettingsViewController.configure()
...
passDataToSettings(source: dataStore, destination: &destinationDS)
navigateToSettings(source: viewController, destination: destinationVC)
}
func navigateToSettings(source: MunziViewController, destination: SettingsViewController) {
...
source.show(destination, sender: nil)
}
func passDataToSettings(source: MunziDataStore, destination: inout SettingsDataStore) {
...
}
}
Worker
자세한 구현은 Swift의 Network Layer을 참고해주세요
- Worker는 API 혹은 CoreData 등의 데이터 소스에 대한 Request와 Response를 관리합니다.
- Interactor에게 데이터를 받아 Request를 요청하거나, Response로 받은 데이터를 넘겨줍니다.