074_MVVM+DiffableDataSource(숫자입력, 검색이미지)

2022. 10. 21. 11:23SeSAC/수업정리

-. iOS 13에서 나온 CollectionView API(DiffableDataSource, Compositional Layout)를 기반으로 iOS14에서 Section Snapshot, List Configuration이 새로 나왔다. 이를 사용하여 테이블뷰처럼 생긴 컬렉션뷰를 만들 수 있다.

-. MVVM이란 M-V-VM 세 개 레이어로 나누어 코드를 작성하는 방식으로 뷰모델을 통해 UI와 비지니스 로직을 분리하는게 핵심이다. *M(Model): 데이터 담당, VM(View Model): 비지니스로직 담당, V(View): UI담당

 

#. MVVM흐름: 데이터 초기화를 뷰모델에서 처리하고, 그 데이터를 뷰컨트롤러가 받아서 작업을 실행한다.

 1. 레이아웃 설정: UICollectionViewLayout타입 레이아웃을 반환하는 메서드 생성

 2. 데이터소스 설정(재사용셀 + 셀컨텐츠): diffable 데이터소스 타입으로 셀재사용(CellForItemAt) + 셀컨텐츠 설정

 3. 데이터소스 설정(재사용셀에 사용할 데이터) + 뷰모델에서 데이터 초기화 및 처리: 뷰모델초기화한 데이터를 받아 snapshot에 넣기

 *뷰모델에서 초기화하는 동시에 Observable에서 데이터 변화 체크(value 초기화)->클로저로 데이터 전달하는 메서드 실행(bind, didSet에 따른 listener실행)->뷰컨트롤러에서 전달받은 클로저 데이터로 작업 실행


3. 데이터소스 설정(재사용셀에 사용할 데이터) + 뷰모델에서 데이터 초기화 및 처리: 뷰모델초기화한 데이터를 받아 snapshot에 넣기

func bindData() {
        //numberTextField.text = "3000"처럼 뷰컨트롤러에서 직접 보여주는게 아니라 뷰모델에서 내용을 전달받아 보여줌
        //viewModel.pageNumber.bind실행구조: pageNumber초기화 동시에 CObservable value초기화-> bind실행시 클로저로 value전달, listener에 클로저 저장->value변경 시 변경된 value를 받은 listener실행
         viewModel.pageNumber.bind { value in
             self.numberTextField.text = value
         }
        
        //뉴스 추가 제거를 뷰모델 처리: datasource초기화하기 전에 사용하면 found nil 발생하므로 configureDataSource()에서 datasource초기화 먼저 해줘야함
        viewModel.sample.bind { item in
            var snapshot = NSDiffableDataSourceSnapshot<Int, News.NewsItem>()
            snapshot.appendSections([0])
            snapshot.appendItems(item) //News구조체가 가진 items 반환값은 itemsInternal()이고, itemsInternal()는 NewsItem타입의 배열이다. 즉, 배열을 스냅샷에 넣어주는 것과 같음.
            self.dataSource.apply(snapshot, animatingDifferences: false)
        }
    }

 


#. MVVM+Diffable흐름: 네트워크 통신으로 받은 데이터 초기화를 뷰모델에서 처리하고, 그 데이터를 뷰컨트롤러가 받아서 작업을 실행한다.

 1. 레이아웃 설정: UICollectionViewLayout타입 레이아웃을 반환하는 메서드 생성

 2. 데이터소스 설정(재사용셀 + 셀컨텐츠): diffable 데이터소스 타입으로 셀재사용(CellForItemAt) + 셀컨텐츠 설정

 3. 데이터 처리: 네트워크 통신으로 받은 String타입 이미지url을 이미지화(백그라운드 스레드(String->Url->Data), 메인 스레드(Data->Image)로 나누어서 작업)

 4. 데이터소스 설정(재사용셀에 사용할 데이터) + 뷰모델에서 데이터 초기화 및 처리: 서치바텍스트 내용을 네트워크 통신요청->클로저로 받은 네트워트통신 내용을 photoList에 초기화->CObservable에서 값변경 인식하면서 snapshot에 데이터 추가하는 bind실행

 *사용할 데이터를 정리한 Codable파일, 타입에 맞는 데이터를 받을 수 있는 네트워크 통신 메서드 미리 준비 필요!

 *뷰모델에서 초기화하는 동시에 Observable에서 데이터 변화 체크(value 초기화)->클로저로 데이터 전달하는 메서드 실행(bind, didSet에 따른 listener실행)->뷰컨트롤러에서 전달받은 클로저 데이터로 작업 실행

 


3. 데이터 처리: 네트워크 통신으로 받은 String타입 이미지url을 이미지화(백그라운드 스레드(String->Url->Data), 메인 스레드(Data->Image)로 나누어서 작업)

 //MARK: - 3. 데이터 처리: 네트워크 통신으로 받은 String타입 이미지url을 이미지화(백그라운드 스레드(String->Url->Data), 메인 스레드(Data->Image)로 나누어서 작업)
            //String->Url->Data->Image: String으로 받은 이미지url을 이미지화 하는 과정
            DispatchQueue.global().async { //네트워크통신은 백그라운드스레드에서 작업: 네트워크통신처리하는 동안 다른 작업 가능
                let url = URL(string: itemIdentifier.urls.thumb)! //String->Url
                let data = try? Data(contentsOf: url) //Url->Data
                
                DispatchQueue.main.async { //UI업데이트는 메인스레드에서 작업
                    content.image = UIImage(data: data!) //Data->Image: 킹피셔는 UIImageView타입만 처리하기 때문에 킹피셔 사용 하지 않고 이미지화
                    cell.contentConfiguration = content //네크워크 비동기통신 하지 않도록 메인async에 선언해줘야 순서대로 실행됨
                }
            }

 

 4. 데이터소스 설정(재사용셀에 사용할 데이터) + 뷰모델에서 데이터 초기화 및 처리: 서치바텍스트 내용을 네트워크 통신요청->클로저로 받은 네트워트통신 내용을 photoList에 초기화->CObservable에서 값변경 인식하면서 snapshot에 데이터 추가하는 bind실행

//MARK: - 4. 네트워크 통신요청: 서치바텍스트 내용을 네트워크 통신요청->클로저로 받은 네트워트통신 내용을 photoList에 초기화->CObservable에서 값변경 인식하면서 snapshot에 데이터 추가하는 bind실행
        viewModel.requestSearchPhoto(query: searchBar.text!)
        
		//뷰모델
            func requestSearchPhoto(query: String) {
        APIService.searchPhoto(query: query) { photo, statucCode, error in
            guard let photo = photo else { return }
            self.photoList.value = photo
        }
        
        //API요청
        static func searchPhoto(query: String, completion: @escaping (SearchPhoto?, Int?, Error?) -> Void) { //통신요청내용(SearchPhoto), 상태코드(Int), 에러케이스 클로저 전달
        let url = "\(APIKey.searchURL)\(query)"
        let header: HTTPHeaders = ["Authorization": APIKey.authorization]
        AF.request(url, method: .get, headers: header).responseDecodable(of: SearchPhoto.self) { response in //responseDecodable에 decode 대상 추가
            let statusCode = response.response?.statusCode
            switch response.result {
            case .success(let value): completion(value, statusCode ,nil) //성공했을때 클로저로 통신요청내용, 상태코드 전달
            case .failure(let error): completion(nil, statusCode ,error) //실패했을때 클로저로 상태코드, 에러케이스 전달
            }
        }
    }