책 검색 앱 만들기 (5)

일단 거의 구현은 완성되었으나 디테일 하게 몇몇 부분이 아직 완성되지 않아 최대한 오늘 안에 끝낼 수 있도록 한번 해 볼 생각이다.

 

2024.04.30 - [◽️ Programming/T I L] - 책 검색 앱 만들기 (1)

2024.05.01 - [◽️ Programming/T I L] - 책 검색 앱 만들기 (2)

2024.05.03 - [◽️ Programming/T I L] - 책 검색 앱 만들기 (3)

2024.05.07 - [◽️ Programming/T I L] - 책 검색 앱 만들기 (4)

마이 페이지 구현

CoreData 불러와 마이페이지 테이블 뷰 내 구현

이전에 책 상세 페이지에서 CoreData에 책을 담아 저장했다면 이곳에 다시 그 데이터를 불러와 테이블 뷰에 보일 수 있도록 할 예정이다.

func getBookListFromCoreData() -> [BookCoreData] {
    var bookList: [BookCoreData] = []
    
    if let context = context {
        let request = NSFetchRequest<NSManagedObject>(entityName: self.coreDataName)
        let idOrder = NSSortDescriptor(key: "title", ascending: true)
        request.sortDescriptors = [idOrder]
        
        do {
            if let fetchBookList = try context.fetch(request) as? [BookCoreData] {
                bookList = fetchBookList
            }
        } catch {
            print("가져오기 실패")
        }
    }
    
    return bookList
}

이렇게 구현 한 코어데이터의 GET 메서드를 활용해 가져와서 쉽게 불러올 수 있다.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    bookList = CoreDataManager.shared.getBookListFromCoreData()
    mypageTableView.reloadData()
}

viewWillAppear를 활용해 책을 저장할 때마다 마이페이지에서 확인할 때 데이터를 불러오고 테이블 뷰를 리로드 하는 방식으로 구현했다.

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    bookList.count
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return 70
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: MypageTableViewCell.identifier, for: indexPath) as? MypageTableViewCell else { fatalError("테이블 뷰 에러") }
    
    let book = bookList[indexPath.row]
    
    cell.mainTitle.text = book.title
    cell.subTitle.text = book.authors
    cell.priceTitle.text = formattedPrice(Int(book.price))
            
    return cell
}

그리고 테이블 뷰의 데이터 소스를 활용해 이렇게 데이터를 넣으면 내가 저장한 데이터가 마이페이지에서 확인이 가능하다 🙂

책 리스트 전체 삭제 , 부분 삭제 구현

그리고 이제 저장된 데이터를 원하는 값을 삭제 혹은 전체를 삭제할 수 있도록 구현해 보자!

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        let bookToDelete = bookList[indexPath.row]
        
        CoreDataManager.shared.deleteBookList(bookToDelete) {
            self.bookList.remove(at: indexPath.row)
            tableView.deleteRows(at: [indexPath], with: .fade)
        }
    }
} 

부분 삭제의 경우는 애플에서 이미 구현되어 있는 스와이프 를 활용해 삭제하는 방법을 사용했다.

 

특정 셀의 값을 스와이프하고 삭제하면 코어데이터에 있는 데이터도 삭제 될 수 있도록 구현했다.

func deleteAllBooks(completion: @escaping () -> Void) {
    guard let context = context else {
        print("CoreData context가 유효하지 않습니다.")
        return
    }
    
    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: "BookCoreData")
    let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
    
    do {
        try context.execute(deleteRequest)
        try context.save()
        completion()
    } catch {
        print("전체 책 삭제에 실패했습니다: \\(error)")
    }
}

전체 삭제하는 메서드는 CoreDataManager에 전체 삭제를 할 수 있는 메서드를 따로 만들어 언제든 사용할 수 있도록 구현했다.

leftButton.addTarget(self, action: #selector(deleteAllBooks), for: .touchUpInside)

전체 삭제 버튼을 미리 오토레이아웃 잡을때 만들어놨기 때문에 addTarget 을 통해 액션을 넣어주면 된다.

@objc func deleteAllBooks() {
    let alertController = UIAlertController(title: "삭제 확인", message: "담은 책을 모두 삭제 하시겠습니까?", preferredStyle: .alert)
    
    let deleteAction = UIAlertAction(title: "삭제", style: .destructive) { _ in
        CoreDataManager.shared.deleteAllBooks {
            self.bookList.removeAll()
            self.mypageTableView.reloadData()
        }
    }
    alertController.addAction(deleteAction)
    
    let cancelAction = UIAlertAction(title: "취소", style: .cancel, handler: nil)
    alertController.addAction(cancelAction)
    
    present(alertController, animated: true, completion: nil)
}

전체 삭제는 버튼 한번 눌렸을때 지워져버리면 안될 것 같아 알럿을 띄워 전체 삭제함을 다시 한번 선택 할 수 있도록 구현했다.

 

이렇게 하면 전체 삭제 및 부분 삭제 구현이 완료 된다 🙂

책 상세 페이지 추가 구현

플로팅 버튼 내 검색하기 구현

플로팅 버튼에서 확인되는 검색하기 버튼을 누르면 이전 책 검색하는 VC로 이동하여 검색 창이 활성화 되는 구현을 할 예정이다.

원래는 dismiss를 활용해 간편하게 넘긴 뒤 completion handler를 통해 구현하려고 했으나, 서치바 델리게이트로 작동을 하고 있는 문제인지.. 뭔지 모르겠지만 원하는 데로 구현이 잘 되지 않았다.

 

그래서 프로토콜을 활용해 델리게이트 패턴을 만들어 해당 내용으로 서치바를 활성화 시켜보고자 한다.

protocol SearchViewControllerDelegate: AnyObject {
    func searchButtonPressed()
} 

먼저 이렇게 프로토콜을 선언한 뒤 메서드를 구현할 수 있도록 틀을 만들어 구성한다.

extension SearchViewController: UICollectionViewDataSource, UICollectionViewDelegate, SearchViewControllerDelegate {
  
  func searchButtonPressed() {
      self.searchBar.becomeFirstResponder()
  }
}

그 다음 VC를 확장한 곳에 동일하게 해당 프로토콜을 선언한 뒤 메서드에 원하는 구현 방식을 넣어준다. 나는 SearchViewController 에 넘어와서 서치바가 활성화 될 수 있도록 하고 싶기 때문에 becomeFirstResponder() 를 활용해 코드를 구성했다.

 

그 다음 이 기능을 사용할 책 상세 페이지 VC로 넘어가서

weak var delegate: SearchViewControllerDelegate?

해당 델리게이트를 선언할 수 있도록 불러온 뒤

actionButton.addItem(title: "검색하기", image: UIImage(systemName: "magnifyingglass")?.withRenderingMode(.alwaysTemplate)) { item in
    self.dismiss(animated: true) {
        self.delegate?.searchButtonPressed()
    }
}

검색하기 버튼을 눌러 모달이 내려가면 completion 핸들러가 실행돼 searchButtonPressed() 가 실행될 수 있도록 코드를 구현하면 된다.

이렇게 검색하기 기능 구현이 완료!

 

책 담기 기능 성공 시 알럿으로 알림

원하는 책을 담으면 해당 책이 저장되었음을 알리는 알럿 기능을 넣어 책 상세페이지 구현을 마무리 하고자 한다.

func showAlert(message: String) {
    let alert = UIAlertController(title: "알림", message: message, preferredStyle: .alert)
    let okAction = UIAlertAction(title: "확인", style: .default) { _ in
        self.dismiss(animated: true, completion: nil)
    }
    alert.addAction(okAction)
    present(alert, animated: true, completion: nil)
}

어디서든 쉽게 알림 창으로 쓸 수 있는 범용성 있는 알럿 메서드를 하나 만든 뒤

actionButton.addItem(title: "책담기", image: UIImage(systemName: "book.fill")?.withRenderingMode(.alwaysTemplate)) { [self] item in
    self.saveBookToCoreData()
    
    let bookName = self.bookData?.title
    if let bookName = bookName {
        showAlert(message: "\\(bookName) 저장되었습니다.")
    } else {
        showAlert(message: "책이름 없이 저장되었습니다.")
    }
}

플로팅 버튼 내 책 담기를 누르면 코어데이터를 저장하는 메서드가 실행되고 저장된 책 데이터의 title이 알럿 메시지에 포함되어 알럿이 뜨도록 구현했다.

혹시 책 제목이 없다면 없이 저장되는 예외처리도 같이 진행했다.

이렇게 해서 책 상세 페이지 구현 완료 🙂

 

최근 본 책 구현하기

최근 본 책 컬렉션 뷰 데이터 전달

최근 본 책의 데이터를 어떻게 전달할까 많이 고민하다가 이 내용은 코어데이터에 저장되어 있는 것 보다 휘발성이 있는 편이 나을 것 같다고 생각해 그냥 데이터만 전달하고 앱을 다시 키면 초기화 할 수 있도록 구현했다.

struct RecentlyBookInfo {
    let title: String
    let thumbnail: String
    let authors: [String]
    let price: Int
    let contents: String
    
    
    func authorsToString() -> String {
        return authors.joined(separator: ", ")
    }
    
    func formattedPrice() -> String {
        let numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .decimal
        let formattedPrice = numberFormatter.string(from: NSNumber(value: price)) ?? ""
        return "\\(formattedPrice)원"
    }
}

데이터 모델링을 새롭게 하나 다시 만들어 최근 본 책만 들어갈 수 있도록 만들었다.

func addToRecentlyViewedBook(indexPath: IndexPath) {
    guard let selectedBook = bookData?.documents[indexPath.item] else { return }
    guard !recentlyViewedBooks.contains(where: { $0.title == selectedBook.title }) else { return }
    
    let recentlyBookInfo = RecentlyBookInfo(title: selectedBook.title, thumbnail: selectedBook.thumbnail, authors: selectedBook.authors, price: selectedBook.price, contents: selectedBook.contents)
    recentlyViewedBooks.insert(recentlyBookInfo, at: 0)
    
    collectionView.reloadData()
}

그리고 최근 본 책 데이터를 책 리스트에서 셀이 선택되었을 때 상세페이지로 넘어간 데이터가 최근 본 책에도 들어올 수 있도록 메서드를 하나 만들었다.

 

원래 데이터가 들어가는걸 append로 했다가 최근 본 책이 오른쪽으로 밀려나는 것 같아 insert를 통해 가장 왼쪽부터 데이터가 들어오도록 수정했다.

func showBookDetail(at indexPath: IndexPath) {
    let detailVC = DetailViewController()
    let bookData = bookData?.documents[indexPath.item]
    
    detailVC.bookData = bookData
    detailVC.delegate = self
    
    detailVC.mainTitle.text = bookData?.title
    detailVC.bookContents.text = bookData?.contents
    detailVC.subTitle.text = bookData?.authorsToString()
    detailVC.bookPrice.text = bookData?.formattedPrice()
    
    if let imageURL = bookData?.thumbnail, let imageURL = URL(string: imageURL) {
        detailVC.bookImageView.kf.setImage(with: imageURL)
    }
    
    present(detailVC, animated: true, completion: nil)
}

이렇게 메서드를 구현하고 셀이 선택되었을때 디테일 뷰에 데이터를 넘기는 방식도 메서드로 만들어 둔다.

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    switch indexPath.section {
    case 0:
        showBookRecent(at: indexPath)
        break
    case 1:
        addToRecentlyViewedBook(indexPath: indexPath)
        showBookDetail(at: indexPath)
    default:
        break
    }
}

이렇게 구현하면 BookList 의 책을 눌렀을 때 상세페이지로 이동하게 되면 상세 페이지에서 확인되는 데이터가 최근 본 책의 데이터로 들어가게 된다.

이렇게 만들어 주면 책을 선택해 상세페이지로 넘어가면 해당 데이터가 최근 본 책으로 넘어갈 수 있게 된다 🙂

 

최근 본 책 탭하면 상세페이지 이동

func showBookRecent(at indexPath: IndexPath) {
    let detailVC = DetailViewController()
    let recentBook = recentlyViewedBooks[indexPath.item]
    let bookData = bookData?.documents[indexPath.item]
    
    detailVC.bookData = bookData
    detailVC.delegate = self
    
    detailVC.mainTitle.text = recentBook.title
    detailVC.subTitle.text = recentBook.authorsToString()
    detailVC.bookContents.text = recentBook.contents
    detailVC.bookPrice.text = recentBook.formattedPrice()
    if let imageURL = URL(string: recentBook.thumbnail) {
        detailVC.bookImageView.kf.setImage(with: imageURL)
    }
    
    present(detailVC, animated: true, completion: nil)
}

전달 받은 데이터를 DetailViewController와 연결해 이렇게 메서드를 구현하고 didSelectItemAt 의 최근 본 책 부분에 메서드를 실행 하면 이렇게 최근 본 책의 탭이 가능해지고 상세페이지도 동일하게 사용이 가능하다 🙂

남은 구현 모두 완성해서 MVVM 한번 도전 해보자!

'◽️ Programming > T I L' 카테고리의 다른 글

[Project 일지] 단어장 앱 만들기 (1)  (0) 2024.05.13
책 검색 앱 만들기 (6)  (2) 2024.05.09
책 검색 앱 만들기 (4)  (3) 2024.05.07
책 검색 앱 만들기 (3)  (0) 2024.05.03
책 검색 앱 만들기 (2)  (3) 2024.05.01