[내일배움캠프 50일차] Ch 3. 앱개발 심화 주차 과제 - Advance2

NH·2025년 5월 13일

내일배움캠프

목록 보기
50/62
post-thumbnail

Step 3 - 책 상세 보기 & 담기 기능 구현

🔫 책 상세 화면 - 트러블 슈팅

문제: 검색 탭에 있는 데이터를 상세 화면으로 바인딩

첫 시도

  • 셀을 탭 하면 DetailBookViewController의 UI 값을 업데이트 하도록 함.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // 객체 생성
	let detailVC = DetailBookViewController()
        
    // UI 값 업데이트
    detailVC.bookTitle.text = data[indexPath.item].title
    print(data[indexPath.item].title)
    detailVC.authors.text = data[indexPath.item].authors.joined(separator: ", ")
    detailVC.priceLabel.text = String(data[indexPath.item].price)
        
    let urlString = data[indexPath.item].thumbnail
    guard let url = URL(string: urlString) else { return }
        
    DispatchQueue.global().async {
		if let data = try? Data(contentsOf: url) {
			if let image = UIImage(data: data) {
				DispatchQueue.main.sync {
					detailVC.thumbnail.image = image
				}
			}
		}
	}
}
  • 결과: 업데이트 반영 안됨!

두번째 시도

  • DetailBookViewController 에서 셀을 업데이트 하는 것처럼 configure 메소드 생성
func configure(data: BookData.Documents) {
        self.authors.text = data.authors.joined(separator: ",")
        self.bookTitle.text = data.title
    }
  • 셀을 탭하면 configure 메소드 실행되어 UI를 업데이트 하도록 함.
 func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
	let detailVC = DetailBookViewController()
	detailVC.configure(data: data[indexPath.item])
}
  • 결과: 업데이트 반영 안됨!

👨‍🏫 오류 원인

  • ViewDidLoad가 실행되기 전에 configure 메소드가 실행되어 UI 값이 업데이트가 일어나지 않은것

세번째 시도

해결 : 프로퍼티 주입을 사용하여 뷰->뷰 로 데이터를 전달

  • SearchTabViewController 에서 프로퍼티 주입 사용
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {    
	let detailVC = DetailBookViewController()

	// 책 상세 보기 화면에 데이터 전달
	detailVC.setBookData(data: data[indexPath.item])
}
  • DetailBookViewController에서 setBookData 메소드로 데이터를 받아옴
  • viewDidLoad 에서 updateUI 메소드를 동해 UI를 업데이트 하여 처리
override func viewDidLoad() {
	super.viewDidLoad()
        
	if let data = data {
		updateUI(data: data) // 검색 화면에서 받아온 데이터로 UI 업데이트
	}
}

func setBookData(data: BookData.Documents) {
        self.data = data
    }
    
    // 위에서 가져온 선택한 셀 데이터로 UI 값 업데이트
func updateUI(data: BookData.Documents) {
	self.bookTitle.text = data.title
	self.authors.text = data.authors.joined(separator: ",")
	if let url = URL(string: data.thumbnail) {
		DispatchQueue.global().async {
			if let imageData = try? Data(contentsOf: url),
				let image = UIImage(data: imageData) {
					DispatchQueue.main.async {
						self.thumbnail.image = image
					}
				}
			}
		}
	priceLabel.text = "\(data.price.formattedWithComma)원"
	detailLabel.text = data.contents
	}
}

문제: 책 담기 완료 알림창 델리게이트 패턴 사용해서 구현하기

첫 시도

  • 프로토콜 정의 (알림을 띄우는 기능만 명시)
protocol Alertable: AnyObject {
    func makeAlert(bookTitle: String)
}
  • 알림창 매니저 클래스 생성
class AlertManager: Alertable {
    func makeAlert() {
        let alert = UIAlertController(title: "로그인 실패", message: "로그인에 실패하였습니다.\n아이디와 비밀번호를 확인해주세요.", preferredStyle: .alert)
        let ok = UIAlertAction(title: "확인", style: .default, handler: nil)
        
        alert.addAction(ok)
        
        present(alert, animated: true, completion: nil)
    }
}
  • DetailBookViewController에서 선언 delegate 선언 (weak)
weak var delegate: Alertable?
  • 버튼 액션 등에서 호출
@objc private func didTapSaveButton() {
    delegate?.makeAlert(bookTitle: data.title)
    self.dismiss(animated: true)
}

👉 이렇게 했는데 present(alert, animated: true, completion: nil) 구문에서 에러 발생
👨‍🏫 오류 원인

  • present()는 UIViewController의 메서드
  • AlertManager가 UIViewController를 상속받지 않았기 때문에 present(alert, animated:completion:) 부분에서 에러 발생!
  • present()는 UIViewController의 메서드이기 때문에, AlertManager에서 직접 호출할 수 없다!

두번째 시도

  • UIViewController 상속 완료
protocol Alertable {
    func makeAlert(bookTitle: String)
}

class AlertManager: UIViewController, Alertable {
    func makeAlert(bookTitle: String) {
        let alert = UIAlertController(title: "책 담기 완료", message: "\(bookTitle) 책 담기 완료!", preferredStyle: .alert)
        let ok = UIAlertAction(title: "확인", style: .default, handler: nil)
        
        alert.addAction(ok)
        
        present(alert, animated: true, completion: nil)
    }
}
  • 담기 버튼 액션 함수 생성
@objc
    private func didTappedSaveButton() {
        self.dismissModal()
        guard let alert = delegate?.makeAlert(bookTitle: "fjfj") else { return }
        print(bookTitle)
    }
  • 결과: 동작 안함

👨‍🏫 오류 원인

  • AlertManager가 UIViewController를 상속받았지만, 실제로 화면에 표시되는 ViewController가 아님
  • present()는 현재 화면에 표시되고 있는 UIViewController가 호출해야 작동함.
  • 그런데 AlertManager는 직접 화면에 표시되지 않기 때문에 present()를 호출해도 아무 일도 안 일어남.

세번째 시도

  • SearchTabViewController에서 delegate 설정 및 구현
  • DetailBookViewController 객체에 delegate를 SearchTabViewController 로 설정
detailVC.delegate = self
  • SearchTabViewController 에서 Alertable 채택한 알림창을 띄우는 함수를 선언
  • SearchTabViewController는 UIViewController를 상속받고 있으므로 present() 함수를 호출할수있며, 알림창을 띄우는 화면이므로 알림창이 정상적으로 띄워진다!
extension SearchTabViewController: Alertable {
    func makeAlert(bookTitle: String) {
        let alert = UIAlertController(
            title: "책 담기 완료",
            message: "\(bookTitle) 책 담기 완료!",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "확인", style: .default))
        present(alert, animated: true)
    }
}
  • 결과: 성공!

느낀점

  • 델리게이트 패턴을 적용해보면서, 어느정도 델리게이트 패턴이 어떻게 동작하는지 알게되었다.
  • 몸에 열이 많아졌지만 많아진 만큼 배움도 컸다.

🔫 담은 책 목록 화면 - 트러블 슈팅

문제: 코어데이터 전체 삭제

처음에 단일 데이터를 for문을 사용하여 삭제하였으나, 너무 번거롭다는 생각이 들어 로직을 변경함.
NSBatchDeleteRequest 방식을 사용하여 메소드 수정

  • 기존 삭제 방식 (context.delete(...))

    for object in fetchedResults {
       context.delete(object)
    }
    try context.save()
    • 데이터를 모두 fetch해서 메모리에 올린 뒤, 하나씩 삭제하고 저장합니다.
    • 단점: 데이터가 많을수록 느리고 비효율적 (메모리에 다 올려야 하므로)
  • NSBatchDeleteRequest 방식

    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = BookEntity.fetchRequest()
    let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
    try context.execute(deleteRequest)
    • 데이터를 fetch하지 않고, 내부적으로 SQL 명령을 바로 날립니다.
    • 장점: 메모리에 데이터를 올리지 않아서 매우 빠르고 가볍다
    • 단점: 삭제된 객체에 대한 정보가 없기 때문에, UI와의 동기화는 수동 처리해야 함
  • 언제 NSBatchDeleteRequest를 써야 하나?

    상황추천 방식
    UI에서 하나씩 삭제하거나 상태 반영 필요context.delete(object)
    전체 삭제, 대량 삭제, 초기화 등NSBatchDeleteRequest
  • 전체 삭제 코드

func deleteAllBooks() {
    let fetchRequest: NSFetchRequest<NSFetchRequestResult> = BookEntity.fetchRequest()
    let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
    
    do {
        try context.execute(batchDeleteRequest)
        try context.save()
        bookEntityData.removeAll() // 배열 동기화도 함께
    } catch {
        print("전체 삭제 실패: \(error)")
    }
}

문제: First Responder 이용해서 검색 탭 서치바 활성화 시키기

먼저 First Responder 란?

  • First Responder는 iOS에서 가장 먼저 사용자 입력(터치, 키보드, 제스처 등)을 받는 객체를 의미

주요 역할

역할설명
입력 이벤트 처리터치나 키보드 입력을 가장 먼저 받음
키보드 표시 제어textField.becomeFirstResponder() → 키보드 표시
textField.resignFirstResponder() → 키보드 내림
UIResponder 체인의 시작점이벤트가 처리되지 않으면 위로 전달됨 (예: 뷰 → 뷰컨트롤러 → 윈도우 등)

사용 예시

textField.becomeFirstResponder() // 키보드 올라옴
textField.resignFirstResponder() // 키보드 내려감

기억하면 좋은 점

  • First Responder는 오직 하나만 존재
  • 일반적으로 텍스트 입력 필드나 버튼이 가장 많이 사용
  • 터치나 키보드 입력을 적절한 UI 요소에 전달하기 위한 중요한 개념

첫 시도

  • 새로 인스턴스 만든 후 becomeFirstResponder() 사용하여 검색바 활성화
let searchTabVC = SearchTabViewController()
searchTabVC.searchBar.becomeFirstResponder()
  • 결과: 실패 - 아무동작 X

👨‍🏫 오류 원인

  • 새로운 인스턴스를 만들었을 뿐이라 동작하지 않음

두번째

  • 탭 이동 구문 추가
  • 탭바 인스턴스를 추가해 접근
self.tabBarController?.selectedIndex = 0

let tabBarVC = TabbarController()
tabBarVC.searchTabVC.searchBar.becomeFirstResponder()
  • 결과: 실패 - 탭만 이동되고 becomeFirstResponder() 동작하지 않음

👨‍🏫 오류 원인

  • 탭만 이동시기고 새로운 인스턴스를 만들었을 뿐이라 becomeFirstResponder()가 동작하지 않음

세번째

해결

  • UIViewController에는 기본적으로 tabBarController 라는 프로퍼티 지원
  • tabBarController는 UIViewController의 computed property로,
    해당 VC가 UITabBarController의 childViewController로 들어가 있을 때만 유효
if let tabBarVC = self.tabBarController as? TabbarController {
    tabBarVC.searchTabVC.searchBar.becomeFirstResponder()
}
  • self.tabBarController :
    • 현재 뷰컨트롤러 (SaveBookListViewController)가 어떤 탭바 컨트롤러에 포함되어 있는지를 가져오는 속성
  • as? TabbarController :
    • 위에서 가져온 tabBarController는 일반적인 UITabBarController 타입
    • 우리가 만든 TabbarController로 다운캐스팅해서, searchTabVC 프로퍼티에 접근할 수 있도록 함

문제: 스와이프 해서 데이터 지우기

  • UITableView에서 셀을 왼쪽으로 스와이프해서 삭제하려면, UITableViewDataSource의 메서드인 tableView(_:commit:forRowAt:)를 구현하면 됨.
func tableView(_ tableView: UITableView,
               commit editingStyle: UITableViewCell.EditingStyle,
               forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        // 1. CoreData 삭제
        let target = CoreDataManager.shared.bookEntityData[indexPath.row]
        CoreDataManager.shared.deleteBook(target)

        // 2. 배열 업데이트
        CoreDataManager.shared.bookEntityData.remove(at: indexPath.row)

        // 3. 테이블 뷰 갱신
        tableView.deleteRows(at: [indexPath], with: .automatic)
    }
}
profile
iOS 개발 블로그

0개의 댓글