내일배움캠프 iOS

UIKit) TIL # 53 앱개발 심화주차 개인과제 - Lv3 상세화면, 담은 책 목록, CoreData

yjuni22 2025. 1. 3. 20:51

 

검색 후 해당 셀을 클릭했을 때 상세화면이 나오도록 해야한다.

 

셀 클릭 시 상세화면 띄우기

- UICollectionViewDelegate 의 didSelectItemAt 메서드 활용

 

extension SearchTapViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
        let detailVC = DetailBookViewController()
        
        // 알럿 delegate 설정
        detailVC.delegate = self
        
        switch indexPath.section {
        case 0:
            return
        case 1:
            detailVC.bookData = bookArrays[indexPath.row]
        default:
            return
        }
        
        present(detailVC, animated: true, completion: nil)
    }
}

 

상세화면에 해당 셀의 데이터를 전달하고

present로 간단하게 화면을 띄울 수 있다.

기본적으로 모달형식으로 화면이 띄워지고 fullScreen 등의 설정을 할 수 있다.

 

 

담기 버튼 기능 구현 - CoreData

코어데이터 매니저

 

코어데이터를 위해 Entity 파일을 생성 후

 

// MARK: - [Create] 코어데이터에 데이터 생성 (Document -> BookSaved)
    func saveBook(with book: Document, completion: @escaping () -> Void) {
        
        // 임시저장소 있는지 확인
        if let context = context {
            
            // 임시저장소에 있는 데이터를 그려줄 형태 파악하기
            if let entity = NSEntityDescription.entity(forEntityName: self.modelName, in: context) {
                
                // 임시저장소에 올라가게 할 객체만들기 (NSManagedObject ===> BookSaved)
                if let bookSaved = NSManagedObject(entity: entity, insertInto: context) as? BookSaved {
                    
                    // MARK: - BookSaved에 실제 데이터 할당
                    bookSaved.title = book.title
                    bookSaved.authors = book.authors?.first
                    bookSaved.price = "\(book.price ?? 0)원"
                    
                    appDelegate?.saveContext()
                }
            }
        }
        completion()
    }

 

코어 데이터에 저장하는 메서드를 구현하였다.


담기 버튼 클로저 활용

- 뷰와 뷰컨트롤러를 나누어 놓았고 뷰에 버튼을 생성해두었다.
뷰컨트롤러에서 addTarget을 사용하기 위해 클로저를 활용하였다.

 

 상세화면 뷰 컨트롤러

final class DetailBookViewController: UIViewController {

 // 1. 버튼이 눌렸을 때 클로저 전달(할당)
    private func setupButtonActions() {
        
        detailBookView.saveButtonPressed = { [weak self] in
               guard let self = self, let bookData = self.bookData else { return }
               self.coreDataManager.saveBook(with: bookData) {
                   
                   self.dismiss(animated: true)
                   
                   self.delegate?.didSaveBook(title: bookData.title ?? "")
               }
           }
           
        detailBookView.closeButtonPressed = { [weak self] in
            self?.dismiss(animated: true)
           }
       }       
}

 

뷰컨트롤러에서 상세화면의 버튼 클로저를 통해 행동 할당

saveBook 으로 셀 클릭시 넘어온 해당 데이터를 코어데이터에 저장


- 상세화면 뷰 클래스

final class DetailBookView: UIView {
    
    // 2. 뷰컨에 있는 클로저 저장(할당)
    var saveButtonPressed: () -> Void =  { }
    var closeButtonPressed: () -> Void =  { }
    
        lazy var saveButton: UIButton = {
        let button = UIButton()
        button.setTitle("담기", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .systemGreen
        button.layer.cornerRadius = 20
        button.addTarget(self, action: #selector(saveButtonTapped), for: .touchUpInside)
        return button
    }()
    
    lazy var closeButton: UIButton = {
        let button = UIButton()
        button.setTitle("X", for: .normal)
        button.setTitleColor(.white, for: .normal)
        button.backgroundColor = .gray
        button.layer.cornerRadius = 20
        button.addTarget(self, action: #selector (closeButtonTapped), for: .touchUpInside)
        return button
    }()
    
      // 3. 버튼이 눌리면 ButtonPressed 에 들어있는 클로저 실행
    @objc func saveButtonTapped(_ sender: UIButton) {
        saveButtonPressed()
    }
    
    @objc func closeButtonTapped(_ sender: UIButton) {
        closeButtonPressed()
    }

 

빈 클로저에 뷰컨트롤러에서 전달한 클로저가 할당되고

버튼을 누르면 해당 클로저가 실행된다.


Delegate로 알럿 띄우기

protocol SaveBookDelegate: AnyObject {
    func didSaveBook(title: String)
}

 

Delegate 프로토콜을 생성하고

/// 알럿 delegate 프로토콜
extension SearchTapViewController: SaveBookDelegate {
    func didSaveBook(title: String) {
        
        let alert = UIAlertController(title: nil, message: "‘\(title)’\n책 담기 완료!", preferredStyle: .alert)
        
               alert.addAction(UIAlertAction(title: "확인", style: .default))
        
               present(alert, animated: true, completion: nil)
    }
}

 

사용할 뷰컨트롤러에서 채택 및 알럿 구현

 

 위의  버튼 클로저 에서

self.delegate?.didSaveBook(title: bookData.title ?? "") 으로 해당 책의 제목을 전달하고 있음

 

 

 

상세화면에서 delegate 변수 생성

 

 

Delegate를 사용하려면 채택한 곳에서 대리자 설정을 해주어야 한다.

 

didSelectAt 에서 상세화면의 대리자 = 메인화면 으로 설정

 

담기 버튼을 누르면 화면이 닫히고 알럿이 띄워진다.

 

 


담은 책 리스트 탭 구현 - CoreData

위에서 코어데이터에 저장한 데이터를 활용하여 담은 책 리스트에 보여지게 하면 된다.

 

코어데이터 Read

func getBookSavedArrayFromCoreData() -> [BookSaved] {
        
        var savedBookList: [BookSaved] = []
        
        // 임시저장소 있는지 확인
        if let context = context {
            // 요청서
            let request = NSFetchRequest<NSManagedObject>(entityName: self.modelName)
            
            do {
                // 임시저장소에서 (요청서를 통해서) 데이터 가져오기
                if let fetchedBookList = try context.fetch(request) as? [BookSaved] {
                    savedBookList = fetchedBookList
                }
            } catch {
                print("가져오는 것 실패")
            }
        }
        
        return savedBookList
    }

 

SavedTapViewController 에서 이를 전달받을 변수를 생성하여 활용

 

final class SavedTapViewController: UIViewController {

  private let savedTapView = SavedTapView()
    
    var savedBooks: [BookSaved] = []
    
    let coreDataManager = CoreDataManager.shared
    
    override func loadView() {
        self.view = savedTapView
    }
    
    // 뷰가 나타날때마다
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // 코어데이터에 저장된 데이터 불러오기
        savedBooks = coreDataManager.getBookSavedArrayFromCoreData()
        savedTapView.tableView.reloadData()
    }
    
     override func viewDidLoad() {
        super.viewDidLoad()
        setupButtonAction()
        savedTapView.tableView.dataSource = self
    }
  
}

extension SavedTapViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        savedBooks.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = tableView.dequeueReusableCell(withIdentifier: SavedListCell.id, for: indexPath) as! SavedListCell
        
        let bookSaved = savedBooks[indexPath.row]
        cell.bookNameLabel.text = bookSaved.title
        cell.authorNameLabel.text = bookSaved.authors
        cell.bookPriceLabel.text = bookSaved.price
        
        cell.selectionStyle = .none
        
        return cell
    }
}

 

코어데이터 전체삭제와 개별삭제

전체삭제

func deleteAllBooks(completion: @escaping () -> Void) {
        
        if let context = context {
            
            let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: self.modelName)
            
            let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
            
            do {
                // 전체 삭제 요청
                try context.execute(deleteRequest)
                context.reset()
                completion()
            } catch {
                print("전체 삭제 실패")
                completion()
            }
        }
    }

 

 

      try context.execute(deleteRequest)

            context.reset()

를 활용하면 쉽게 전체삭제가 가능하다.

 

개별 삭제

func deleteBook(with book: BookSaved) {
        
        if let context = context {
            
            context.delete(book)
            
            appDelegate?.saveContext()
        }
    }

 

해당하는 데이터를 삭제해야되기 때문에 해당 데이터를 삭제해야한다.


전체삭제, 추가 버튼 기능 구현

 

 /// 버튼 클로저 할당
    func setupButtonAction() {
        
        savedTapView.deleteButtonPressed = { [weak self] in
            guard let self = self else { return }
            
            // 알럿 생성
            let alert = UIAlertController(title: "⚠️ 주의", message: "정말 삭제하시겠습니까?", preferredStyle: .alert)
            
            let succes = UIAlertAction(title: "확인", style: .default) { action in
                
                // 코어데이터에서 전체 삭제
                self.coreDataManager.deleteAllBooks {
                    DispatchQueue.main.async {
                        self.savedBooks = []
                        self.savedTapView.tableView.reloadData()
                        print("전체 삭제 완료")
                    }
                }
            }
            
            let cancel = UIAlertAction(title: "취소", style: .cancel, handler: nil)
            
            alert.addAction(succes)
            alert.addAction(cancel)
            
            self.present(alert, animated: true, completion: nil)
        }
        
        savedTapView.plusButtonPressed = { [weak self] in
            guard let self = self else { return }
            
            // 화면 이동
            self.tabBarController?.selectedIndex = 0
            
            // 기존 검색 탭 뷰 컨트롤러의 서치바 활성화
            if let searchVC = self.tabBarController?.viewControllers?.first as? SearchTapViewController {
                searchVC.searchTapView.searchBar.becomeFirstResponder()
            }
        }
    }

 

전체삭제의 경우 알럿을 활용하였고

추가 버튼은 클릭 시 탭바의 index를 활용하여 쉽게 화면이동을 할 수 있었다.

그리고 탭바가 가지고 있는 뷰컨트롤러에 접근하여 서치바를 활성화 시킬 수 있었다.


테이블 뷰 스와이프삭제 기능

extension SavedTapViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        
        if editingStyle == .delete {
            
            let deleteBook = savedBooks[indexPath.row]
            
            // 배열에서 삭제
            savedBooks.remove(at: indexPath.row)
            
            // 코어데이터에서 삭제
            coreDataManager.deleteBook(with: deleteBook)
            
            // 삭제 애니메이션
            tableView.deleteRows(at: [indexPath], with: .fade)
            
        }
    }
    
    func tableView(_ tableView: UITableView, titleForDeleteConfirmationButtonForRowAt indexPath: IndexPath) -> String? {
        return "삭제"
    }
}

 

UITableViewDelegate 에서 주어지는 기능으로 스와이프 기능을 구현할 수 있었다.

 


테이블 뷰 스와이프 기능의 두가지 방법

1. tableView(_:editingStyleForRowAt:)

이 메서드는 UITableView에서 기본 제공 편집 스타일(삭제, 삽입)을 설정하기 위한 메서드이다.
주로 삭제 또는 삽입 작업에 사용된다.

 

2. tableView(_:trailingSwipeActionsConfigurationForRowAt:)

iOS 11에서 추가된 메서드로, 스와이프 시 다양한 작업을 커스터마이징할 수 있다.
삭제뿐 아니라 **다양한 액션(예: 편집, 공유, 즐겨찾기 추가)**을 추가할 수 있다.

 

쉽게 말해 1번은 간단한 구현에서 사용

2번은 커스터마이징 등 복잡한 구현에 사용한다고 한다.

 

 

 

 

참고)

 

https://velog.io/@loganberry/iOS-TableViewCell-row%EB%A5%BC-%EB%B0%80%EC%96%B4%EC%84%9C-%EC%82%AD%EC%A0%9C%ED%95%98%EA%B8%B0

 

[iOS] TableViewCell row를 밀어서 삭제하기

TableViewCell 밀어서 삭제하기

velog.io