[MVVM 연습 프로젝트][Lotto](0) - MVC로 만들어보기
Device : 2021 M1 Macbook pro
OS : macOS Monterey 12.0.1
Xcode : Version 13.1 (13A1030d)
안녕하세요! miniOS입니다.
이번에는 MVVM을 공부하면서 간단하게 적용해볼 수 있는 연습용 프로젝트를 만들어 보려고 합니다.
회차별로 로또 추첨 결과에 대해서 얻어오는 서비스를 만들어 보겠습니다.
코드는 아래 명령을 통해서 다운받을 수 있습니다.
git clone -b lotto_0 https://github.com/sseungmn/MVVM_Practice.git
준비
로또 API
https://www.dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=<#회차번호#>
Project
Info.plist를 다른 위치로 옮겼을 때에는
위 요소를 현재 Info.plist의 위치로 수정해주어야 합니다.
withoutStoryboard
처음이신 분들은 Storyboard없이 code로 UI 구현하기를 참고해주세요!
API Handing
위에 기입해놓은 로또 API를 호출해보면 결과는 아래와 같습니다.
이 JSON구조를 구조화 하기 위해서 QuickType을 이용합니다.
왼쪽 필드에 JSON결과를 붙여넣어주면 오른쪽에 구조체 코드가 나오는데, 이것이 우리의 Model이 됩니다.
위 코드를 Model파일에 넣어주면 됩니다.
이제, 코드를 통해서 API 통신을 하고, 위와 같은 결과를 받아와봅시다.
// 각각의 error에 대해서 처리하기 위해서, 간단한 세부 오류를 정의했습니다.
enum APIError: Error {
case invalidResponse, invalidData, noData, failed
}
class APIService {
// 요청에 성공했을 때와 오류가 발생했을 때 각각의 값들을 전달해주기 위해서
// completion closure을 아래와 같이 선언했습니다.
static func requestLotto(_ round: Int, completion: @escaping (Lotto?, APIError?) -> Void) {
let url = URL(string: "https://www.dhlottery.co.kr/common.do?method=getLottoNumber&drwNo=\(round)")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard error == nil else {
completion(nil, .failed)
return
}
guard let response = response as? HTTPURLResponse else {
completion(nil, .invalidResponse)
return
}
// 이곳의 response는 위에서 캐스팅해준 HTTPURLResponse타입입니다. 순서를 주의!
guard response.statusCode == 200 else {
completion(nil, .failed)
return
}
guard let data = data else {
completion(nil, .noData)
return
}
do {
// 이곳에서 JSON으로 받은 data를 우리의 Model에 맞춰서 decoding해줍니다.
let decodedData = try JSONDecoder().decode(Lotto.self, from: data)
completion(decodedData, nil)
} catch {
completion(nil, .invalidData)
}
// dataTask는 기본적으로 suspend상태이기 때문에 명시적으로 resume()을 해주어야 합니다.
}.resume()
}
}
요청이 잘 되는지 테스트 해보기 위해서 LottoViewController에 넣고 실행해보겠습니다.
override func viewDidLoad() {
super.viewDidLoad()
APIService.requestLotto(854) { lotto, error in
print(lotto)
}
}
UI
당첨번호, 당첨금, 날짜. 세 가지 정보를 보여주기 위해서 간단하게 UI를 구성해보겠습니다.
LottoView
import UIKit
import SnapKit
class LottoView: UIView {
let num1Label = UILabel()
let num2Label = UILabel()
let num3Label = UILabel()
let num4Label = UILabel()
let num5Label = UILabel()
let num6Label = UILabel()
let num7Label = UILabel()
let dateLabel = UILabel()
let moneyLabel = UILabel()
let stackView: UIStackView = {
let view = UIStackView()
view.axis = .horizontal
view.spacing = 8
view.backgroundColor = nil
view.distribution = .fillEqually
return view
}()
// code로만 View를 구성했을 때 불리는 method이다.
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .white
setConfig()
setConstraints()
}
// xib파일로 View를 구성했을 때 호출되는 method이다.
// coder를 통해서 xib를 nib으로 바꿔준다.
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setConfig() {
[num1Label, num2Label, num3Label, num4Label,
num5Label, num6Label, num7Label, dateLabel, moneyLabel].forEach { label in
label.backgroundColor = .lightGray
label.textAlignment = .center
label.textColor = .black
}
}
func setConstraints() {
addSubview(dateLabel)
dateLabel.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.center.equalToSuperview()
make.height.equalTo(50)
}
// 포함관계를 갖는 View의 Contraints를 구성할 때에는 항상 순서에 주의해야한다.
// 가장 큰 View의 Contraints를 먼저 잡아주고 점점 그 하위 View를 적용해주어야 한다.
addSubview(stackView)
stackView.snp.makeConstraints { make in
make.leading.trailing.equalToSuperview()
make.bottom.equalTo(dateLabel.snp.top).offset(-20)
make.height.equalTo(50)
}
[num1Label, num2Label, num3Label, num4Label,
num5Label, num6Label, num7Label].forEach { label in
stackView.addArrangedSubview(label)
}
addSubview(moneyLabel)
moneyLabel.snp.makeConstraints { make in
make.top.equalTo(dateLabel.snp.bottom).offset(20)
make.leading.trailing.equalToSuperview()
make.height.equalTo(50)
}
}
}
LottoViewController
import UIKit
class LottoViewController: UIViewController {
let mainView = LottoView()
// 앱의 생명주기 상 viewDidLoad보다 먼저 불리는 method이다.
// 이곳에서 view를 바꿔주면 기본적으로 ViewController에 보여주는 view를 바꿀 수 있다.
override func loadView() {
super.loadView()
view = mainView
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
UI에 Data 보여주기
LottoViewController
에 아래 코드를 넣게 되면 API를 통해서 가져온 데이터를 View에 보여주게 됩니다.
override func viewDidLoad() {
super.viewDidLoad()
APIService.requestLotto(854) { lotto, error in
guard error == nil else { return }
guard let lotto = lotto else { return }
self.mainView.num1Label.text = "\(lotto.drwtNo1)"
self.mainView.num2Label.text = "\(lotto.drwtNo2)"
self.mainView.num3Label.text = "\(lotto.drwtNo3)"
self.mainView.num4Label.text = "\(lotto.drwtNo4)"
self.mainView.num5Label.text = "\(lotto.drwtNo5)"
self.mainView.num6Label.text = "\(lotto.drwtNo6)"
self.mainView.num7Label.text = "\(lotto.bnusNo)"
self.mainView.moneyLabel.text = "\(lotto.firstWinamnt)원"
self.mainView.dateLabel.text = lotto.drwNoDate
}
}
하지만, 예상과는 달리 오류가 발생하게 됩니다.
UI에 대한 코드를 적용할 때, mainThread에서 하지 않았기 때문에 발생한 오류입니다.
이에 대한 자세한 내용은 UIKit과 DispatchQueue.main의 관계에서 보실 수 있습니다.
LottoAPI
override func viewDidLoad() {
super.viewDidLoad()
APIService.requestLotto(854) { lotto, error in
guard error == nil else { return }
guard let lotto = lotto else { return }
DispatchQueue.main.async {
// 기존 코드
}
}
}
결과는 아래와 같이 나옵니다