📚 BookSearchApp
Kakao API 를 이용하여 책을 검색하고, 정보를 받아온 뒤 선택한 책을 CoreData를 사용하여 내부저장소에 저장하는 앱을 만들어보자!
ViewController 2개와 Modal을 사용하는 ViewController 1개까지 총 3개의 화면으로 구성되어 있다.
KAKAO REST API를 사용하여 SearchBar에 사용자가 검색한 내용이 있는 책을 받아오고,
그 중 하나를 선택하면 Modal을 사용하여 화면에 선택한 책의 정보를 보여준다.
그리고 담기 버튼을 누르면 CoreData를 사용하여 휴대폰 내부 저장소에 저장하고 담은 책 리스트 탭에서 저장된 책들을 보여준다.
✓ 현재까지 작업 내용
1. ViewController UI 작업
2. Kakao REST API 사용하여 책 검색 후 리스트로 보여주기
// AlamoFire를 이용한 기본 통신 구조
private func fetchData<T: Decodable>(
url: URL,
headers: HTTPHeaders,
completion: @escaping (Result<T, AFError>) -> Void
) {
AF.request(url, headers: headers)
.validate()
.responseDecodable(of: T.self) { response in
completion(response.result)
}
}
// kakao REST API 사용하여 데이터 통신 함수
func fetchBooksFromKakaoAPI() {
// xcconfig에 숨긴 API키를 사용할 수 있게 가져오는 코드
guard let filePath = Bundle.main.path(forResource: "Info", ofType: "plist") else {
return
}
let plist = NSDictionary(contentsOfFile: filePath)
guard let apiKey = plist?.object(forKey: "KakaoApiKey") as? String else {
return
}
// 사용자가 searchBar에 입력한 text를 쿼리로 추가
let query = "\(searchBar.text ?? "")"
let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!
let urlString = "https://dapi.kakao.com/v3/search/book?query=\(encodedQuery)"
guard let url = URL(string: urlString) else { return }
// KAKAO REST API 사용시 Header를 추가해줘야 함
let headers: HTTPHeaders = [
"Authorization": "KakaoAK \(apiKey)"
]
// AlamoFire 이용
fetchData(url: url, headers: headers) { (result: Result<BookResponse, AFError>) in
switch result {
case .success(let response):
print("도서 수: \(response.documents.count)")
//bookData에 받아온 데이터 저장
self.bookData = response.documents
// 메인 쓰레드에서 콜렉션뷰 reload
DispatchQueue.main.async {
self.collectionView.reloadData()
}
case .failure(let error):
print("에러 발생: \(error.localizedDescription)")
}
}
}
3. 책을 하나 선택하면 Modal-Present를 사용하여 화면에 정보 표시
// cell 클릭 이벤트
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.section == 1 {
let selectedBook = bookData[indexPath.item]
let modalVC = ModalViewController(bookData: selectedBook)
modalVC.modalPresentationStyle = .automatic
present(modalVC, animated: true, completion: nil)
}
}
4. gitignore 작성 (API키 숨기기)
48. 스파르타 코딩 클럽 - xcconfig 파일을 사용하여 Github로 부터 API 키 감추기
xcconfig 파일을 통해 Github ignore 설정하기[ 1. xcconfig 파일 생성 ] [ New File from Templete... 클릭 ] → [Other 부분에 Configuration Settings File 클릭 ] [ 2. API 키 작성하기 ]만든 xcconfig 파일에 사용하는 API 키를
coding-pill.tistory.com
이 부분은 내 블로그의 글을 참고
5. Modal ViewController에 ScrollView 추가
// scrollView를 하나 생성하고, 그 안에 들어갈 요소들을 담을 innerView를 생성
private let scrollView = UIScrollView()
private let innerView = UIView()
// view에 scrollView 추가
view.addSubview(scrollView)
// scrollView에 innerView 추가
scrollView.addSubview(innerView)
// innerView안에 들어갈 요소들 추가
[titleLabel, authorLabel, imageView, priceLabel, contentsLabel].forEach {
innerView.addSubview($0)
}
// 버튼들은 스크롤 영역에 관계없이 사용하기 위해서 플로팅 형식으로 view에 추가
[cancelButton, cartButton].forEach {
view.addSubview($0)
}
6. CartViewController에 CoreData 불러와서 Data 표시하기
// 싱글톤 패턴의 coredata를 사용하기 위한 코드 작성
var coredata = CoreDataManager.shared
var container: NSPersistentContainer!
// view가 willAppear 될 때 coreData에서 저장된 bookData를 불러오고 tableView에 표시
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
readData()
tableView.reloadData()
}
private func readData() {
// bookData를 빈 배열로 초기화
bookData = []
let book = coredata.readAllData()
// 불러온 책 정보들을 bookData 배열에 append
for data in book {
let price = data.price
guard let title = data.title,let author = data.author, let thumbnail = data.thumbnail, let contents = data.contents, let isbn = data.isbn else { return }
bookData.append(Book(title: title, contents: contents, authors: author.components(separatedBy: ", "), price: Int(price), thumbnail: thumbnail, isbn: isbn))
}
}
7. CartViewController에서 전체 삭제 버튼을 클릭시 CoreData 전체 삭제 및 tableView.reloadData()로 갱신
// 전체 삭제 버튼
@objc private func deleteAllButtonTapped() {
print("전체 삭제 버튼 클릭")
// coredata에서 데이터 전체 삭제
coredata.deleteAllData()
// 테이블 뷰의 data로 사용되는 bookData 배열을 빈 배열로 초기화
bookData.removeAll()
// 삭제 후 테이블 뷰 reload
tableView.reloadData()
}
// CoreDataManager.deleteAllData()
// CoreData 저장된 데이터 전체 삭제
func deleteAllData() {
let fetchRequest = BookData.fetchRequest()
do {
let result = try self.container.viewContext.fetch(fetchRequest)
for data in result as [NSManagedObject] {
self.container.viewContext.delete(data)
print("삭제된 데이터: \(data)")
}
try self.container.viewContext.save()
print("모든 데이터 삭제 완료")
} catch {
print("모든 데이터 삭제 실패: \(error)")
}
}
8. 스와이프 방식을 통하여 담은 책 개별삭제 구현 및 추가 버튼 클릭 시 검색 탭으로 이동
// 왼쪽으로 스와이프 했을 때 데이터 삭제
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "삭제") { [weak self] (_, _, completionHandler) in
guard let self = self else { return }
let book = self.bookData[indexPath.item]
self.coredata.deleteData(isbn: book.isbn)
self.bookData.remove(at: indexPath.item)
tableView.deleteSections(IndexSet(integer: indexPath.section), with: .automatic)
completionHandler(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
// 추가 버튼
@objc private func addButtonTapped() {
print("추가 버튼 클릭")
// 첫번째 탭으로 이동 하기
self.tabBarController?.selectedIndex = 0
}
🎯 Trouble Shooting
[ 1. 전체 삭제를 하고 tableView.reloadData()를 수행했지만 콜렉션 뷰가 새로고침 되지 않음 ]
// 전체 삭제 버튼
@objc private func deleteAllButtonTapped() {
coredata.deleteAllData()
bookData.removeAll()
tableView.reloadData()
}
이유 : coredata 에서는 데이터가 삭제 완료 되었지만
tableView는 UITableViewDataSource의 데이터를 기반으로 그리므로,
UITableViewDataSource의 데이터로 사용중인 bookData의 배열을 비우거나 coreData에서 새로 불러오지 않으면
화면에는 이전 데이터가 보이게 됨
// 전체 삭제 버튼
@objc private func deleteAllButtonTapped() {
coredata.deleteAllData()
bookData.removeAll() // 해결 방법
tableView.reloadData()
}
해결 방법 : bookData를 removeAll() 메서드를 이용해서 빈 배열로 비워줌
[ 2. CollectionView에서 trailingSwipeActionsConfigurationForRowAt 메서드의 부재 ]
나는 담은 책 리스트 탭 화면에서 검색탭에서 만든 CollectionView Compositional layout을 재사용하여 화면을 구현했었다.
그러나 Step3-2의 요구사항 중 스와이프 등의 방식을 통하여 담은 책 개별 삭제를 구현하기 위해 방법을 찾아보다가
trailingSwipeActionsConfigurationForRowAt 라는 메서드를 사용해서 Swipe Action을 구현할 수 있는 방법을 찾게되었다.
이유 : trailingSwipeActionsConfigurationForRowAt 메서드는 CollectionView에는 없고 TableView에 있는 메서드이다.
해결 방법 : 담은 책 리스트 탭을 TableView로 새로 구현하고, TableView의 trailingSwipeActionsConfigurationForRowAt 메서드를 이용해서 왼쪽으로 스와이프 했을 때 해당 책이 삭제되게 구현하였다.
// 왼쪽으로 스와이프
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let deleteAction = UIContextualAction(style: .destructive, title: "삭제") { [weak self] (_, _, completionHandler) in
guard let self = self else { return }
let book = self.bookData[indexPath.item]
self.coredata.deleteData(isbn: book.isbn)
self.bookData.remove(at: indexPath.item)
tableView.deleteSections(IndexSet(integer: indexPath.section), with: .automatic)
completionHandler(true)
}
return UISwipeActionsConfiguration(actions: [deleteAction])
}
'스파르타 코딩 클럽 - iOS 스타터 6기 > 본 캠프' 카테고리의 다른 글
51. 스파르타 코딩 클럽 - 봄여어름갈겨어울 #1 (날씨앱) (0) | 2025.05.20 |
---|---|
50. 스파르타 코딩 클럽 - BookSearchApp #2 (0) | 2025.05.16 |
48. 스파르타 코딩 클럽 - xcconfig 파일을 사용하여 Github로 부터 API 키 감추기 (0) | 2025.05.14 |
47. 스파르타 코딩 클럽 - UITabBarController (0) | 2025.05.12 |
46. 스파르타 코딩 클럽 - RxSwift (1) | 2025.05.09 |