• Jan
  • Feb
  • Mar
  • Apr
  • May
  • Jun
  • Jul
  • Aug
  • Sep
  • Oct
  • Nov
  • Dec
  • Sun
  • Mon
  • Tue
  • Wed
  • Thu
  • Fri
  • Sat
  • 27
  • 28
  • 29
  • 30
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

UICollectionView MVVM 예제로 살펴보기

image

요 뷰를 앱잼 기간 때 MVVM으로 마이그레이션 해 놨었는데,
따로 정리를 해 놓지 않아서.. 이번에 정리하면서 다시 정확하게 이해하려고 한다! (MVVM 처음 써봐서 코드가 완전 100% 정답인지는 모르겠음)

달랑 CollectionView 하나 있는 간단한 뷰 컨트롤러인데, 클릭 이벤트 등은 제외하고, 딱! 데이터를 서버에서 받아와서 fetch하고, 표시하는 것만 다뤄보자,,

ViewModel.swift

class CourseListViewModel {
    
     var courses = [Course]()
    
    func getCourseLibrary(completion: @escaping ((ViewModelState) -> Void)) {
        CourseAPI.shared.getCourseLibrary { (response) in
            switch response {
            case .success(let courses):
                if let data = courses as? CoursesData {
                    self.courses = data.courses
                    completion(.success)
                }
            case .requestErr(let message):
                print("requestErr", message)
                completion(.failure)
            case .pathErr:
                print(".pathErr")
                completion(.failure)
            case .serverErr:
                print("serverErr")
                completion(.failure)
            case .networkFail:
                print("networkFail")
                completion(.failure)
            }
        }
    }
}

요로코롬… CourseListViewModel 클래스에는 받아온 데이터를 저장할 변수와, 서버에서 통신한 결과를 까서 데이터를 원하는 변수에 저장해주는 함수가 있다.

struct CourseViewModel {
    let course: Course
    
    // 의존성 주입 (Dependency Injection)
    init(_ course: Course) {
        self.course = course
    }
}

그리고 CourseViewModel 구조체에는 1개의 Course Model 데이터를 받아 저장하는 상수를 만들어 주고,
의존성 주입(DI)을 위한 init 함수를 작성해준다.

extension CourseListViewModel {
    var numberOfSections: Int {
        return 1
    }
    
    func numberOfRowsInSection(_ section: Int) -> Int {
        return self.courses.count
    }
    
    func courseAtIndex(_ index: Int) -> CourseViewModel {
        let course = self.courses[index]
        return CourseViewModel(course)
    }
}

그리고 CourseListViewModel의 extension을 작성하는데,
여기서는 View에서 사용하지만 Data에 관련이 있는 내용들 (?)을 작성해준다. CollectionView에서는 numberOfSections, numberOfRowsInSection, 특정 idx의 데이터값 등이 있겠다 이 때, 특정 idx의 데이터값은 CourseViewModel 형을 반환한다.

즉, CourseListViewModel은 말 그대로 Course의 List 관련, CourseViewModel은 단일 Course 관련이다!

이런식으로 작성해주고, 뷰 컨트롤러에서 직접 사용해보자

ViewController.swift

    private func fetchCourses() {
        courseListViewModel.getCourseLibrary { state in
            switch state {
            case .success:
                return self.updateUI()
            case .failure:
                return
            }
        }
    }
    
    private func updateUI() {
        self.courseLibraryCollectionView.reloadData()
    }

서버에서 데이터를 fetch하는 fetchCourses() 함수와,
그 과정이 성공했을 때 UI를 업데이트 하는 함수를 만들어준다.

지금은 그냥 reload 한 줄이지만, 나중을 대비해 함수로 빼 놨다.

UICollectionViewDataSource

// MARK: - UICollectionViewDataSource

extension CourseLibraryViewController: UICollectionViewDataSource {
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        return courseListViewModel.numberOfSections
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return courseListViewModel.numberOfRowsInSection(0)
    }

numberOfSections, numberOfItemsInSection은 우리가 viewmodel의 extension에서 작성한 내용들을 그대로 써 주면 된다.

개인적으로는 이 부분에서 View와 Data(?)가 분리되어 있는 느낌을 받았다. Data에 관련된 건 ViewModel이 전담하는 느낌이라… 마음이 편안해지는 느낌…( ◠‿◠ ) ..

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        
        switch courseListViewModel.courseAtIndex(indexPath.row).course.situation {
        case 0:
            if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Const.Xib.Identifier.undoneCourseCollectionViewCell, for: indexPath) as? UndoneCourseCollectionViewCell {
                
                let viewModel = courseListViewModel.courseAtIndex(indexPath.row)
                cell.courseViewModel = viewModel
                cell.setButtonTitle(doingCourse: doingCourse)
                
                return cell
            }
            
        case 2:
            if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Const.Xib.Identifier.doneCourseCollectionViewCell, for: indexPath) as? DoneCourseCollectionViewCell {
                
                let viewModel = courseListViewModel.courseAtIndex(indexPath.row)
                cell.courseViewModel = viewModel
                cell.setButtonTitle(doingCourse: doingCourse)
                
                return cell
            }
        default:
            return UICollectionViewCell()
        }
    
        return UICollectionViewCell()
    }

cellForItemAt 함수를 보면, 지금 현재 내가 개발하는 뷰에서는 course의 situation에 따라 다른 CollectionViewCell를 반환해야 한다.

switch courseListViewModel.courseAtIndex(indexPath.row).course.situation {

CourseListViewModel의 courseAtIndex를 써서 courseData를 받아온 후, course의 situation에 따라 분기 해 준다.

if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Const.Xib.Identifier.undoneCourseCollectionViewCell, for: indexPath) as? UndoneCourseCollectionViewCell {
                
let viewModel = courseListViewModel.courseAtIndex(indexPath.row)
cell.courseViewModel = viewModel
                
return cell
}

cell을 정의한 후 cell에 데이터를 넘겨 줄 때에도, ViewModel로 해결한다.
courseListViewModel.courseAtIndex(indexPath.row), (CourseViewModel 형)을 viewModel이라는 상수에 할당해주고,

cell의 courseViewModel이라는 변수에 그 값을 넘겨준다.

cell.swift

cell의 courseViewModel에 우리가 해당 idx에 해당하는 course view model을 넘겨줬는데,
cell 파일을 보면 아래와 같이 작성되어있다.

class UndoneCourseCollectionViewCell: UICollectionViewCell {
    
    // MARK: - Properties
    
    var courseViewModel: CourseViewModel! {
        didSet {
            titleLabel.text = courseViewModel.course.title
            courseDaysLabel.text = "\(courseViewModel.course.totalDays)일"
            descriptionTextView.text = courseViewModel.course.courseDescription
            setProperty(by: courseViewModel.course.property)
        }
    }

courseViewModel 변수는 우리가 넘겨준 CourseViewModel 형이고,
값이 들어오면 그 값이 didSet을 통해 뷰의 여러 오브젝트들에 반영되거나, 매개변수로 전달된다.