BookStore (1)
UI 구성
이렇게 구성을 한다.
좌측 부터 1, 3, 2 으로 하여 UI 구성을 해본다.
1. MainView 구성
첫번째 화면이고 SearchBar, Grid, list가 UIComponent의 Point이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
NavigationStack {
VStack {
VStack(alignment: .leading) {
Text("최근 본 책")
.font(.system(size: 25))
.fontWeight(.bold)
HStack {
ScrollView {
// LazyHGrid(rows: [GridItem]) {
//
// }
}
}
.frame(height: UIScreen.main.bounds.width * 0.35)
}
.padding(.horizontal, 20)
VStack(alignment: .leading) {
Text("검색 결과")
.font(.system(size: 25))
.fontWeight(.bold)
List {
}
}
.padding(.horizontal, 20)
}
}
우선은 이렇게 구상을 한다.
실행화면은 다음과 같다.
이때 Searchable의 경우 그냥 쓰게되면 보이지 않았다.
그래서 NavigationStack을 사용하였다.
1. Int Extension
1
2
3
4
5
6
7
8
extension Int {
func toString() -> String {
let formatter = NumberFormatter()
formatter.numberStyle = .currency
formatter.locale = Locale(identifier: "ko_KR")
return formatter.string(from: self as NSNumber) ?? String(describing: self)
}
}
이렇게 가격에 ,가 자동으로 붙고 앞에 ₩가 붙는다.
2. APIRequest 구현
1. Modeling
KAKAO_Docs를 참고 하여 모델링을 한다 아래 사진은 모델링 부분에 대한 내용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
struct BookModel: Codable {
let meta: Meta
let documents: [Document]
}
// MARK: - Document
struct Document: Codable {
let authors: [String]
let contents: String
let price: Int
let publisher: String
let status: String
let thumbnail: String
let title: String
let url: String
enum CodingKeys: String, CodingKey {
case authors, contents, price, publisher
case status, thumbnail, title, url
}
}
// MARK: - Meta
struct Meta: Codable {
let isEnd: Bool
let pageableCount, totalCount: Int
enum CodingKeys: String, CodingKey {
case isEnd = "is_end"
case pageableCount = "pageable_count"
case totalCount = "total_count"
}
}
2. APIService
특이점이라면 이번엔 Header / Query가 필요하다. 이건 예전에 했던것 처럼 Header와 Query가 필요한 파트라 오래간만에 해서 가물가물하여 이전에 작성한 글을 참고하였다. 그리면서 Generic을 추가해주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class APIRequestService {
enum NetworkError: Error {
case badUrl
case invalidRequest
case badResponse
case badStatus
case failedToDecodeResponse
}
func requestAPI<T: Codable> (searchText: String) async -> T? {
let urlString = "https://dapi.kakao.com/v3/search/book?target=title"
let headers = ["Authorization" : "KakaoAK \(Secret().apiKey)"]
var urlComponent = URLComponents(string: urlString)
urlComponent?.queryItems?.append(URLQueryItem(name: "query", value: searchText))
guard let url = urlComponent?.url else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = headers
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
throw NetworkError.badResponse
}
guard response.statusCode >= 200 && response.statusCode < 300 else {
throw NetworkError.badStatus
}
guard let decodedData = try? JSONDecoder().decode(T.self, from: data) else {
throw NetworkError.failedToDecodeResponse
}
return decodedData
} catch NetworkError.badUrl {
print("There was an error creating the URL")
} catch NetworkError.badResponse {
print("Did not get a valid response")
} catch NetworkError.badStatus {
print("Did not get a 2xx status code from the response")
} catch NetworkError.failedToDecodeResponse {
print("Failed to decode response into the given type")
} catch {
print("An error occured downloading the data")
}
return nil
}
}
3. ViewModel
1
2
3
4
5
6
7
8
9
10
@MainActor
class APIViewModel: ObservableObject {
@Published var books: [BookModel] = []
func request(searchText: String) async {
guard let requestedData: BookModel = await APIRequestService().requestAPI(searchText: searchText) else { return }
print(requestedData)
books = [requestedData]
}
}
이렇게 작성을 했다.
print를 넣은건 제대로 되는지 확인하기 위함.
수정.
1
2
3
4
5
6
7
8
9
10
@MainActor
class APIViewModel: ObservableObject {
@Published var books: [Document] = []
func request(searchText: String) async {
guard let requestedData: BookModel = await APIRequestService().requestAPI(searchText: searchText) else { return }
print(requestedData.documents)
books = requestedData.documents
}
}
모델 전체를 가져오는건 나중에할 무한스크롤에서 필요한기능이고 List에 필요한건 Document라서 이렇게 바꿔준다.
4. Test
MainView에 onAppear
Modifier를 통하여 호출을 해본다.
1
2
3
4
5
6
.onAppear {
Task {
await apiViewModel.request(searchText: "미움받을 용기")
}
}
이렇게 하여 작동이 되는지 확인을 해본다.
1
2
BookModel(meta: BookStore.Meta(isEnd: false, pageableCount: 22, totalCount: 22), documents: [BookStore.Document(authors: ["기시미 이치로", "고가 후미타케"], contents: "부정하며, 자유도 행복도 모두 ‘용기’의 문제일 뿐 환경이나 능력의 문제가 아님을 보여준 알프레드 아들러(Alfred Adler)다.
생략...
작동이 잘되는것 확인
5. 문제점
하지만 여기 문제점이 있는데 List는 Identifiable 프로토콜을 따른다. 현재 모델링에는 해당 프로토콜이 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Document: Codable, Identifiable {
let id = UUID() // new
let authors: [String]
let contents: String
let price: Int
let publisher: String
let status: String
let thumbnail: String
let title: String
let url: String
enum CodingKeys: String, CodingKey {
case authors, contents, price, publisher
case status, thumbnail, title, url
}
}
이때 주의할점은 id: UUID 를 하게되면 Decoding 에러가 난다.
해당 부분은 api 결과에 id가 있다고 판단하기 때문.
그래서 반드시 let id = UUID()를 해줘야 한다.
6. 호출하기
처음에 searchbar에 대한 내용을 어떻게 적용을 해야할지 생각이 나질 않았다.
그래서 검색을 해보다가 YouTube를 참고했다.
1
2
3
4
5
6
7
8
9
.onChange(of: searchText) { _, _ in
Task {
if !searchText.isEmpty && searchText.count > 2 {
await apiViewModel.request(searchText: searchText)
} else {
apiViewModel.books.removeAll()
}
}
}
검색해보니 onSubmit은 엔터를 치거나 돋보기 버튼을 눌렀을때 작동한다.
1
2
3
4
5
.onSubmit(of: .search) {
Task {
await apiViewModel.request(searchText: searchText)
}
}
3. ListCell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@State var title: String = "title"
@State var author: String = "author"
@State var price: Int = 0
var body: some View {
HStack {
Text(title)
.font(.system(size: 13))
.lineLimit(0)
Spacer()
Text(author)
.font(.system(size: 8))
Spacer()
Text(price.toString())
.font(.system(size: 10))
}
}
설명은 패스.
4. 실행결과
우선 이렇게 했다.
보완할 점이라면, x를 눌렀을때 검색결과를 날리는것? 그정도가 될듯하다.
내일은 아마도 NavigatoinLink 사용하여 클릭했을때 상세페이지 나오기,
SwiftData 사용하여 담기 기능을 해보지 않을까 싶다.