[UIKit] NetflixClone: CoreData

Junyoung Park·2022년 11월 2일
0

UIKit

목록 보기
76/142
post-thumbnail
post-custom-banner

Building Netflix App in Swift 5 and UIKit - Episode 14 - CoreData and stuff

NetflixClone: CoreData

구현 목표

  • 코어 데이터를 통한 데이터 핸들링

구현 태스크

  • 컬렉션 뷰 탭 제스처 시 컨텍스트 메뉴 커스텀 구현
  • 데이터베이스 매니저를 통한 로컬 데이터베이스 싱글턴 관리
  • 코어데이터 패치, 저장, 삭제 기능 구현
  • 뷰 모델 바인딩

핵심 코드

import Foundation
import CoreData
import Combine

class DatabaseManager {
    enum DatabaseError: LocalizedError {
        case failedToSaveData
        case failedToFetchData
        case failedToDeleteData
    }
    
    private var container: NSPersistentContainer?
    private var context: NSManagedObjectContext {
        guard let context = container?.viewContext else { fatalError() }
        return context
    }
    static let shared = DatabaseManager()
    private init() {
    }
    
    func setUp(with modelName: String) {
        container = NSPersistentContainer(name: modelName)
        container?.loadPersistentStores(completionHandler: { description, error in
            guard error == nil else { return }
            print("Successfully Loaded CoreData")
        })
    }
...
}
  • 코어 데이터 관리를 위한 싱글턴 매니저 클래스 구현
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        DatabaseManager.shared.setUp(with: "NetflixContentModel")
        return true
    }
  • 앱 딜리게이트 상에서 최초의 setUp 이후 데이터베이스 클래스 사용 가능
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
        guard let indexPath = indexPaths.first else { return nil }
        let config = UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
            let downloadAction = UIAction(title: "Donwload", subtitle: nil, image: nil, identifier: nil, discoverabilityTitle: nil, state: .off) { _ in
                self?.downloadContentAt(indexPath: indexPath)
            }
            return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: [downloadAction])
        }
        return config
    }
  • 다운로드 이벤트를 실행하기 위한 한 가지 방법
  • 컬렉션 뷰 셀을 길게 탭할 경우 뜨는 컨텍스트 메뉴 커스텀 구현
  • 해당 액션 중 다운로드 버튼을 통해 코어데이터 내 해당 데이터 저장
downloadButton
            .tapPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                self?.viewModel.saveData()
            }
            .store(in: &cancellables)
  • 다운로드 이벤트를 실행하는 두 번째 방법
  • 컨텐츠 디테일 뷰에서 저장 버튼을 클릭하면 발생
func save(with model: ContentModel) -> AnyPublisher<Bool, Error> {
        let item = ContentItem(context: context)
        item.original_title = model.original_title
        item.id = Int64(model.id)
        item.title = model.title
        item.media_type = model.media_type
        item.poster_path = model.poster_path
        item.popularity = model.popularity
        item.release_date = model.release_date
        item.vote_count = Int64(model.vote_count)
        item.vote_average = model.vote_average
        item.overview = model.overview
        item.adult = model.adult
        
        return Future { [weak self] promise in
            do {
                try self?.context.save()
                promise(.success(true))
            } catch {
                promise(.failure(DatabaseError.failedToSaveData))
            }
        }
        .eraseToAnyPublisher()
    }
  • Int64 등 코어 데이터에서 사용하는 프로퍼티로 변환한 새로운 아이템을 저장
import Foundation
import Combine

class DownloadViewModel {
    let downloadedContentsModel: CurrentValueSubject<[ContentModel], Never> = .init([])
    private var cancellables = Set<AnyCancellable>()
    private let database = DatabaseManager.shared
    
    func deleteData(model: ContentModel) {
        database
            .delete(with: model)
            .sink { completion in
                switch completion {
                case .failure(let error):
                    print(error.localizedDescription)
                case .finished: break
                }
            } receiveValue: { [weak self] success in
                self?.fetchData()
            }
            .store(in: &cancellables)
    }
    
    func fetchData() {
        database
            .fetch()
            .sink { completion in
                switch completion {
                case .failure(let error): print(error.localizedDescription)
                case .finished: break
                }
            } receiveValue: { [weak self] models in
                self?.downloadedContentsModel.send(models)
            }
            .store(in: &cancellables)
    }
}
  • 다운로드받은 데이터를 테이블 뷰의 데이터 소스로 제공하는 뷰 모델
  • 해당 뷰 컨트롤러의 viewWillAppear 단에서 자동으로 fetch가 이루어지기 때문에 데이터 업데이트 가능
  • 데이터 패치 및 삭제 기능 제공
extension DownloadViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            let model = viewModel.downloadedContentsModel.value[indexPath.row]
            viewModel.deleteData(model: model)
        }
    }
}
  • 삭제의 경우 기존 델리게이트 제공 함수를 통해 뷰 모델의 삭제 함수 호출

구현 화면

실제 넷플릭스의 모든 인터렉션을 담기에는 UI를 그리는 디테일한 능력도, 데이터 API 역시 부족했지만, 컬렉션 뷰의 컨텍스트 메뉴, 검색 바 커스텀 등 다양한 종류로 뷰를 그릴 수 있다는 데 자신감을 얻었다! 사실 강의의 내용과는 매우 달라진 결과물을 얻게 되었는데, 생각보다 매우 재미있었다.

profile
JUST DO IT
post-custom-banner

0개의 댓글