apple/DesignPattern, Architecture
iOS VIPER 아키텍처 정리
lgvv
2024. 12. 3. 02:29
iOS VIPER 아키텍처 정리
VIPER Architecture는 Jeff Gilbert와 Conrad Stoll이 클린 아키텍처를 iOS 앱 개발에 특별하게 적용하기 위해서 개발하고, 대중화
해당 포스팅에서는 조금 더 최신화 형태로 구성
예제 파일
VIRER란?
- View
- ViewController로 xib/storyboard
- Interactor:
- 데이터, 네트워킹 및 비즈니스 로직을 담당.
- Presenter
- View와 Interactor 사이에 데이터를 전달하고, 사용자 이벤트를 처리하여 라우터를 호출.
- VIPER의 다른 모든 객체들과 통신하는 유일한 객체
- Entity
- 데이터 객체이며 데이터 접근은 Interactor에서 가능
- VIPER 모듈 사이에서 화면 전환을 담당Router
- Builder (해당 포스팅에서 구현에 추가된 개념)
- RIBs 처럼 하나의 VIPER 모듈을 만드는 Builder 객체
- 의존성 문제 해결, 빌드 성능 개선 등을 위해 만들어짐.
VIRER의 폴더 구조
VIPER 폴더 구조는 아래처럼 구성되며, 의존성이 적은 순서대로 아래 코드 예제를 진행.
위의 이미지와 다르게 Builder를 통해 객체를 생성
Entity 코드
간단한 객체로 인터렉터에 의해서 데이터를 가공
// MARK: - Entity
public struct HomeEntity {
public let title: String
public let description: String
public init(title: String, description: String) {
self.title = title
self.description = description
}
}
Interactor 코드
Interactor의 인터페이스는 Input과 Output으로 구분하여 Interactor가 비대해지지 않도록 구성
- Input: 인터렉터로 들어오는 이벤트 (인터렉터를 호출)
- Output: 인터렉터가 비즈니스 로직 등의 처리를 끝내고 Presenter로 전달
// MARK: - Interactor
import Model
protocol HomeInteractorInputInterface {
func fetch()
}
protocol HomeInteractorOutputInterface: AnyObject {
func updateHomeLabel(title: String, description: String)
}
class HomeInteractor: HomeInteractorInputInterface {
weak var output: HomeInteractorOutputInterface?
func fetch() {
let data = HomeEntity(title: "Viper", description: "Home")
output?.updateHomeLabel(title: data.title, description: data.description)
}
}
Router 코드
기본적으로 화면전환을 담당
- 해당 화면이 현재 UINavigationContoller로 되어 있어서 UINavigationController를 들고 있음
- RIBs의 didBecomeActive처럼 해당 예제에서는 ViewController의 ViewDidLoad에서 navgiationContoller를 넣어줌.
import UIKit
import HomeDetail
protocol HomeRouterInterface {
var navigationContoller: UINavigationController? { get }
func pushToDetail()
}
class HomeRouter: HomeRouterInterface {
var navigationContoller: UINavigationController?
init() {}
func pushToDetail() {
let view = HomeDetailBuilder().build()
navigationContoller?.pushViewController(view, animated: true)
}
}
View 코드
ViewController로 View를 담당
- presenter로 사용자에게 들어온 이벤트를 전송
- HomeViewInterface를 통해 View가 이벤트를 수신
import UIKit
// MARK: - View
protocol HomeViewInterface: AnyObject {
func updateHomeLabel(title: String, description: String)
}
final class HomeViewController: UIViewController {
var presenter: HomePresenterInterface
override func viewDidLoad() {
super.viewDidLoad()
presenter.viewDidLoad(self)
configureUI()
}
init(presenter: HomePresenterInterface) {
self.presenter = presenter
super.init(nibName: nil, bundle: nil)
configureUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - UIComponents
private lazy var button: UIButton = {
$0.setTitle("디테일 화면 이동 버튼", for: .normal)
$0.addTarget(self, action: #selector(didTapButton), for: .touchUpInside)
return $0
}(UIButton())
@objc
func didTapButton() {
presenter.didTapButton()
}
private let label: UILabel = {
$0.lineBreakMode = .byWordWrapping
$0.numberOfLines = 0
$0.textAlignment = .center
$0.textColor = .black
$0.font = .systemFont(ofSize: 24, weight: .semibold)
return $0
}(UILabel())
private func configureUI() {
view.backgroundColor = .blue
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
button.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
view.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: button.bottomAnchor, constant: 20),
label.leadingAnchor.constraint(equalTo: view.leadingAnchor),
label.trailingAnchor.constraint(equalTo: view.trailingAnchor)
])
}
}
extension HomeViewController: HomeViewInterface {
func updateHomeLabel(title: String, description: String) {
label.text = "\(title)\n\(description)"
}
}
Presenter 코드
VIPER 모듈의 다른 객체를 모두 알고 있음
- 이벤트 간의 중재자 역할을 수행
import UIKit
// MARK: - Presenter
protocol HomePresenterInterface: AnyObject {
func viewDidLoad(_ viewControllable :HomeViewInterface)
func didTapButton()
}
class HomePresenter: HomePresenterInterface {
weak var view: HomeViewInterface?
var interactor: HomeInteractorInputInterface
var router: HomeRouterInterface
init(interactor: HomeInteractorInputInterface, router: HomeRouterInterface) {
self.interactor = interactor
self.router = router
}
func viewDidLoad(_ viewControllable: HomeViewInterface) {
self.view = viewControllable
interactor.fetch()
}
func didTapButton() {
router.pushToDetail()
}
}
extension HomePresenter: HomeInteractorOutputInterface {
func updateHomeLabel(title: String, description: String) {
view?.updateHomeLabel(title: title, description: description)
}
}
Builder 코드
VIPER 모듈의 한 객체를 생성하는 역할을 담당
- 빌드시간 개선을 과정에서 의존성 관리를 위해 필요
- BuilderInterface에 의존
import UIKit
public protocol HomeBuilderInterface {
func build() -> UINavigationController
}
public struct HomeBuilder: HomeBuilderInterface {
public init() {}
public func build() -> UINavigationController {
let router = HomeRouter()
let interactor = HomeInteractor()
let presenter = HomePresenter(
interactor: interactor,
router: router
)
interactor.output = presenter
let navigationController = UINavigationController(
rootViewController: HomeViewController(presenter: presenter)
)
router.navigationContoller = navigationController
return navigationController
}
}
VIPER 모듈 사용하는 방법
Builder를 통해 생성한 객체를 해당 형태로 사용하면 끝.
let builder: HomeBuilderInterface = HomeBuilder()
let rootViewController = builder.build()
window?.rootViewController = rootViewController
정리
VIPER 장점
- 독립적인 모듈 및 낮은 결합도로 인한 유지보수성 증대
- 테스트하기 좋은 코드
- 모듈화 용이성
VIPER의 아쉬운점
- Presenter가 로직을 주도
- 따라서 Presenter가 없는 형태로 구성하기 여려워 완전한 모듈화의 어려움
- 이를 보완하고자 RIBs 등장
- RIBs는 비지니스 로직(Interactor)가 주도
(참고)