-
[MVVM 연습 프로젝트][Lotto](2) - 기능 추가하기iOS 2021. 12. 30. 16:00
이전 게시글에서 완성한 Lotto프로젝트에 새로운 기능을 추가해보려고 합니다.
전까지는 임의로 회차를 지정해서 정보를 받아왔었는데,
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 적용하기
이건 간단합니다.
방법은 두 가지가 있는데요.
- Int 구조체에 extension으로 구현하는 방법
extension Int { func format() -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal return formatter.string(for: self)! } }
- viewModel에서 method로 구현하는 방법
(model에 구현해야한다고 생각하는데, 수업시간에 viewModel에 넣으셨기 때문에,, 어느것이 맞을지는 조금 더 고민해봐야겠습니다.)
func format(_ won: Int) -> String { let formatter = NumberFormatter() formatter.numberStyle = .decimal return formatter.string(for: won)! }
작은 프로젝트를 만드는 과정을 설명 형식으로 기록해보려고 했지만,
중간중간 고민이 되는 부분들도 있고 아직 배우고 학습하는 과정에 있기 때문에
완벽한 정보보다는 지금 시점에 하게되는 고민들에 대해서도 기록해보려고 합니다.
중간중간 제가 하는 고민들에 대해서 같이 생각해보고 의견을 나누기도 했으면 좋겠습니다.
'iOS' 카테고리의 다른 글
[SwiftGen] Assets을 쉽고 안정적으로 사용하자! (0) 2022.01.20 [MVVM 연습 프로젝트][Lotto](1) - MVVM으로 리펙토링 (0) 2021.12.30 [MVVM 연습 프로젝트][Lotto](0) - MVC로 만들어보기 (0) 2021.12.28 [Architecture] MVVM에 대해서 알아보기 (0) 2021.12.28 [맥주] 맥주 추천 어플 만들기(0) - 준비 (0) 2021.12.23 - Int 구조체에 extension으로 구현하는 방법