포스트

TourApp (4)

json 적용하기

우선 json 양식으로 파일을 바꿔준다.

이때 이전과 특이점이라면 json 로드가 from server가 아닌 from local이라는것.

모델링

모델링을 해준다.

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
struct JsonModel: Codable {
    let tours: [Tour]
}

// MARK: - Tour
struct Tour: Codable {
    let title: String
    let imageURL: String
    let description, address: String
    let latitude, longitude: Double
    let resList: [ResList]

    enum CodingKeys: String, CodingKey {
        case title
        case imageURL = "imageUrl"
        case description, address, latitude, longitude, resList
    }
}

// MARK: - ResList
struct ResList: Codable {
    let imageURL: String
    let shopTitle: String
    let shopURL: String

    enum CodingKeys: String, CodingKey {
        case imageURL = "imageUrl"
        case shopTitle
        case shopURL = "shopUrl"
    }
}

사실 모델링은 사이트를 통해서 하는게 제일 빠르긴 하다.

json load 함수 만들기.

여러 자료를 찾아보았는데 UrlSession을 사용하는 경우와 그렇지 않은 경우 두가지가 있었는데

urlsession을 사용한 자료는 4년 전이고 근래 자료들은 그냥 로드를 하는듯 하다.

기본뼈대는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
class loadJsonModel: ObservableObject {
    @Published var json = [JsonModel]()
    
    init() {
        load()
    }
    
    func load() {
        
    }
}
Without Urlsession

Youtube 를 참고하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class loadJsonModel: ObservableObject {
    @Published var tours = [JsonModel]()
    
    init() {
        load()
    }
    
    func load() {
        guard let url = Bundle.main.url(forResource: "data", withExtension: "json")
        else {
            print("Json file not found")
            return
        }
      
        let data = (try? Data(contentsOf: url))!
        let tours = try? JSONDecoder().decode([JsonModel].self, from: data)
        
        self.tours = tours!
    }
}

이후 listview를 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct ListView: View {
    
    @ObservedObject var json = loadJsonModel()
    
    var body: some View {
        NavigationStack {
            Text("관광 고고")
                List {
                    ForEach(json.tours, id: \.self) { tour in
                        NavigationLink(value: tour) {
                            CellView(title: tour.title, imageUrl: tour.imageUrl)
                        }
                    }
                }
            .navigationDestination(for: TourModel.self) { model in
                DetailView(title: model.title, imageUrl: model.imageUrl, description: model.description, address: model.address, coordinate: CLLocationCoordinate2D(latitude: model.latitude, longitude: model.longitude), shopList: model.resList, cameraPosition: .camera(MapCamera(centerCoordinate: CLLocationCoordinate2D(latitude: model.latitude, longitude: model.longitude), distance: 1000, heading: 90, pitch: 80)))
            }
        }
        
    }
}

하지만 에러가 발생

1
Cannot convert value of type '[JsonModel]' to expected argument type 'Binding<C>'

json에서 잘못된걸 발견

1
2
3
4
5
@Published var tours = [Tour]() // 수정

let decodedData = try? JSONDecoder().decode(JsonModel.self, from: data) // 수정
        
self.tours = decodedData!.tours // 수정

그랬더니

1
Generic struct 'ForEach' requires that 'loadJsonModel' conform to 'RandomAccessCollection'

에러발생

수정을 하던 도중 hashable 프로토콜이 필요하다고 에러가 발생.

현재는 id가 없다.

그래서 UUID를 사용하여 id를 부여한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Tour: Codable, Hashable {

    let id = UUID()
    let title: String
    let imageURL: String
    let description, address: String
    let latitude, longitude: Double
    let resList: [ResList]
    
    enum CodingKeys: String, CodingKey {
        case title
        case imageURL = "imageUrl"
        case description, address, latitude, longitude, resList
    }
}

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
struct PageView: View {
    
    var lists = [ResList]()
    
    @State private var currentPage: Int = 0
    
    var body: some View {
        VStack(spacing: 20) {
            TabView(selection: $currentPage.animation()) {
                ForEach(0..<lists.count, id: \.self) { index in
                        VStack {
                            AsyncImage(url: URL(string: lists[index].imageURL)) { image in
                                image
                                    .resizable()
                                    .frame(maxWidth: 150, maxHeight: 150)
                            } placeholder: {
                                Image(systemName: "photo")
                            }
                            Link(destination: URL(string: lists[index].shopUrl)!) { Text(lists[index].shopTitle)
                                    .fontWeight(.bold)
                                    .foregroundStyle(.blue)
                        }
                    }
                }
            }

바로 ForEach 부분에서 Error가 발생

미완인채로 같이 공부를 하던 분과 회고를 하던 도중 비슷한 에러를 겪으셨다고 하여, @State var lists = [ResList]() 로 바인딩을 가능하게 하여 적용을 해보려 한다.

그렇게 하니 이번엔

AsyncImage(url: URL(string: list.imageURL)) { image in 여기서 에러가 발생

1
Cannot convert value of type 'Binding<String>' to expected argument type 'String'

bindingString이라 생기는 문제인듯하다.

AsyncImage(url: URL(string: list.wrappedValue.imageURL))

bindingString을 string을 사용할때는 wrappedValue를 사용한다.

📘 Swift의 wrappedValue 이해하기

📝 정의

  • wrappedValueSwift의 프로퍼티 래퍼(Property Wrapper)에서 원래 값을 접근할 때 사용하는 속성이다.
  • @State, @Binding, @Published 등의 프로퍼티 래퍼는 내부 값을 감싸고 있으며, 이 값을 읽거나 수정할 때 wrappedValue를 사용한다.

🔍 주요 특징

  • 프로퍼티 래퍼의 실제 값을 반환하거나 수정할 수 있다.
  • SwiftUI에서는 $를 사용하여 wrappedValue에 간단하게 접근할 수 있다. 예를 들어, $countcount.wrappedValue와 동일하다.
  • 주로 SwiftUI에서 자동으로 처리되지만, 명시적으로 값을 가져오고 싶을 때 wrappedValue를 사용할 수 있다.

💡 사용 예시

1️⃣ 기본 사용 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import SwiftUI

struct ContentView: View {
    @State private var count: Int = 0

    var body: some View {
        VStack {
            Text("Count: \(count)") // 원래 값 사용
            Text("Wrapped Count: \($count.wrappedValue)") // wrappedValue로 접근

            Button("Increment") {
                $count.wrappedValue += 1 // wrappedValue를 사용하여 값 수정
            }
        }
    }
}

설명:

  • @State는 SwiftUI에서 값의 변화를 추적하고 뷰를 업데이트하는 데 사용된다.
  • $count는 count의 Binding을 나타내며, .wrappedValue를 통해 원래 값에 접근할 수 있다.
2️⃣ @Binding과 wrappedValue 예시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import SwiftUI

struct ParentView: View {
    @State private var name: String = "Swift"

    var body: some View {
        ChildView(text: $name)
    }
}

struct ChildView: View {
    @Binding var text: String

    var body: some View {
        VStack {
            Text("Wrapped Value: \(text.wrappedValue)") // Binding의 wrappedValue 사용
            TextField("Enter Name", text: $text)
        }
    }
}

설명:

  • ParentView에서 $name을 전달하면 ChildView에서 @Binding을 통해 값을 받는다.
  • text.wrappedValue는 @Binding의 원래 값을 가져온다.

✅ 결론

  • wrappedValue는 프로퍼티 래퍼가 감싸고 있는 원래 값을 반환하거나 수정하는 데 사용된다.
  • SwiftUI에서는 $를 통해 wrappedValue에 쉽게 접근할 수 있다.
  • 주로 SwiftUI에서 자동으로 처리되지만, 명시적으로 접근할 때 wrappedValue를 사용할 수 있다.

하지만 문제가 발생

그래서 혹시나해서

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ForEach(lists.indices, id: \.self) { index in
                        VStack {
                            AsyncImage(url: URL(string: lists[index].imageURL)) { image in
                                image
                                    .resizable()
                                    .frame(maxWidth: 150, maxHeight: 150)
                            } placeholder: {
                                Image(systemName: "photo")
                            }
                            Link(destination: URL(string: lists[index].shopURL)!) { Text(lists[index].shopTitle)
                                    .fontWeight(.bold)
                                    .foregroundStyle(.blue)
                        }
                    }

여기를 다시 돌려봤는데 갑자기 된다.

안되었던 이유를 모르겠다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.