2024.04.30 - [◽️ Programming/T I L] - 책 검색 앱 만들기(1)
2024.05.01 - [◽️ Programming/T I L] - 책 검색 앱 만들기 (2)
2024.05.03 - [◽️ Programming/T I L] - 책 검색 앱 만들기 (3)
책 검색 상세 페이지 구현 (DetailViewController)
데이터 전달
이제 검색 한 책을 상세페이지로 데이터를 전달 할 수 있도록 구현하자!
컬렉션 뷰 메서드 중 “didSelectItemAt” 를 사용해 셀이 선택되었을때 이벤트가 발생 하도록 구현할 예정이다. 해당 메서드 안에 데이터를 전달하며 화면 전환까지 같이 진행될 수 있도록 코드를 구현한다.
let detailVC = DetailViewController()
let bookData = bookData?.documents[indexPath.item]
// 전체 데이터 전달
detailVC.bookData = bookData
detailVC.mainTitle.text = bookData?.title
detailVC.bookContents.text = bookData?.contents
detailVC.subTitle.text = bookData?.authorsToString()
// 천원 단위에 , 붙을 수 있도록 구현
let bookPrice = bookData?.price ?? 0
let formattedPrice = NumberFormatter.localizedString(from: NSNumber(value: bookPrice), number: .decimal)
detailVC.bookPrice.text = "\\(formattedPrice)원"
// URL 이미지를 변환하여 전달
if let imageURL = bookData?.thumbnail, let imageURL = URL(string: imageURL) {
detailVC.bookImageView.kf.setImage(with: imageURL)
}
// 상세 페이지로 화면 전환
present(detailVC, animated: true, completion: nil)
이렇게 데이터를 전달하고 화면을 전환하면 완성이다 🙂
이제 이 데이터를 들어갈 컴포넌트를 구성하고 연결만 해주면 된다!
부분 ScrollView 구현
이제 상세 페이지를 구현 해보자 🙂
상세페이지에는 scrollView가 들어갈 예정인데 이전 팀 프로젝트 영화 예매 앱 만들기에서 전체 스크롤 뷰 만들기에 성공 했으니 이번에는 처음으로 부분적인 scrollView가 들어가도록 코드로 구현해 볼 예정이다!
코드로 컴포넌트 넣는 부분은 이제 많이 나왔으니 빼고 스크롤뷰를 넣는 곳 부터 살펴보자
let scrollView = UIScrollView()
let contentView = UIView()
let bookContents: UILabel = {
var label = UILabel()
label.font = UIFont.systemFont(ofSize: 25)
label.numberOfLines = 0
return label
}()
ScrollView 안에 UIView를 넣어 안의 컴포넌트들을 보다 쉽게 관리 할 수 있도록 설정할 예정이다.
그리고 이 UIView 안에 책의 소개 글이 들어갈 예정이므로 해당하는 레이블을 만들어 넣어주자!
view.addSubview(scrollView)
scrollView.addSubview(contentView)
contentView.addSubview(bookContents)
각각의 계층에 맞게 addSubview를 설정해주어야 한다. scrollView 위에 올라와야하고 scrollView 위에 contentView가 올라오고 그 위에 bookContents가 보여지도록 설정하면 된다 🙂
스크롤 뷰 구성에서 가장 중요한 부분은 역시 오토레이아웃의 올바른 설정..
scrollView.snp.makeConstraints {
$0.top.equalTo(bookPrice.snp.bottom).offset(30)
$0.leading.trailing.equalToSuperview()
$0.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom)
}
contentView.snp.makeConstraints {
$0.top.leading.trailing.bottom.equalTo(scrollView)
$0.width.equalTo(scrollView)
}
bookContents.snp.makeConstraints {
$0.top.bottom.equalTo(contentView)
$0.leading.trailing.equalTo(contentView).inset(20)
}
scrollView는 컴포넌트 밑에 위치하도록 top을 잡아주고 나머지는 화면을 가득 채우게 구성한다.
그 다음 contentView는 모든 면이 scrollView와 맞추고 스크롤의 방향을 세로로 할 것이니 width 값을 스크롤뷰와 동일하게 설정한다.
contentView 안에 책 줄거리 내용이 들어오도록 레이아웃을 잡아주면 부분 스크롤 뷰 성공!
Floting Button 구현
이제 뒤로가기 , 책 담기 등 여러 이벤트를 수행하고 스크롤이 되어도 그 자리에 고정되어 있는 플로팅 버튼을 구현해보자!
이전 글에 해당 내용을 소개하는 글을 남겼으니 한번 더 참고하면 좋을 듯 하다!
let actionButton = JJFloatingActionButton()
func addFloatingButton() {
actionButton.addItem(title: "책담기", image: UIImage(systemName: "book.fill")?.withRenderingMode(.alwaysTemplate)) { item in
self.saveBookToCoreData()
}
actionButton.addItem(title: "검색하기", image: UIImage(systemName: "magnifyingglass")?.withRenderingMode(.alwaysTemplate)) { item in
// do something
}
actionButton.addItem(title: "뒤로가기", image: UIImage(systemName: "return")?.withRenderingMode(.alwaysTemplate)) { item in
self.dismiss(animated: true, completion: nil)
}
actionButton.display(inViewController: self)
}
SPM 을 통해 JJFloatingActionButton을 넣어주고 이렇게 변수에 넣어 메서드를 구현하고 해당 내용을 선언만 해주면 간단하게 구현이 완료된다 🙂
코어 데이터 연결
이제 상세 페이지에서 확인되는 책의 데이터를 코어데이터에 저장하는 과정을 살펴보자! 오랜만에 코어데이터를 적용하는거라 조금 막힌 부분이 있어서 정리하고 넘어가고자 한다 🙂
먼저 코어데이터를 설정하고 entity를 만드는 과정은 이전에 자세하게 적어둔 블로그가 있으니 이번에는 살짝 스킵하고 넘어가자 바로 코어데이터 매니저를 구현하자!
class CoreDataManager {
// 싱글톤으로 구현
static let shared = CoreDataManager()
private init() {}
let appDelegate = UIApplication.shared.delegate as? AppDelegate
lazy var context = appDelegate?.persistentContainer.viewContext
// entity 명칭 인스턴스 화
let coreDataName: String = "BookCoreData"
// 코어 데이터 GET
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
}
// 코어 데이터 SAVE
func saveBookListData(_ booklist: Document, completion: @escaping () -> Void) {
guard let context = context else {
print("context를 가져올 수 없습니다.")
return
}
if let entity = NSEntityDescription.entity(forEntityName: coreDataName, in: context) {
let newProduct = NSManagedObject(entity: entity, insertInto: context)
let authorsString = booklist.authors.joined(separator: ", ")
newProduct.setValue(authorsString, forKey: "authors")
newProduct.setValue(booklist.title, forKey: "title")
newProduct.setValue(booklist.price, forKey: "price")
do {
try context.save()
print("코어데이터에 저장되었습니다.")
completion()
} catch {
print("코어데이터에 저장하는데 실패했습니다.", error)
completion()
}
}
}
// 코어 데이터 DELETE
func deleteBookList(_ bookList: Document, completion: @escaping () -> Void) {
guard let context = context else {
print("content를 가져올 수 없습니다.")
return
}
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = NSFetchRequest(entityName: coreDataName)
fetchRequest.predicate = NSPredicate(format: "title == %@", bookList.title)
do {
if let result = try context.fetch(fetchRequest) as? [NSManagedObject] {
for object in result {
context.delete(object)
}
try context.save()
print("삭제가 완료되었습니다.")
completion()
}
} catch {
print("삭제가 실패했습니다.", error)
completion()
}
}
}
코어 데이터를 CRUD 하기 위해 각각의 해당하는 메서드를 구현한다! 이번 프로젝트에서는 저장, 읽기, 삭제 이 세가지 만 사용될 것 같아 3개의 메서드로만 구성했다🙂
이제 이 코어데이터 매니저를 바탕으로 코어데이터를 저장하자
var bookData: Document?
DetailViewController에 이렇게 데이터를 가져왔다고 생각했는데 자꾸 nil이 되어 코어 데이터가 저장되지 않는 수렁에 빠져있었다.. 튜터님께 도움을 요청한 결과 현재는 텍스트는 전달해서 화면에 보이긴 하지만 데이터를 전달하지 않은 상태인 것 같다고 하셨다..
컬렉션 뷰 셀 didSelectItemAt 으로 넘어와서
detailVC.bookData = bookData
bookData 값에 데이터를 넣어주니 정상적으로 데이터가 넘어와 저장될 수 있었다.. ㅠㅠ 감사합니다..
func saveBookToCoreData() {
guard let bookData = bookData else {
print("bookData가 없습니다.")
return
}
CoreDataManager.shared.saveBookListData(bookData) {
print("코어데이터에 저장되었습니다.")
}
}
func addFloatingButton() {
actionButton.addItem(title: "책담기", image: UIImage(systemName: "book.fill")?.withRenderingMode(.alwaysTemplate)) { item in
self.saveBookToCoreData()
}
}
이제 코어 데이터를 저장하는 메서드를 호출해 선언하면 책 담기 버튼을 누르면 코어 데이터 저장이 완료 된다 🙂
이렇게 상세페이지 구현을 마치고 이제는 마이페이지에 코어데이터를 불러올 예정이다.
배열 형태의 API 데이터 String으로 변환 하는 메서드 추가
코어데이터를 저장하면서 한가지 오류에 더 막히는 부분이 있었다. 바로바로 API 를 통해 받아오는 작가의 데이터가 일반적인 String이 아닌 배열 형태의 [String] 이었기 때문에 생기는 오류였다!
이 부분을 해결하기 위해 데이터 모델링과 코어 데이터 부분을 살짝 손봤다.
struct Document: Codable {
let authors: [String]
let contents, datetime, isbn: String
let price: Int
let publisher: String
let salePrice: Int
let status: String
let thumbnail: String
let title: String
let translators: [String]
let url: String
enum CodingKeys: String, CodingKey {
case authors, contents, datetime, isbn, price, publisher
case salePrice = "sale_price"
case status, thumbnail, title, translators, url
}
// 배열 -> String 변환
func authorsToString() -> String {
return authors.joined(separator: ", ")
}
}
이렇게 배열에서 스트링으로 변환하는 메서드를 만들어 데이터 모델링에 넣어두면 쉽게 어디서든 호출 시 해당 메서드로 호출이 가능하다!
이 내용을 코어 데이터 매니저에서
let newProduct = NSManagedObject(entity: entity, insertInto: context)
let authorsString = booklist.authors.joined(separator: ", ")
newProduct.setValue(authorsString, forKey: "authors")
newProduct.setValue(booklist.title, forKey: "title")
newProduct.setValue(booklist.price, forKey: "price")
이렇게 변형 해 String을 저장할 수 있도록 설정하고
DetailViewController로 데이터를 전달할때
detailVC.mainTitle.text = bookData?.title
detailVC.bookContents.text = bookData?.contents
detailVC.subTitle.text = bookData?.authorsToString()
이런식으로 편하게 사용이 가능하다 🙂
여기까지 이제 연휴동안 구현한 내용이고
오늘은 이제 마이페이지를 구현하고 최근 본 책을 구현하면 과제는 어느정도 마무리 될 것 같다!
'◽️ Programming > T I L' 카테고리의 다른 글
책 검색 앱 만들기 (6) (2) | 2024.05.09 |
---|---|
책 검색 앱 만들기 (5) (0) | 2024.05.08 |
책 검색 앱 만들기 (3) (0) | 2024.05.03 |
책 검색 앱 만들기 (2) (3) | 2024.05.01 |
책 검색 앱 만들기 (1) (3) | 2024.04.30 |