궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
PROJECT/Simple

SwiftUI Project10 : 영화 캐릭터 정보 앱 #3 - JSON

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

 

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프레임워크에서 사용할 수 있는 타입으로 변환시켜주는 기능을 가지고 있습니다. JSONDecoderdecode메서드는 변환 타입이 반드시 Decodable프로토콜을 준수해야 하기 때문에 ProductDecodable프로토콜을 채택해야 합니다.

 

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.swiftmovie프로퍼티를 추가하고 리스트를 이용해 상품을 나열합니다.

// 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 파일 Listid부분을 지워줘도 됩니다.

struct Home: View {
    let movie: Movie
    
    var body: some View {
        List(movie.products) { product in
            ProductRow(product: product)
            
        }
    }
}
GitHub 현재 까지의 코드 확인

6. JSON을 이용한 리스트 상품 목록 표시

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("지브리 스튜디오")
}

이렇게 하면 목록 오른쪽에 화살표(">") 모양으로 디스클로저 인디케이터가 나오게 되는데, 이것의 의미는 해당 뷰와 관련된 페이지가 더 존재한다는 것입니다. 이제 이 목록을 눌러 디테일 뷰로 전환되는 것을 확인해 보겠습니다.

 

디테일뷰 부분은 다음 포스팅에서 구현해보도록 하겠습니다.

 

읽어주셔서 감사합니다🤟

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.