UIKit에 Clean Swift 적용하기

Clean Swift

  • Clean Swift는 Clean Architecture를 iOS 앱 개발에 적용한 것입니다.
  • 각 Scene은 기능별로 만들어지고, 아래의 컴포넌트로 이뤄져 있습니다.
uikit-clean-swift

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로 받은 데이터를 넘겨줍니다.

참고