--- id: wiki-2026-0508-vip title: VIP (Clean Swift) category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Clean Swift, VIP Cycle, VIP Architecture] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [architecture, ios, swift, clean-architecture, mvc-alternative] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: Swift framework: UIKit/SwiftUI --- # VIP (Clean Swift) ## 매 한 줄 > **"매 unidirectional View → Interactor → Presenter cycle, 매 use-case-per-scene granularity"**. 매 Raymond Law 의 Clean Swift (2016) — 매 Robert Martin 의 Clean Architecture 의 iOS-specific adaptation. 매 MVC 의 Massive ViewController 문제 해결 + MVVM 의 testability 강화 + 매 scene-based isolation. ## 매 핵심 ### 매 VIP Cycle - **View** (ViewController): user input 수신 → Request DTO 생성 → Interactor 호출. Response DTO 수신 → display. - **Interactor**: business logic. Worker (network/DB) 호출 → Response DTO → Presenter. - **Presenter**: Response DTO → ViewModel 변환 (formatting, localization) → View update. - **Router**: scene transition (push/present/dismiss) + data passing 사이 scenes. ### 매 Models (boundary types) - `Request`: View → Interactor input (raw user data). - `Response`: Interactor → Presenter output (raw business data). - `ViewModel`: Presenter → View output (formatted display strings). ### 매 Scene 단위 - 매 1 screen = 1 scene = 5-7 files (VC, Interactor, Presenter, Router, Models, Worker, Configurator). 매 Xcode template 으로 자동 생성. ### 매 응용 1. iOS 대규모 앱 — Uber, Kickstarter, Trivago 의 production 사용. 2. Legacy MVC 의 점진적 migration — scene-by-scene 교체. 3. Snapshot testing + unit testing — 매 deterministic ViewModel. ## 💻 패턴 ### Protocols (boundary contracts) ```swift protocol ListBusinessLogic: AnyObject { func fetchItems(request: List.FetchItems.Request) } protocol ListPresentationLogic: AnyObject { func presentItems(response: List.FetchItems.Response) } protocol ListDisplayLogic: AnyObject { func displayItems(viewModel: List.FetchItems.ViewModel) } ``` ### Models (namespaced DTOs) ```swift enum List { enum FetchItems { struct Request { let query: String let page: Int } struct Response { let items: [Item] // domain model let totalCount: Int } struct ViewModel { struct DisplayedItem { let title: String let subtitle: String } let items: [DisplayedItem] let totalLabel: String // "Showing 20 of 100" } } } ``` ### ViewController (View) ```swift final class ListViewController: UIViewController, ListDisplayLogic { var interactor: ListBusinessLogic? var router: ListRoutingLogic? private var displayedItems: [List.FetchItems.ViewModel.DisplayedItem] = [] override func viewDidLoad() { super.viewDidLoad() let request = List.FetchItems.Request(query: "", page: 1) interactor?.fetchItems(request: request) } func displayItems(viewModel: List.FetchItems.ViewModel) { displayedItems = viewModel.items title = viewModel.totalLabel tableView.reloadData() } } ``` ### Interactor (business logic) ```swift final class ListInteractor: ListBusinessLogic { var presenter: ListPresentationLogic? var worker: ListWorker = ListWorker() func fetchItems(request: List.FetchItems.Request) { worker.fetch(query: request.query, page: request.page) { [weak self] result in switch result { case .success(let items): let response = List.FetchItems.Response(items: items, totalCount: items.count) self?.presenter?.presentItems(response: response) case .failure(let error): self?.presenter?.presentError(error) } } } } ``` ### Presenter (formatting) ```swift final class ListPresenter: ListPresentationLogic { weak var viewController: ListDisplayLogic? func presentItems(response: List.FetchItems.Response) { let displayed = response.items.map { List.FetchItems.ViewModel.DisplayedItem( title: $0.name.uppercased(), subtitle: DateFormatter.localizedString(from: $0.createdAt, dateStyle: .medium, timeStyle: .none) ) } let viewModel = List.FetchItems.ViewModel( items: displayed, totalLabel: "Showing \(displayed.count) of \(response.totalCount)" ) viewController?.displayItems(viewModel: viewModel) } } ``` ### Router (navigation) ```swift protocol ListRoutingLogic { func routeToDetail(id: String) } final class ListRouter: ListRoutingLogic { weak var viewController: ListViewController? func routeToDetail(id: String) { let detail = DetailViewController() DetailConfigurator.configure(detail, with: id) viewController?.navigationController?.pushViewController(detail, animated: true) } } ``` ### Configurator (DI wiring) ```swift extension ListViewController { func configureVIP() { let interactor = ListInteractor() let presenter = ListPresenter() let router = ListRouter() self.interactor = interactor self.router = router interactor.presenter = presenter presenter.viewController = self router.viewController = self } } ``` ### Unit test (Presenter) ```swift func test_presentItems_formatsCorrectly() { let spy = ListDisplayLogicSpy() let sut = ListPresenter() sut.viewController = spy let response = List.FetchItems.Response(items: [Item(name: "foo", createdAt: Date())], totalCount: 1) sut.presentItems(response: response) XCTAssertEqual(spy.displayedViewModel?.items.first?.title, "FOO") XCTAssertEqual(spy.displayedViewModel?.totalLabel, "Showing 1 of 1") } ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | 대규모 iOS 팀, 신규 프로젝트 | VIP — 매 scene isolation 큰 가치 | | 작은 앱, prototype | MVC/MVVM — VIP boilerplate 의 X | | SwiftUI primary | TCA / MVVM — VIP 의 ViewController-centric design 의 어색 | | Cross-team consistency 필요 | VIP — Xcode template 으로 enforced 매 same structure | | Legacy MVC 의 점진적 cleanup | VIP scene-by-scene migration — 매 새로운 screen 부터 | **기본값**: 매 SwiftUI 우선이면 [[The_Composable_Architecture]] (TCA), UIKit 의 대규모 codebase 면 VIP. ## 🔗 Graph - 부모: [[Clean Architecture]] · [[SOLID]] - 응용: [[UIKit]] - Adjacent: [[Unit Tests (단위 테스트)]] · [[Dependency Injection]] ## 🤖 LLM 활용 **언제**: 매 새 iOS scene 의 boilerplate 생성 (5-7 파일), Request/Response/ViewModel DTO 의 design, Presenter formatting logic test 작성. **언제 X**: 매 SwiftUI-only 앱 (TCA 우선), 매 1-2 screen 의 prototype (MVC 의 충분). ## ❌ 안티패턴 - **Massive Interactor**: 매 worker 분리 의 X — Interactor 의 network/DB 직접 호출 하지 않음. - **ViewModel 의 domain logic**: 매 Presenter 의 단순 formatting 만 — 매 business decision 의 X. - **Router 의 data-only passing 의 X**: 매 다음 scene 의 Configurator 호출 — DI wiring 의 Router 의 책임. - **Cycle skip**: 매 View → Interactor → Presenter → View 의 strict 의 — 매 shortcut View → Presenter 직접 호출 의 anti-pattern. ## 🧪 검증 / 중복 - Verified (Clean-Swift.com Raymond Law 공식 docs, Uber engineering blog). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — full VIP/Clean Swift architecture with 7 patterns and decision matrix |