ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [MVVM 연습 프로젝트][Lotto](2) - 기능 추가하기
    iOS 2021. 12. 30. 16:00

    이전 게시글에서 완성한 Lotto프로젝트에 새로운 기능을 추가해보려고 합니다.

    UIPickerView를 추가한 모습

    전까지는 임의로 회차를 지정해서 정보를 받아왔었는데,

    PickerView를 추가해서 여러 회차에 대해서 로또 당첨 정보를 얻어와보려고 합니다.

    그리고 1등 당첨금액이 너무 길어서 읽기 불편했기 때문에, 숫자에 포맷을 지정해 보기 편하게 해 주겠습니다.

    코드는 아래 명령을 통해 다운받으실 수 있습니다.

    git clone -b lotto_2 https://github.com/sseungmn/MVVM_Practice.git

    PickerView 추가하기

    새로운 PickerView를 추가해보도록 하겠습니다.

    MVVM에 맞춰서 UI는 View에, 기능은 ViewModel에 구현해보면 좋을 것 같습니다.

    View에 UI추가하기

    LottoView

    class LottoView: UIView {
    
      let roundPickerView = UIPickerView()
      ...
      func setConstraints() {
        addSubview(roundPickerView)
            roundPickerView.snp.makeConstraints { make in
              make.leading.trailing.equalToSuperview()
              make.top.equalToSuperview().inset(100)
              make.height.equalTo(200)
            }
        ...
        }
      }
    }
    

    ViewController에 PickerView연결하기

    PickerView의 데이터와 여러가지 기능을 설정해주기 위해서는 ViewController에 PickerView의 역할을 위임해주어야 합니다.

    class LottoViewController: UIViewController {
      ...
      func setConfiguration() {
        bindView()
    
        // PickerView의 delegate, dataSource를 LottoViewController로 지정해줍니다.
        mainView.roundPickerView.delegate = self
        mainView.roundPickerView.dataSource = self
        // 시작값을 995(12/25일자)로 맞추기 위해서 해당 값을 가지는 row로 선택해줍니다.
        mainView.roundPickerView.selectRow(995 - 1, inComponent: 0, animated: true)
        // 시작값에 대해 API를 호출해줍니다.
        viewModel.fetchLottoInfo(mainView.roundPickerView.selectedRow(inComponent: 0) + 1)
      }
      ...
    }
    
    extension LottoViewController: UIPickerViewDelegate, UIPickerViewDataSource {
      // 몇개의 Component를 갖을건지 설정
      func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return 1
      }
    
      // 각 Component당 몇개의 row를 갖을건지 설정
      func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return 995
      }
    
      // 각 Component -> Row에 어떤 값을 보여줄 것인지 설정
      func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return "\(row + 1)"
      }
    
      // 해당 row를 눌렀을 때의 동작을 설정
      func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        viewModel.fetchLottoInfo(row + 1)
      }
    }

    이렇게 하면 일단, MVC로 기능을 구현할 수 있습니다.

    MVVM으로 전환하기 위해서 ViewModel을 수정해줍니다.

    ViewModel에게 일 시키기

    LottoViewModel

    class LottoViewModel {
      // Lotto API에서 최신 회차에 대한 정보를 제공하지 않기 때문에 직접 설정합니다..
      var maxRound = 995
      ...
    }
    
    // PickerView에 대한 method들을 extension으로 따로 분리해주었습니다.
    extension LottoViewModel {
      var pickerViewInitValue: Int {
        // 시작할때 값에 대해서 설정해줍니다.
           // row는 0부터 시작하기 때문에, 회차와 row사에서는 그 차이를 반영해주어야 합니다.
        return maxRound - 1
      }
      // 각 delegate method에 대해서 매칭할 수 있게 비슷한 이름으로 작성해줍니다.
      func numberOfComponents() -> Int {
        return 1
      }
    
      func numberOfRowsInComponent() -> Int {
        return maxRound
      }
    
      func titleForRow(_ row: Int) -> String? {
        return "\(row + 1)"
      }
    
      func didSelectRow(_ row: Int) {
        fetchLottoInfo(row + 1)
      }
    }

    위의 ViewController에서도 그랬듯이, 이렇게 각각의 Delegate마다 extension으로 나눠서 코드를 구성하는것이 가독성과 유지보수 측면에서 효과적이라고 생각합니다.

    코드가 길어질 경우, 한번에 해당 기능에 대해 찾아서 유지/보수 할 수 있기 때문입니다.

    그럴 때 또 도움이 되는것이 MARK comment입니다.

    // MARK: - Extensions
    // MARK: PickerView

    위와 같은 형식으로 comment를 작성하게 되면 에디터 화면이나 미니맵에서 쉽게 해당 부분에 접근할 수 있습니다.

    왼쪽은 에디터화면, 오른쪽은 미니맵

     

    아무튼, ViewModel을 수정했다면 이제 ViewController에서 ViewModel의 method들을 활용하도록 작성해주면 됩니다.

    다시 ViewController

    LottoViewController

    func setConfiguration() {
        ...
          // 이 부분만 viewModel에서 가져오는 코드로 변경
        mainView.roundPickerView.selectRow(viewModel.pickerViewInitValue, inComponent: 0, animated: true)
        ...
      }
    
    extension LottoViewController: UIPickerViewDelegate, UIPickerViewDataSource {
      func numberOfComponents(in pickerView: UIPickerView) -> Int {
        return viewModel.numberOfComponents()
      }
    
      func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
        return viewModel.numberOfRowsInComponent()
      }
    
      func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
        return viewModel.titleForRow(row)
      }
    
      func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
        viewModel.didSelectRow(row)
      }
    }

    위와 같이 모든 정보를 ViewModel에서 가져오게 만들어, View는 말 그대로 UI를 보여주는 역할만을 담당하도록 해줍니다.

    고민...

    그런데 여기서 학습하는 입장에서 고민인 부분이 있습니다.

    func setConfiguration() {
        ...
        mainView.roundPickerView.selectRow(viewModel.pickerViewInitValue, inComponent: 0, animated: true)
        viewModel.fetchLottoInfo(mainView.roundPickerView.selectedRow(inComponent: 0) + 1)
      }

    바로 이곳인데요, 초기에 pickerView의 값을 지정해주고, 해당 값으로 API 호출을 진행해주는 부분입니다.

    이 부분은 제 생각에는 viewModel이 처리해 주어야 한다고 생각합니다.

    그렇게 하기 위해서는, viewModel에 pickerView 객체를 파라미터로 넘겨주어서 값을 초기값으로 수정하고, 해당 값을 불러와 API를 호출해야합니다.

    viewModel은 어떤 View에든 종속적이지 않아야 한다고 하는데, pickeView 객체를 넘겨받는것이 종속적인건지 아닌지 조금 의아합니다...

    좀 더 공부하면 이 부분에 대해서도 깨닳을 수 있을것이라 생각합니다..!

    숫자 format 적용하기

    이건 간단합니다.

    방법은 두 가지가 있는데요.

    1. Int 구조체에 extension으로 구현하는 방법

      extension Int {
          func format() -> String {
              let formatter = NumberFormatter()
              formatter.numberStyle = .decimal
              return formatter.string(for: self)!
          }
      }
    2. viewModel에서 method로 구현하는 방법
      (model에 구현해야한다고 생각하는데, 수업시간에 viewModel에 넣으셨기 때문에,, 어느것이 맞을지는 조금 더 고민해봐야겠습니다.)
      func format(_ won: Int) -> String {
          let formatter = NumberFormatter()
          formatter.numberStyle = .decimal
          return formatter.string(for: won)!
      }

    작은 프로젝트를 만드는 과정을 설명 형식으로 기록해보려고 했지만,

    중간중간 고민이 되는 부분들도 있고 아직 배우고 학습하는 과정에 있기 때문에

    완벽한 정보보다는 지금 시점에 하게되는 고민들에 대해서도 기록해보려고 합니다.

    중간중간 제가 하는 고민들에 대해서 같이 생각해보고 의견을 나누기도 했으면 좋겠습니다.

    댓글

Designed by Tistory.