Skip to content

MainView

WooSeok Suh edited this page Jul 15, 2021 · 8 revisions

2021-07-05

by Seok

Step.1

  • MainView 하단의 ViewController 이동 Button을 UIButton으로 생성하고 Horizontal StackView에 배치
  • AutoLayout 확인 (iPod touch, 8, 11, 12)

Step.2

Button의 다형성 구현을 위하여 ButtonController 및 ButtonMapper 객체 생성

  • 각 Button Touch시 어떤 Button이 눌리는지 구분 기능 구현 완료
  • 해당 기능 바탕으로 이동할 ViewController와 Mapping 예정
import UIKit

enum ViewControllerType: CaseIterable {
    case giftVC
    case advertiseVC
    case rankVC
    case itemVC
}

final class ButtonMapper {
    private var map: [UIButton: ViewControllerType]
    
    init(from buttons: [UIButton]) {
        self.map = Dictionary(uniqueKeysWithValues: zip(buttons, ViewControllerType.allCases))
    }
    
    subscript(button: UIButton) -> ViewControllerType? {
        return map[button]
    }
}

final class ButtonController: NSObject {
    
    @IBOutlet var moveToVCButtons: [UIButton]!
    
    private var moveToVCMapper: ButtonMapper?
    private var buttonTouchedHandler: (ViewControllerType) -> ()
    
    override init() {
        buttonTouchedHandler = { _ in }
    }
    
    func setupButton() {
        self.moveToVCMapper = ButtonMapper(from: moveToVCButtons)
    }
    
    @IBAction func buttonTouched(sender: UIButton)  {
        guard let vc = moveToVCMapper?[sender] else { return }
        buttonTouchedHandler(vc)
    }
    
    func bind(action: @escaping (ViewControllerType) -> ()) {
        self.buttonTouchedHandler = action
    }
}

이때 SceneCoordinator 생성하여 진행할지 회의 필요 -> SceneCoordinator로 구현하기로 협의하여 객체생성

import Foundation
import RxSwift
import RxCocoa

final class SceneCoordinator: SceneCoordinatorType {
    
    private var window: UIWindow
    private var currentVC: UIViewController
    
    required init(window: UIWindow) {
        self.window = window
        currentVC = window.rootViewController!
    }
    
    @discardableResult
    func transition(to scene: Scene, using style: TransitionStyle, animated: Bool) -> Completable {
        let subject = PublishSubject<Void>()
        let target = scene.instantiate()
        
        switch style {
        case .root:
            currentVC = target
            window.rootViewController = target
            subject.onCompleted()
            
        case .fullScreen:
            target.modalPresentationStyle = .fullScreen
            currentVC.present(target, animated: animated) {
                subject.onCompleted()
            }
            currentVC = target
            
        case .modal:
            currentVC.present(target, animated: animated) {
                subject.onCompleted()
            }
            currentVC = target
        }
        return subject.ignoreElements().asCompletable()
    }
    
    @discardableResult
    func close(animated: Bool) -> Completable {
        let subject = PublishSubject<Void>()
        
        if let presentingVC = self.currentVC.presentingViewController {
            self.currentVC.dismiss(animated: animated) {
                self.currentVC = presentingVC
                subject.onCompleted()
            }
        } else {
            subject.onError(TransitionError.unknown)
        }
        
        return subject.ignoreElements().asCompletable()
    }
}

Step.3

ViewController가 ViewModel로 Button이 가지고있는 ViewControllerType을 넘기면 VC 이동기능 구현

  • CommonViewModel이 가지고 있는 SceneCoordinator 객체 활용
import Foundation

class MainViewModel:CommonViewModel {
    
    func makeMoveAction(to viewController: ViewControllerType) {
        switch viewController {
        case .giftVC:
            let giftViewModel = GiftViewModel(sceneCoordinator: self.sceneCoordinator)
            let giftScene = Scene.gift(giftViewModel)
            self.sceneCoordinator.transition(to: giftScene, using: .modal, animated: true)
            
        case .advertiseVC:
            let advertiseViewModel = AdvertiseViewModel(sceneCoordinator: self.sceneCoordinator)
            let advertiseScene = Scene.ad(advertiseViewModel)
            self.sceneCoordinator.transition(to: advertiseScene, using: .fullScreen, animated: true)
            
        case .rankVC:
            let rankViewModel = RankViewModel(sceneCoordinator: self.sceneCoordinator)
            let rankScene = Scene.rank(rankViewModel)
            self.sceneCoordinator.transition(to: rankScene, using: .modal, animated: true)
            
        case .itemVC:
            let itemViewModel = ItemViewModel(sceneCoordinator: self.sceneCoordinator)
            let itemScene = Scene.item(itemViewModel)
            self.sceneCoordinator.transition(to: itemScene, using: .modal, animated: true)
        }
    }
    
}

추후 로직에 따라 Storage 등의 추가 객체가 필요하면 CommonViewModel의 init에 주입하여 SceneDelegate에서 넣어주면 쉽게 코드 변경 가능

  • 이때도 마찬가지로 Protocol활용하여 다형성 구현가능
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    let coordinator = SceneCoordinator(window: window!)
    let mainViewModel = MainViewModel(sceneCoordinator: coordinator)
    let mainScene = Scene.main(mainViewModel)
    coordinator.transition(to: mainScene, using: .root, animated: false)
}

2021-07-06

by Seok

Step.3

GiftVC & ViewModel 객체 생성 및 CancelButton 기능구현

  • init 시점에서 action을 주입하여 변화에 열려있도록 설계
import Foundation
import Action

class GiftViewModel: CommonViewModel {
    
    let confirmAction: Action<String, Void>
    let cancelAction: CocoaAction
    
    init(sceneCoordinator: SceneCoordinatorType, storage: ItemStorageType, confirmAction: Action<String, Void>? = nil ,cancelAction: CocoaAction? = nil) {
        
        self.confirmAction = Action<String, Void> { input in
            if let action = confirmAction {
                action.execute(input)
            }
            return sceneCoordinator.close(animated: true).asObservable().map{ _ in }
        }
        
        self.cancelAction = CocoaAction {
            if let action = cancelAction {
                action.execute(())
            }
            return sceneCoordinator.close(animated: true).asObservable().map{ _ in }
        }
        
        super.init(sceneCoordinator: sceneCoordinator, storage: storage)
    }
}

2021-07-15

by Seok

Step.4

SceneCoordinator Transition Method Refactoring

import Foundation
import RxSwift

protocol SceneCoordinatorType {
    
    @discardableResult
    func transition(to scene: Scene, using style: TransitionStyle, with type: StoryboardType, animated: Bool) -> Completable
    
    @discardableResult
    func close(animated: Bool) -> Completable
    
    @discardableResult
    func toMain(animated: Bool) -> Completable
}
  • 기존에 없던 StoryboardType을 파라미터로 추가
  • Scene에서 불필요하게 중복으로 Storyboard 생성했던 코드 삭제

Disable SwiftLint Waring

// swiftlint:disable:next cyclomatic_complexity
func instantiate(from storyboard: String) -> UIViewController
  • Swith Case에서 처리해야하는 VC 이동 코드 증가로 인하여 cyclomatic_complexity 경고
  • 일단 Lint 경고를 무시하도록 처리하였지만, 추후 sceneCoordinator 또는 Method 분리를 고려하여 수정해야 함

StoryboardType enum타입 생성

import Foundation

enum StoryboardType {
    case main
    case game
    
    var name: String {
        switch self {
        case .main:
            return "Main"
        case .game:
            return "Game"
        }
    }
}
  • 파라미터 타입을 한정적(안정성 고려)으로 사용하기 위해 static let이 아닌 case로 구현

Step.5

GhostTypewriter 라이브러리 추가

import UIKit
import GhostTypewriter

final class MainViewController: UIViewController, ViewModelBindableType {
    
    var viewModel: MainViewModel!
    @IBOutlet var buttonController: MainButtonController!
    @IBOutlet weak var titleLabel: TypewriterLabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.startTypewritingAnimation()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        titleLabel.restartTypewritingAnimation()
    }
}
  • titleLabel animation 효과 추가
  • 다른 VC로 이동 후, mainVC로 돌아올때 animationRestart 호출(viewWillAppear)
Clone this wiki locally