책 검색 앱 만들기 (4)

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