Files
2nd/10_Wiki/Topics/Architecture/VIP.md
T
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

7.7 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-vip VIP (Clean Swift) 10_Wiki/Topics verified self
Clean Swift
VIP Cycle
VIP Architecture
none A 0.9 applied
architecture
ios
swift
clean-architecture
mvc-alternative
2026-05-10 pending
language framework
Swift 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)

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)

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)

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)

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)

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)

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)

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)

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

🤖 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