본문 바로가기

개발/디자인 패턴

[iOS] 디자인 패턴 - VIPER 화면 전환 예제

반응형

앞선 글은 아래에 있다.

 

[iOS] 디자인패턴 - VIPER

Clean Architecture 이란? iOS 도메인에 있는 Clean Architecture의 다양한 구성 요소 사이의 데이터 흐름을 살펴봅시다. 교차 경계 섹션에서는 다음과 같이 데이터 흐름이 단반향이어야 하는 방법에 대해 논의했..

djgmd1021.tistory.com

 

VIPER에서 화면 전환인 View -> Router 에 대해 알아보려 한다.

아래 화면은 새롭게 추가한 시작 화면이다. 
이곳에서 사용자는 Candy Store 뷰를 클릭하면, 다음 화면으로 넘어가도록 구성했다. 

 

화면 이동은 Router담당이니 View에서 바로 Router로 보내버린다. 
새롭게 추가된 화면은 Main이다.

 

MainEntity

struct StoreEntity{
    let StoreName : String
    let StoreImage : String
}

 

MainView

import UIKit

protocol MainViewProtocol : class {
    var presenter: MainPresenterProtocol? {get set}
    func setCandyStore(viewmodel: StoreViewModel)
}

class MainView : UIViewController {
    @IBOutlet weak var candyView: UIView!
    @IBOutlet weak var candyName: UILabel!
    @IBOutlet weak var candyImage: UIImageView!
    
    var presenter: MainPresenterProtocol? // 프레센터 연결
    
    override func viewDidLoad() {
        super.viewDidLoad()
        presenter?.fetch()
    }
}

extension MainView : MainViewProtocol {
    func setCandyStore(viewmodel: StoreViewModel) {
        let candyGesture : UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(touchCandyView(sender:)))
        candyName.text = viewmodel.name
        candyImage.image = UIImage(named: viewmodel.imageName)
        
        self.candyView.addGestureRecognizer(candyGesture)
    }
       
    @objc func touchCandyView(sender: UITapGestureRecognizer){
        // 화면 전환 코드
    }
}

 

MainPresenter

protocol MainPresenterProtocol : class {
    func fetch()
    func mergeInteractionCandy(_ interactor : MainInteractorProtocol, FetchCandy storeObject: StoreEntity)
}

struct StoreViewModel {
    let name: String
    let imageName: String
}

class MainPresenter {
    weak var view : MainViewProtocol? // 뷰 연결 
    var router : RouterProtocol? // 라우터 연결
    var interactor : MainInteractorProtocol? // 인터렉터 연결
}

extension MainPresenter : MainPresenterProtocol {
    func fetch() {
        interactor?.fetchStore()
    }
   
    func mergeInteractionCandy(_ interactor: MainInteractorProtocol, FetchCandy storeObject: StoreEntity) {
        let viewModel = StoreViewModel(name: storeObject.StoreName, imageName: storeObject.StoreImage)
        view?.setCandyStore(viewmodel: viewModel)
    }
}

 

MainInteractor

protocol MainInteractorProtocol {
    func fetchStore()
}

class MainInteractor : MainInteractorProtocol {
    private var candyStore : StoreEntity?
    private let storeAPIWorker : APIStoreWorkerProtocol
    
    var presenter : MainPresenterProtocol? // 프레센터 연결
    
    required init(withAPIWorker apiworker : APIStoreWorkerProtocol) {
        self.storeAPIWorker = apiworker
    }
    
    func fetchStore() {
        storeAPIWorker.fetchCandyStore{ [unowned self] (candyStore) in
            self.candyStore = candyStore
            self.presenter?.mergeInteractionCandy(self, FetchCandy: candyStore)
        }
    }
}

 

MainBuilder 
Builder는 Router의 무게를 줄이기 위한 클래스로 UIKit을 사용할 수 있다.
Router의 한 종류라고 생각하면 된다.

import UIKit

class MainBuilder {
    func buildModule(arroundView view: MainViewProtocol) {
        let presenter = MainPresenter()
        let interactor = MainInteractor(withAPIWorker: StoreAPIWorker())
        let router = MainRouter()
        
        view.presenter = presenter
        presenter.view = view
        presenter.router = router as RouterProtocol
        presenter.interactor = interactor
        interactor.presenter = presenter
    }
}

 

MainRouter 
MainRouter에서는 전의 CandyBuilder을 통해 CandyView를 제작하고 넘겨주는 코드를 포함하고 있다.
이러한 흐름을 가능하게 한 것은 Viewable 이다.

import UIKit

protocol RouterProtocol {
    func setCandyview() -> CandyView
    func push(from:Viewable)
    func present(from:Viewable)
}

class MainRouter : RouterProtocol {
    func setCandyview()-> CandyView {
        let storeView = CandyView(nibName: "CandyView",bundle: nil)
        CandyBuilder.buildModule(arroundView: storeView)
        return storeView
    }
    
    func push(from:Viewable) {
        let view = self.setCandyview()
        from.push(view, animated: true)
    }
    
    func present(from:Viewable) {
        let nav = UINavigationController(rootViewController: setCandyview())
        from.present(nav, animated: true)
    }
}

 

Viewable.swift 

import Foundation
import UIKit

protocol Viewable: AnyObject {
    func push(_ vc: UIViewController, animated: Bool)
    func present(_ vc: UIViewController, animated: Bool)
    func pop(animated: Bool)
    func dismiss(animated: Bool)
    func dismiss(animated: Bool, _completion:  @escaping (() -> Void))
}

extension Viewable where Self: UIViewController {

    func push(_ vc: UIViewController, animated: Bool) {
        self.navigationController?.pushViewController(vc, animated: animated)
    }

    func present(_ vc: UIViewController, animated: Bool) {
        self.present(vc, animated: animated, completion: nil)
    }

    func pop(animated: Bool) {
        self.navigationController?.popViewController(animated: animated)
    }

    func dismiss(animated: Bool) {
        self.dismiss(animated: animated, completion: nil)
    }

    func dismiss(animated: Bool, _completion: @escaping (() -> Void)) {
        self.dismiss(animated: animated, completion: _completion)
    }

    var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
    }
    var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
        return .portrait
    }

}

 

다시 MainView로 넘어와, 비어있던 화면 전환 코드에 이 코드를 넣어주면 정상적으로 작동하는 것을 볼 수 있다.

MainView

extension MainView : MainViewProtocol {
	...
    @objc func touchCandyView(sender: UITapGestureRecognizer){
        MainRouter().push(from: self)
    }
}

extension MainView : Viewable {}

 

시작 위치가 바뀌었으니, AppDelegate 파일에도 수정을 해준다.

AppDelegate

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window : UIWindow?
    var navController : UINavigationController?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
    
        window = UIWindow(frame: UIScreen.main.bounds)
        
        let mainView = MainView(nibName: "MainView", bundle: nil)
        MainBuilder().buildModule(arroundView: mainView)
        navController = UINavigationController(rootViewController: mainView)
        
        window?.rootViewController = navController
        window?.makeKeyAndVisible()
       
        return true
    }
}

 

 

Viewable 출처

 

yokurin/Swift-VIPER-iOS

SwiftVIPER is an sample iOS App written in Swift using the VIPER architecture. Also SwiftVIPER is not a strict VIPER architecture. - yokurin/Swift-VIPER-iOS

github.com

반응형

'개발 > 디자인 패턴' 카테고리의 다른 글

[iOS] 디자인패턴 - MVP  (0) 2020.03.20
[iOS] 디자인패턴 - VIPER  (4) 2020.02.24