ProJecr10의 세 번째 포스팅입니다.
저번 포스팅에서 컬리 익스텐션을 만들고, ProductRow
를 설정해서 화면을 구현해봤습니다. 이번에는 리스트를 이용하여 전체 캐릭터 정보 목록을 나타내고, 강 상품의 상세화면으로 들어가 보기까지 해보겠습니다.
1. 테이터 변환
먼저 캐릭터 정보가 들어있는 JSON
을 하나 만들어주도록 하겠습니다. [View
] 템플릿을 선택하고 이름은 ProductData.json
이라고 설정 후 생성해줍니다. 그리고 그 안에 있는 내용은 모두 지운 뒤 이래 코드를 입력해줍니다.
// ProductData.json
[
{
"name": "소스케",
"ImageName": "Sosuke",
"movie": "벼랑위의 포뇨",
"price": 124000,
"description": "5살 남자 아이. 본작품의 주인공. 벼랑 위에 있는 2~3층 정도 되는 주택에서 엄마와 함께 살고 있다.",
"isFavorite": false
},
{
"name": "포뇨",
"ImageName": "Ponyo",
"movie": "벼랑위의 포뇨",
"price": 134000,
"description": "귀여운 물고기. 햄을 굉장히 좋아하며, 자신에게 적대적인 태도를 보이는 사람 에게는 입으로 물총을 쏴서 응수한다.",
"isFavorite": false
},
{
"name": "소피아",
"ImageName": "Sophia",
"movie": "하울의 움직이는 성",
"price": 102000,
"description": "어린 소녀지만 마법 때문에 90살의 늙은 할머니 모습이 되어버렸다. 하지만 삶을 여유롭게 포용할 수 있는 마음의 소유자.",
"isFavorite": false
},
{
"name": "하울",
"ImageName": "Howl",
"movie": "하울의 움직이는 성",
"price": 134000,
"description": "움직이는 성의 주인이자 뛰어난 마법사. 잘생긴 외모로 여성들의 심장을 빼앗아간다는 으스스한 소문의 주인공이기도 하다.",
"isFavorite": false
},
{
"name": "캘시퍼",
"ImageName": "Calcifer",
"movie": "하울의 움직이는 성",
"price": 104000,
"description": "하울이 어렸을 때 하울과 계약을 맺고 하울의 심장을 가진 대신, 하울의 성에 묶여 동력원 겸 관리자가 된다.",
"isFavorite": false
},
{
"name": "황야의 마녀",
"ImageName": "Witch",
"movie": "하울의 움직이는 성",
"price": 124000,
"description": "본래 킹스베리 왕실의 마법사였으나 하울처럼 악마와 계약하고 황야로 쫓겨난 탓에 황야의 마녀라 불린다. 마력으로 본래 나이보다 더 젊게 유지하고 있었다.",
"isFavorite": false
},
{
"name": "치히로",
"ImageName": "Chihiro",
"movie": "센과 치히로의 행방불명",
"price": 134000,
"description": "탐욕과 대비되는 순수'로, 이후에는 특유의 순수함에 용기마저 더해져 자신의 문제를 해결하며 주인공다운 면모를 보이게 된다.",
"isFavorite": false
},
{
"name": "하쿠",
"ImageName": "Haku",
"movie": "센과 치히로의 행방불명",
"price": 124000,
"description": "곤경에 빠진 치히로를 도와주는 수수께끼의 인물. 외관상으로는 12~13살로 추정되지만 실제 나이는 알려지지 않았다.",
"isFavorite": false
},
{
"name": "유바바",
"ImageName": "Yubaba",
"movie": "센과 치히로의 행방불명",
"price": 124000,
"description": "사람이나 요괴 등 여러 존재의 이름을 빼앗아서 지배하는데, 거의 창씨개명 수준으로 이름을 빼앗긴 사람의 본명에 대한 기억까지 없앤다. 외모와 같이 괴팍하고 음험하며, 돈을 엄청 밝히는 성격을 가졌다.",
"isFavorite": false
},
]
JSON을 Foundation으로 변환
JSON
구조의 테이터를 메모리로 읽어와 리스트에 전달할 데이터를 준비해야 합니다. 이것을 사용하기 위해서는 번들의 기능을 확장하는 방법이 필요합니다. [Swift File
] 템플릿으로 BoundleExtension.swift
파일을 만들어 다음과 같은 코드를 작성해줍니다.
// BundleExtension.swift
import Foundation
extension Bundle {
func decode<T: Decodable>(filename: String, as type: T.Type) -> T {
guard let url = self.url(forResource: filename, withExtension: nil) else {
fatalError("번들에 \(filename)이 없습니다.")
}
guard let data = try? Data(contentsOf: url) else {
fatalError("\(url)로부터 데이터를 불러올 수 없습니다.")
}
guard let decodedData = try? JSONDecoder().decode(T.self, from: data) else {
fatalError("데이터 복호화에 실패했습니다.")
}
return decodedData
}
}
이 코드는 파일명을 전달받으면 번들에 있는 파일로 접근해서 JSON
구조의 데이터를 Foundation
프레임워크에서 사용할 수 있는 타입으로 변환시켜주는 기능을 가지고 있습니다. JSONDecoder
의 decode
메서드는 변환 타입이 반드시 Decodable
프로토콜을 준수해야 하기 때문에 Product
에 Decodable
프로토콜을 채택해야 합니다.
Product.swift
에 다음 코드를 추가해줍니다. (JSON
구조의 테이터와 Product
타입의 구조가 반드시 일치해야 합니다.)
import SwiftUI
struct Product {
let name: String
let ImageName: String
...
}
// 1.
extension Product: Decodable {}
let ProductList = [
...
]
Movie 모델 생성
다음은 이 앱에 전반적인 데이터를 관리할 movie
타입을 선언하고, 이 안에 상품 데이터들을 보관해주겠습니다. [Swift File
] 템플릿을 생성 후 Movie.swift
라고 이름 지어 주겠습니다. 그리고 다음과 같은 코드를 추가해줍니다.
// Movie.swift
import Foundation
final class Movie {
var products: [Product]
// MARK: Initialization
//Json 파일명 입력
init(filename: String = "ProductData.json") {
self.products = Bundle.main.decode(filename: filename, as: [Product].self)
}
}
Movie
인스턴스가 생성될 때 파일 이름을 다른 것으로 지정하지 않는다면, BundleExtension
파일에서 작성한 기능을 이용하여 ProductData.json
파일에 있는 데이터를 복호화하여 products
프로퍼티에 저장할 것입니다.
상품 목록 표시
이 데이터를 사용하여 화면에 띄워보도록 하겠습니다. Home.swift
에 movie
프로퍼티를 추가하고 리스트를 이용해 상품을 나열합니다.
// Home.swift
import SwiftUI
struct Home: View {
let movie: Movie
var body: some View {
// 리스트는 identifiable을 대체할 id 값을 반드시 지정해줘야 합니다.
List(movie.products, id: \.name) { product in
ProductRow(product: product)
}
}
}
이렇게만 하고 런을 해보면 오류가 나는데 movie
프로퍼티를 추가해줬으니 Home
을 생성하는 프리뷰와 Scene에도 아래와 같이 변경해줘야 합니다.
// Home.swift
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home(movie: Movie())
}
}
// SwiftUI_Project_10App.swift
import SwiftUI
@main
struct SwiftUI_Project_10App: App {
var body: some Scene {
WindowGroup {
Home(movie: Movie())
}
}
}
실행하기
코드를 모두 올바르게 넣었다면 리스트가 캐릭터 이름을 식별자로 사용해 각 정보를 구분하고 화면에 출력하게 될 것입니다. 결과 화면은 아래와 같습니다.
Identifiable 채택
그런데 매번 Product
타입을 다룰 때마다 id
를 지정해주는 것보다는 Identifiable
프로토콜을 채택하는 것이 좋습니다. Product.swift
파일에서 다음과 같이 코드를 추가해줍니다.
import SwiftUI
struct Product {
// 1.
let id: UUID = UUID() //identifiable 프로토콜 준수를 위한 id 프로퍼티 추가
...
}
extension Product: Decodable {}
// 2.
extension Product: Identifiable {} // 프로토콜 채택
let ProductList = [
...
]
Product
타입에 identifiable
프로토콜을 채택해 주었으니, Home.swift
파일 List
의 id
부분을 지워줘도 됩니다.
struct Home: View {
let movie: Movie
var body: some View {
List(movie.products) { product in
ProductRow(product: product)
}
}
}
2. 내비게이션 링크를 통한 화면 전환
정보 목록을 보여주는 기본 작업이 완료되었으니 이제 NavigationView
를 추가하여 화면을 좀 더 보기 좋게 해 주겠습니다.
NavigationBarTitle
우선 리스트를 NavigationView
로 감싸주고 NavigationBarTitle
수식어를 추가해서 "지브리 스튜디오"라고 명시해주겠습니다.
// Home.swift
import SwiftUI
struct Home: View {
let movie: Movie
var body: some View {
NavigationView { ... }
.navigationBarTitle("지브리 스튜디오")
}
}
}
NavigationLink를 이용한 화면 전환
내비게이션 뷰를 추가해줬으니 이제 내비게이션 링크를 사용해서 화면을 전환할 수 있도록 해주겠습니다. ProductRow
를 눌었을 때 그것과 관련된 화면의 상세페이지를 보여주도록 해주겠습니다. 디테일뷰를 만들어 주기 전에 일단 임시로 Text
로 지정해주겠습니다.
// Home.swift
NavigationView {
List(movie.products, id: \.name) { product in
NavigationLink(
destination: Text("디테일 뷰")) {
ProductRow(product: product)
}
}
.navigationBarTitle("지브리 스튜디오")
}
이렇게 하면 목록 오른쪽에 화살표(">") 모양으로 디스클로저 인디케이터가 나오게 되는데, 이것의 의미는 해당 뷰와 관련된 페이지가 더 존재한다는 것입니다. 이제 이 목록을 눌러 디테일 뷰로 전환되는 것을 확인해 보겠습니다.
디테일뷰 부분은 다음 포스팅에서 구현해보도록 하겠습니다.
읽어주셔서 감사합니다🤟
[프로젝트/간단] - SwiftUI Project10 : 영화 캐릭터 정보 앱 #1
'PROJECT > Simple' 카테고리의 다른 글
SwiftUI Project11 : Shape를 활용하여 뷰 꾸미기 (3) | 2021.04.24 |
---|---|
SwiftUI Project10 : 영화 캐릭터 정보 앱 #4 - 디테일뷰 (3) | 2021.04.20 |
SwiftUI Project10 : 영화 캐릭터 정보 앱 #2 - 모델생성 (0) | 2021.04.17 |
SwiftUI Project10 : 영화 캐릭터 정보 앱 #1 (0) | 2021.04.16 |
SwiftUI Project9 : CustomTabView (geometryReader) (0) | 2021.03.26 |