구현 내용
- 영화 리스트를 길게 누르면 해당 영화를 저장한다.
- 앱을 껐다 켜도 그 내용이 사라지지 않게 코어 데이터에 저장한다.
구현 코드
- 코어 데이터의 Entities를 지정한다.
- 이 때 해당 내용은 데이터 모델과 같아야 한다
(같지 않아도 되지만, 뭐 여기선 그렇게 한다.)
import Foundation
struct TrendingTitleResponse: Codable {
let results: [Title]
}
struct Title: Codable {
let id: Int
let media_type: String?
let original_name: String?
let original_title: String?
let poster_path: String?
let overview: String?
let vote_count: Int
let release_date: String?
let vote_average: Double
}
- 코어 데이터의 데이터를 처리할 함수를 구현한다.
import Foundation
import UIKit
import CoreData
class DatePersistenceManager {
enum DatabaseError: Error {
case failedToSave
case failedToFetchData
case failedToDeleteData
}
static let shared = DatePersistenceManager()
func downloadTitleWith(model: Title, completion: @escaping (Result<Void, Error>) -> Void) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let context = appDelegate.persistentContainer.viewContext
// 먼저 이미 존재하는지 확인
let request: NSFetchRequest<TitleItem> = TitleItem.fetchRequest()
request.predicate = NSPredicate(format: "id == %d", model.id)
do {
let existingTitles = try context.fetch(request)
if let existingTitle = existingTitles.first {
// 이미 존재하면 업데이트
existingTitle.original_title = model.original_title
existingTitle.original_name = model.original_name
existingTitle.overview = model.overview
existingTitle.media_type = model.media_type
existingTitle.poster_path = model.poster_path
existingTitle.release_date = model.release_date
existingTitle.vote_count = Int64(model.vote_count)
existingTitle.vote_average = model.vote_average
} else {
// 존재하지 않으면 새로 생성
let item = TitleItem(context: context)
item.id = Int64(model.id)
item.original_title = model.original_title
item.original_name = model.original_name
item.overview = model.overview
item.media_type = model.media_type
item.poster_path = model.poster_path
item.release_date = model.release_date
item.vote_count = Int64(model.vote_count)
item.vote_average = model.vote_average
}
try context.save()
completion(.success(()))
} catch {
completion(.failure(DatabaseError.failedToSave))
}
}
func fetchingTitlesFromDataBase(completion: @escaping (Result<[TitleItem], Error>) -> Void) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let context = appDelegate.persistentContainer.viewContext
let request: NSFetchRequest<TitleItem>
request = TitleItem.fetchRequest()
do {
let titles = try context.fetch(request)
completion(.success(titles))
} catch {
completion(.failure(DatabaseError.failedToFetchData))
}
}
func deleteTitleWith(model: TitleItem, completion: @escaping (Result<Void, Error>) -> Void) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
let context = appDelegate.persistentContainer.viewContext
context.delete(model)
do {
try context.save()
completion(.success(()))
} catch {
completion(.failure(DatabaseError.failedToDeleteData))
}
}
}
- 클래스 구조: DatePersistenceManager는 싱글톤 패턴을 사용하여 구현했다.
- (static let shared = DatePersistenceManager()). 이는 앱 전체에서 하나의 인스턴스만 사용하도록 보장한다.
- 에러 처리: DatabaseError 열거형을 정의하여 데이터베이스 작업 중 발생할 수 있는 오류를 명시적으로 처리한다.
- CRUD 작업: a. Create (downloadTitleWith):
- Title 모델을 받아 Core Data의 TitleItem 엔티티로 변환하여 저장한다.
- AppDelegate를 통해 Core Data 컨텍스트에 접근하고 모든 속성을 설정한 후 컨텍스트를 저장한다.
- 작업 완료 후 결과를 completion handler를 통해 비동기적으로 반환한다.
- 저장된 모든 TitleItem을 가져오는데, NSFetchRequest를 사용하여 쿼리를 실행한다.
- 결과를 completion handler를 통해 비동기적으로 반환한다.
- 주어진 TitleItem 모델을 Core Data 컨텍스트에서 삭제한다.
- 변경사항을 저장하고 결과를 completion handler를 통해 반환한다.
- 비동기 처리:
- 모든 메서드는 @escaping closure를 사용하여 비동기적으로 결과를 반환하고, 이는 데이터베이스 작업이 메인 스레드를 차단하지 않도록 한다.
- 에러 핸들링:
- 각 작업에서 do-catch 블록을 사용하여 오류를 포착하고 적절한 DatabaseError를 반환했다.
- Core Data 스택 사용:
- AppDelegate의 persistentContainer를 통해 Core Data 스택에 접근한다.
- 컬렉션 뷰의 아이템이 있는 파일 안에 코어데이터 함수를 호출한다.
import UIKit
class CollectionViewTableViewCell: UITableViewCell {
...
private func downloadTitleAt(indexPath: IndexPath) {
DatePersistenceManager.shared.downloadTitleWith(model: titles[indexPath.row]) { result in
switch result {
case .success():
NotificationCenter.default.post(name: NSNotification.Name("downloaded"), object: nil)
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
extension CollectionViewTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
...
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemsAt indexPaths: [IndexPath], point: CGPoint) -> UIContextMenuConfiguration? {
guard let indexPath = indexPaths.first else {
return nil // 유효한 IndexPath가 없으면 nil 반환
}
let config = UIContextMenuConfiguration(
identifier: nil,
previewProvider: nil) { [weak self] _ in
let downloadAction = UIAction(
title: "Download",
image: UIImage(systemName: "arrow.down.circle"),
identifier: nil,
discoverabilityTitle: nil,
state: .off) { _ in
self?.downloadTitleAt(indexPath: indexPath)
}
return UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: [downloadAction])
}
return config
}
}
private func downloadTitleAt(indexPath: IndexPath) {
DatePersistenceManager.shared.downloadTitleWith(model: titles[indexPath.row]) { result in
switch result {
case .success():
NotificationCenter.default.post(name: NSNotification.Name("downloaded"), object: nil)
case .failure(let error):
print(error.localizedDescription)
}
}
}
- 함수 목적:
- 이 함수는 특정 인덱스 패스에 해당하는 타이틀(영화나 TV 쇼 등)을 다운로드(또는 저장)하는 역할을 한다.
- 매개변수:
- indexPath: 다운로드할 타이틀의 위치를 나타내는 IndexPath 객체다.
- 구현 내용:
- DatePersistenceManager.shared.downloadTitleWith(model:completion:) 메서드를 호출한다.
- titles[indexPath.row]를 통해 해당 인덱스의 타이틀 모델을 전달한다.
- 비동기 처리:
- downloadTitleWith 메서드는 비동기적으로 동작하며, 완료 시 클로저를 통해 결과를 반환한다.
- 결과 처리:
- 성공 시: NotificationCenter를 통해 "downloaded" 라는 이름의 알림을 발송한다.
- 실패 시: 에러 메시지를 콘솔에 출력한다.
- 알림 사용:
- "downloaded" 알림을 발송함으로써, 앱의 다른 부분(예: UI 업데이트)에 다운로드 완료를 알릴 수 있다.
import UIKit
class DownloadsViewController: UIViewController {
// MARK: Variables
private var titles: [TitleItem] = []
// MARK: UI Components
private let downloadedTable: UITableView = {
let table = UITableView()
table.register(TitleTableViewCell.self, forCellReuseIdentifier: TitleTableViewCell.identifier)
return table
}()
// MARK: Life Cycle
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
title = "Downloads"
navigationController?.navigationBar.prefersLargeTitles = true
navigationController?.navigationItem.largeTitleDisplayMode = .always
view.addSubview(downloadedTable)
downloadedTableDelegate()
fetchLocalStorageForDownload()
NotificationCenter.default.addObserver(forName: NSNotification.Name("downloaded"), object: nil, queue: nil) { _ in
self.fetchLocalStorageForDownload()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
downloadedTable.frame = view.bounds
}
// MARK: Functions
private func downloadedTableDelegate() {
downloadedTable.delegate = self
downloadedTable.dataSource = self
}
private func fetchLocalStorageForDownload() {
DatePersistenceManager.shared.fetchingTitlesFromDataBase { [weak self] result in
switch result {
case .success(let titles):
DispatchQueue.main.async {
self?.titles = titles
self?.downloadedTable.reloadData()
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
// MARK: Extensions
extension DownloadsViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return titles.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: TitleTableViewCell.identifier, for: indexPath) as? TitleTableViewCell else { return UITableViewCell() }
let title = titles[indexPath.row]
cell.configure(with: TitleViewModel(titleName: (title.original_title ?? title.original_name) ?? "UnKnown", posterURL: title.poster_path ?? ""))
// 셀을 선택할 때 회색 배경 비활성화된다.
cell.selectionStyle = .none
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 140
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
switch editingStyle {
case .delete:
DatePersistenceManager.shared.deleteTitleWith(model: titles[indexPath.row]) { result in
switch result {
case .success():
print("Deleted from the databse")
case .failure(let error):
print(error.localizedDescription)
}
self.titles.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
default:
break;
}
}
}
private func fetchLocalStorageForDownload() {
DatePersistenceManager.shared.fetchingTitlesFromDataBase { [weak self] result in
switch result {
case .success(let titles):
DispatchQueue.main.async {
self?.titles = titles
self?.downloadedTable.reloadData()
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
- 목적:
- 이 메서드는 로컬 데이터베이스(Core Data)에서 저장된 타이틀들을 가져오는 역할을 한다.
- 구현:
- DatePersistenceManager.shared.fetchingTitlesFromDataBase를 호출하여 데이터를 비동기적으로 가져온다.
- 결과를 받으면 성공 또는 실패에 따라 다르게 처리하낟.
- 성공 시 처리:
- 메인 스레드에서 UI 업데이트를 수행한다.
- 가져온 타이틀들을 self.titles에 저장하고, downloadedTable을 리로드하여 UI를 업데이트합니다.
- 실패 시 처리:
- 에러 메시지를 콘솔에 출력한다.
- 메모리 관리:
- [weak self]를 사용하여 강한 참조 순환을 방지한다.
override func viewDidLoad() {
super.viewDidLoad()
...
fetchLocalStorageForDownload()
NotificationCenter.default.addObserver(forName: NSNotification.Name("downloaded"), object: nil, queue: nil) { _ in
self.fetchLocalStorageForDownload()
}
}
- fetchLocalStorageForDownload() 함수를 viewDidLoad() 와 NotificationCenter 두 군데에서 호출한 이유
- viewDidLoad()에서의 호출:
- 뷰 컨트롤러가 처음 로드될 때 저장된 데이터를 가져와 표시한다.
- 이는 사용자가 화면에 진입했을 때 최신 데이터를 볼 수 있게 한다.
- NotificationCenter 옵저버에서의 호출:
- "downloaded" 알림이 발생했을 때 (즉, 새로운 항목이 다운로드되었을 때) 데이터를 다시 가져온다.
- 이를 통해 다운로드된 새 항목이 즉시 목록에 반영된다.
'iOS > UIKIT' 카테고리의 다른 글
git에 개인 API_KEY 안보이게 올리는 방법 (0) | 2024.08.07 |
---|---|
Xcode에서 iPhone Orientation 설정 (0) | 2024.08.01 |
컬렉션 뷰의 아이템을 길게 눌렀을 때, 호출되는 메서드 사용하기 (0) | 2024.07.11 |
검색 결과로 나온 영화 포스터를 누르면 상세페이지로 넘어가기 (0) | 2024.07.10 |
랜덤으로 영화 표시하기 (0) | 2024.07.10 |