ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [MVVM 연습 프로젝트][Lotto](0) - MVC로 만들어보기
    iOS 2021. 12. 28. 23:33

    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를 다른 위치로 옮겼을 때에는

    TARGETS -&gt; Build Setting -&gt; Packaging

    위 요소를 현재 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()
      }
    }

    각각의 위치가 정해진 View를 볼 수 있다.

    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 {
              // 기존 코드
          }
        }
      }

    결과는 아래와 같이 나옵니다

    잘 나온다!

    댓글

Designed by Tistory.