VIPER 패턴 - Team-HGD/SniffMEET GitHub Wiki

-
V (View)
The user interface, responsible for displaying data and receiving user input.
ViewController, View를 의미하며 사용자가 보는 화면을 보여주거나 사용자 인터렉션을 Presenter에 전달한다.
-
I (Interactor)
Contains the business logic and communicates with data sources.
비지니스 로직을 처리하여 데이터를 준비하거나 외부에서 데이터를 가져온다.
-
P (Presenter)
Acts as a mediator between the View and Interactor, handling user input and updating the View.
View에서 이벤트를 전달받아 Interactor에 요청하고 View에 data와 함께 UI Update 요청을 보내거나 Router를 통한 화면 이동을 처리한다.
View, Router, Interactor에 대한 의존성이 있다.
-
E (Entity)
Represents the data model or entities.
-
R (Router or Wireframe)
Manages navigation between screens.
화면 전환을 담당하며, 다른 화면으로의 네비게이션을 처리한다. 의존성을 주입해주는 부분이기도 하다.
-
View → Presenter
View는 사용자 입력이 발생할 때 이를 Presenter에 전달한다.
-
Presenter → Interactor
Presenter는 View의 요청에 따라 필요한 데이터 처리를 Interactor에 요청한다.
-
Interactor → Presenter
Interactor는 데이터 처리가 끝나면 Presenter에 처리 결과를 전달한다.
-
Presenter → View
Presenter는 Interactor에서 받은 데이터를 가공하여 View에 전달한다.
-
Presenter → Router
Presenter는 네비게이션이 필요할 때 Router에 요청하여 화면 전환을 수행한다.
-
모듈화: 각 컴포넌트가 독립적이고 역할이 분리되어 있어 유지보수와 확장이 용이하다.
-
테스트 용이성: 각 컴포넌트가 독립적이므로 단위 테스트가 용이하다.
각 요소를 protocol을 통해 의존성 분리 작업을 진행하면 용이성을 올릴 수 있다.
-
유연성: 각 레이어가 독립적으로 개발되므로, 특정 레이어를 교체하거나 수정하는 것이 상대적으로 쉽다.
- 복잡성 증가: MVC, MVVM보다 레이어가 많아져서 초기 구현이 복잡성이 높다. 작은 프로젝트에서는 과도할 수 있다.
- 상호작용이 많음: 컴포넌트 간의 상호작용이 많아져서, 각 컴포넌트가 잘못 연결되면 디버깅이 어려울 수 있다. 특히 View와 Presenter는 서로를 모두 알고 있다는 점에서 양방향 통신을 수행하고 있다.
VIPER에 대한 명확한 가이드가 존재하지 않는다. 데이터 전달 방식이나 Usecase를 두어 비지니스 로직을 관리하는 것을 추가하는 등 커스텀하는 건 자유일 듯!
[GitHub - amitshekhariitbhu/iOS-Viper-Architecture: This repository contains a detailed sample app that implements VIPER architecture in iOS using libraries and frameworks like Alamofire, AlamofireImage, PKHUD, CoreData etc.](https://github.com/amitshekhariitbhu/iOS-Viper-Architecture)
게시물 리스트를 보여주는 화면에 대한 Viper 구조
protocol PostListViewProtocol: class {
var presenter: PostListPresenterProtocol? { get set }
// PRESENTER -> VIEW
func showPosts(with posts: [PostModel])
func showError()
func showLoading()
func hideLoading()
}
-
PostListViewProtocol
Presenter가 구현한 함수로 뷰가 보여주는 방식을 결정한다.
Presenter에서 보여질 수 있는 방식을 함수로 캡슐화해서 손쉽게 사용 가능
import UIKit
import PKHUD
class PostListView: UIViewController {
@IBOutlet weak var tableView: UITableView!
var presenter: PostListPresenterProtocol?
var postList: [PostModel] = []
override func viewDidLoad() {
super.viewDidLoad()
presenter?.viewDidLoad()
tableView.tableFooterView = UIView()
}
}
extension PostListView: PostListViewProtocol {
func showPosts(with posts: [PostModel]) {
postList = posts
tableView.reloadData()
}
func showError() {
HUD.flash(.label("Internet not connected"), delay: 2.0)
}
func showLoading() {
HUD.show(.progress)
}
func hideLoading() {
HUD.hide()
}
}
protocol PostListPresenterProtocol: class {
var view: PostListViewProtocol? { get set }
var interactor: PostListInteractorInputProtocol? { get set }
var wireFrame: PostListWireFrameProtocol? { get set }
// VIEW -> PRESENTER
func viewDidLoad()
func showPostDetail(forPost post: PostModel)
}
protocol PostListInteractorOutputProtocol: class {
// INTERACTOR -> PRESENTER
func didRetrievePosts(_ posts: [PostModel])
func onError()
}
-
PostListPresenterProtocol 프로토콜 구현체
View의 이벤트에 따라 View를 보여주거나 Router를 활용하여 화면전환을 수행한다.
-
PostListInteractorOutputProtocol 프로토콜 구현체
Interactor의 output에 따라 어떤 것을 보여줄지 presenter가 결정한다.
class PostListPresenter: PostListPresenterProtocol {
weak var view: PostListViewProtocol?
var interactor: PostListInteractorInputProtocol?
var wireFrame: PostListWireFrameProtocol?
func viewDidLoad() {
view?.showLoading()
interactor?.retrievePostList()
}
func showPostDetail(forPost post: PostModel) {
wireFrame?.presentPostDetailScreen(from: view!, forPost: post)
}
}
extension PostListPresenter: PostListInteractorOutputProtocol {
func didRetrievePosts(_ posts: [PostModel]) {
view?.hideLoading()
view?.showPosts(with: posts)
}
func onError() {
view?.hideLoading()
view?.showError()
}
}
protocol PostListInteractorInputProtocol: class {
var presenter: PostListInteractorOutputProtocol? { get set }
var localDatamanager: PostListLocalDataManagerInputProtocol? { get set }
var remoteDatamanager: PostListRemoteDataManagerInputProtocol? { get set }
// PRESENTER -> INTERACTOR
func retrievePostList()
}
protocol PostListDataManagerInputProtocol: class {
// INTERACTOR -> DATAMANAGER
}
-
PostListInteractorInputProtocol 구현체
Repository에서 데이터를 받아와서 이를 presenter에 해당 정보를 전달한다.
datamanager → interactor → presenter ( in interactor) || presenter → view (in presenter)
class PostListInteractor: PostListInteractorInputProtocol {
weak var presenter: PostListInteractorOutputProtocol?
var localDatamanager: PostListLocalDataManagerInputProtocol?
var remoteDatamanager: PostListRemoteDataManagerInputProtocol?
func retrievePostList() {
do {
if let postList = try localDatamanager?.retrievePostList() {
let postModelList = postList.map() {
return PostModel(id: Int($0.id), title: $0.title!, imageUrl: $0.imageUrl!, thumbImageUrl: $0.thumbImageUrl!)
}
if postModelList.isEmpty {
remoteDatamanager?.retrievePostList()
}else{
presenter?.didRetrievePosts(postModelList)
}
} else {
remoteDatamanager?.retrievePostList()
}
} catch {
presenter?.didRetrievePosts([])
}
}
}
extension PostListInteractor: PostListRemoteDataManagerOutputProtocol {
func onPostsRetrieved(_ posts: [PostModel]) {
presenter?.didRetrievePosts(posts)
for postModel in posts {
do {
try localDatamanager?.savePost(id: postModel.id, title: postModel.title, imageUrl: postModel.imageUrl, thumbImageUrl: postModel.thumbImageUrl)
} catch {
}
}
}
func onError() {
presenter?.onError()
}
}
protocol PostListWireFrameProtocol: class {
static func createPostListModule() -> UIViewController
// PRESENTER -> WIREFRAME
func presentPostDetailScreen(from view: PostListViewProtocol, forPost post: PostModel)
}
-
PostListWireFrameProtocol
의존성 주입 역할 수행 (Component, Builder) → 이래서 RIBs가 생긴 듯 하다.
화면 전환 수행
class PostListWireFrame: PostListWireFrameProtocol {
class func createPostListModule() -> UIViewController {
let navController = mainStoryboard.instantiateViewController(withIdentifier: "PostsNavigationController")
if let view = navController.childViewControllers.first as? PostListView {
let presenter: PostListPresenterProtocol & PostListInteractorOutputProtocol = PostListPresenter()
let interactor: PostListInteractorInputProtocol & PostListRemoteDataManagerOutputProtocol = PostListInteractor()
let localDataManager: PostListLocalDataManagerInputProtocol = PostListLocalDataManager()
let remoteDataManager: PostListRemoteDataManagerInputProtocol = PostListRemoteDataManager()
let wireFrame: PostListWireFrameProtocol = PostListWireFrame()
view.presenter = presenter
presenter.view = view
presenter.wireFrame = wireFrame
presenter.interactor = interactor
interactor.presenter = presenter
interactor.localDatamanager = localDataManager
interactor.remoteDatamanager = remoteDataManager
remoteDataManager.remoteRequestHandler = interactor
return navController
}
return UIViewController()
}
static var mainStoryboard: UIStoryboard {
return UIStoryboard(name: "Main", bundle: Bundle.main)
}
func presentPostDetailScreen(from view: PostListViewProtocol, forPost post: PostModel) {
let postDetailViewController = PostDetailWireFrame.createPostDetailModule(forPost: post)
if let sourceView = view as? UIViewController {
sourceView.navigationController?.pushViewController(postDetailViewController, animated: true)
}
}
}
해당 프로젝트에서는 각 역할 별로 Entity를 구분해서 사용하지 않았다.
해당 프로젝트에서 DTO, Model, Entity를 구분해서 사용할 필요가 있을까?
사용하지 않는다면 하나의 Entity에 너무 의존적인건 아닐까?
-
양방향 통신
View ↔ Presenter
Presenter ↔ Interactor
위와 같이 양방향으로 통신(?)이 이루어지는 부분이 많다.
따라서 서로 다른 방향 로직 간의 구분할 필요가 있다. ( 가독성 측면에서 또는 테스트 용이성을 위해서)
-
Interactor
Interactor는 화면전환을 수행하고, 비지니스로직을 수행하고 Repository에 대한 정보도 가지고 있다.
여러 역할을 수행하고 있다. 이를 프로토콜로 구분할 필요가 있다. 유즈케이스를 이용하여 비지니스 로직이 눈에 한번에 보일 수 있게끔 작성해도 좋을 거 같다.