[아키텍처] Clean Swift(VIP)

정말 클린한 것은 없는게 아닐까?

Clean Swift?

  • ViewController
  • Interactor
  • Presenter
  • Worker
  • Router
  • Models

Clean Architecture 를 기반으로 IOS에 맞게 재해석한 결과물이 Clean Swift라고 한다.
ViewController, Interactor, Presenter의 앞글자를 따서 VIP 패턴이라고도 불린다.


공식 홈페이지에 나와있는 이미지로
원 형태의 데이터 흐름이 만들어지는 것이 가장 큰 특징이다.

ViewController
화면 표현과 사용자와의 상호작용 담당

Interactor
비즈니스 로직 담당

Presenter
Interactor에서 얻은 결과물을 View에 표현 가능한 형태로 변환 담당

Worker
Interactor에서 복잡한 로직을 분리하거나 확장하는 용도

Router
화면 이동에 대한 로직 담당

Models
UseCase 및 케이스 별 계층 간 데이터 구조 정의

좀 더 자세한 설명은 이곳을 확인해주세요.

구현

Clean Swift는 파일 분리를 통해 역할 or 관심사 분리를 하고자 하는 면이 강한 것 같다.
그런 점이 규모가 작은 개발을 할 때는 효율성이 떨어지는 측면이 있다.

다만 파일로 분리되다 보니 각 계층의 기능은 명확하게 구별 할 수 있다.

ViewController

검색 이벤트 발생시 해당 비즈니스 로직이 구현된 함수를 호출한다.

func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
    interactor?.fetchRepositories(request: Search.FetchRepositories.Request(text: searchBar.text))
}

리스트 갱신을 위한 함수를 구현한다.

func displayRepositories(viewModel: Search.FetchRepositories.ViewModel) {
    DispatchQueue.main.async {
        self.displayedRepositories = viewModel.fetchedRepositories
        self.tableView.reloadData()
    }
}

Interactor

비즈니스 로직을 담당하고, 세부적인 부분은 Worker에서 처리하도록 구현한다.

func fetchRepositories(request: Search.FetchRepositories.Request) {
    worker = SearchWorker()
    worker?.fetchRepositories(text: request.text, completion: { result in
        switch result {
        case .success(let response):
            self.repositories = response.items
            self.presenter?.presentFetchedRepositories(response: response)
                
        case .failure(let error):
            print("error : \(error.localizedDescription)")
            let response = Search.FetchRepositories.Response()
            self.repositories = response.items
            self.presenter?.presentFetchedRepositories(response: response)
        }
    })
}

Presenter

interactor에서 넘겨받은 데이터를 view에 맞게 변환해 전달한다.

func presentFetchedRepositories(response: Search.FetchRepositories.Response) {
    let viewModel = Search.FetchRepositories.ViewModel(fetchedRepositories: response.items)
    self.viewController?.displayRepositories(viewModel: viewModel)
}

Worker

interactor에서 필요한 세부적인 비즈니스 로직을 구현한다.

func fetchRepositories(text: String?, completion: @escaping(Result<Search.FetchRepositories.Response, Error>) -> Void) {
    var urlComponents = URLComponents(string: "https://api.github.com/search/repositories")
    urlComponents?.queryItems = [
        URLQueryItem(name: "q", value: text)
    ]
        
    URLSession.shared.dataTask(with: (urlComponents?.url)!) { data, response, error in
        guard error == nil else {
            completion(.failure(error!))
            return
        }
            
        if let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data {
            do {
                let searchResponse = try JSONDecoder().decode(Search.FetchRepositories.Response.self, from: data)
                completion(.success(searchResponse))
            } catch {
                print("error : \(error.localizedDescription)")
                completion(.failure(error))
            }
        }
    }.resume()
}

Router

Segue를 통해 화면이동을 하거나 코드적으로 수행할 수 있도록 구현이 가능하다.
이때 Swinject와 같은 DI를 적용한다면 ViewController의 setup을 분리해낼 수 있다.

또한 passData함수를 통해 Scene간 데이터 전달이 가능하다.

func routeToSearchDetail(segue: UIStoryboardSegue?) {
    if let segue = segue {
        let destinationVC = segue.destination as! SearchDetailViewController
        var destinationDS = destinationVC.router!.dataStore!
        passDataToSearchDetail(source: dataStore!, destination: &destinationDS)
    } else {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let destinationVC = storyboard.instantiateViewController(withIdentifier: "SearchDetailViewController") as! SearchDetailViewController
        var destinationDS = destinationVC.router!.dataStore!
        passDataToSearchDetail(source: dataStore!, destination: &destinationDS)
        navigateToSearchDetail(source: viewController!, destination: destinationVC)
    }
}

Models

해당 Scene에서 사용되는 UseCase를 정의하고, 해당 케이스의 계층별 데이터 구조를 정의한다.

enum Search {
    // MARK: Use cases
    enum FetchRepositories {
        struct Request {
            let text: String?
        }
        struct Response: Codable {
            var total_count: Int = 0
            var incomplete_results: Bool = false
            var items: [Repository] = []
        }
        struct ViewModel {
            let fetchedRepositories: [Repository]
        }
    }
}

결과

전체 코드는 깃허브에 있습니다.

좋은 웹페이지 즐겨찾기