개발/Swift

Swift MVVM 패턴 Network -> Decode -> View

덤벨로퍼 2021. 1. 12. 14:46

 

https://inf.run/V3b51

 

iOS Clean Architecture & MVVM: RxSwift 완전 정복 강의 | 덤벨로퍼 - 인프런

덤벨로퍼 | Clean Architecture와 MVVM 패턴을 실무에서 적용할 수 있도록 설명하며, RxSwift, Concurrency 등 필수 기술을 다룹니다., iOS Clean Architecture & MVVM: RxSwift 완전 정복현업에서 Clean

www.inflearn.com

 

네트워킹과 받아온 데이터를 모델로 디코딩 후에 테이블 뷰에 보여주는 간단한 예제이다.

먼저 모델을 만든다.

각 뉴스의 내용을 담을 Article, Article의 리스트 형태인 ArticleList 이다

struct ArticleList : Codable {
    let articles :[Article]
}

struct Article : Codable {
    let title :String
    let description : String?
}


 1. Codable은 추후에 디코딩을 위해 필요하다. 서버로 보낼 데이터가 아니라면 Decodable로 사용해도된다.

2. description 은 옵셔널 String 타입으로 되어있다. 이는 나중에 에러가 발생해서 수정했던 내용이다. 먼저 얘기 하자면 desctiption에 null값이 들어갈수 있기 때문이다.

 

 

네트워크

 

func getArticles(url:URL,completion: @escaping ([Article]?)->()){
        URLSession.shared.dataTask(with: url){ data , response, error in
            if let error = error {
                print(error.localizedDescription)
                completion(nil)
            }else if let fetchedData = data {

                let articleList = try? JSONDecoder().decode(ArticleList.self,from:fetchedData)
                
                if let articleList = articleList {
                    completion(articleList.articles)
                }

                print(articleList?.articles)

                 
            }
        }.resume()
    }

 

getArticles 라는 API 호출 메소드를 만들었다. 이 메소드는 view에서 실행할 것이다.

view 에서 getArticle() 함수를 사용하면서 콜백함수를 사용할것이다. 이럴때 @escaping을 사용해준다.

completion: @escaping ([Article]?)->()

 

에러처리를 우선 해준다음

data 가 들어오면 디코딩 작업을해준다. 

 let articleList = try? JSONDecoder().decode(ArticleList.self,from:fetchedData)

try? 의경우 에러가 발생해도 에러가 나지않고 articleList는 nil이 들어간다.

try! 를 사용하면 에러발생시 에러가 나고 코드가 진행되지않는다.

여기서  이런 에러가 발생했다. 

 

에러 발생

Swift.DecodingError.valueNotFound(Swift.String, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "articles", intValue: nil), _JSONKey(stringValue: "Index 11", intValue: 11), CodingKeys(stringValue: "description", intValue: nil)], debugDescription: "Expected String value but found null instead.", underlyingError: nil)

쉽게는 "description" 에 null 값이 들어가서 에러가 난 경우이다.

그래서 아까처럼 모델을 옵셔널 타입으로 변경했다.

 

ArticleList 모델로 디코딩이 완료되면 모델을 넘겨주는 completion이라는 콜백 함수를 실행한다. 

실행하는 view는 잠시후에 보고 viewModel 먼저 보자

 

viewModel

struct ArticleViewModel {
    private let article : Article
    
}


extension ArticleViewModel{
    init(_ article:Article) {
        self.article = article
    }
}

extension ArticleViewModel{
    var title :String {
        return self.article.title
    }
   var description :String? {
        return self.article.description
    }
    
}

ㄹ먼저 Article 모델의 뷰모델이다.

간단하게 init함수 와 필드 get 변수 들이있다.

extension 하지않고 struct 안에 해도 상관없다.

 

struct ArticleListViewModel {
     let articles :[Article]
}

extension ArticleListViewModel{
    var numberOfSection:Int{
        return 1
    }
    
    func numberOfRowInSetion() -> Int{
        return self.articles.count
    }
    func articleAtIndex(index:Int) -> ArticleViewModel{
        let viewModel = ArticleViewModel.init(self.articles[index])
        return viewModel;
    }
}

ㅁArticle 리스트 형태인 필드와

추후에 TableView에서 사용할 메소드들을 넣었다.

 

viewController

  let url = URL(string: "fetch 할 url 주소")!
  
   Webservice().getArticles(url: url) { articles in
            
            if let articles = articles{
                self.articlesViewModel = ArticleListViewModel(articles:articles)
            }
            
            //reload table
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }

getArticle함수를 사용하면 콜백으로 articles가 리턴될것이다.

articles 는 여기서 뷰 모델로 바꾼후 글로벌로 저장한다.

DispatchQueue.main.async 를 사용해서 이후에 view를 리로드 해준다.

 

 override func numberOfSections(in tableView: UITableView) -> Int {
        return self.articlesViewModel == nil ?0:  self.articlesViewModel.numberOfRowInSetion()
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.articlesViewModel == nil ? 0 : self.articlesViewModel.numberOfRowInSetion()
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "ArticleTableViewCell", for: indexPath) as? ArticleTableViewCell else {
            fatalError("not found")
        }
        
        let articleVm = articlesViewModel.articleAtIndex(index: indexPath.row)
        cell.titleLabel.text = articleVm.title
        cell.descriptionLabel.text = articleVm.description
        return cell
    }

테이블 뷰를 위한 오버라이딩 메소드이다.

아까 뷰모델에서 article 개수와 섹션갯수 , article 객체 가져오는 메소드를 만들어놔서 사용했다.

마지막 에사용하는 ArticleTableViewCell 은 따로 만들어놨고 as ArticleTableViewCell 를 사용해 그안에있던

label text 에 접근할수있다.