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
이해하기
📝 정의
wrappedValue
는 Swift의 프로퍼티 래퍼(Property Wrapper)에서 원래 값을 접근할 때 사용하는 속성이다.@State
,@Binding
,@Published
등의 프로퍼티 래퍼는 내부 값을 감싸고 있으며, 이 값을 읽거나 수정할 때wrappedValue
를 사용한다.
🔍 주요 특징
- 프로퍼티 래퍼의 실제 값을 반환하거나 수정할 수 있다.
- SwiftUI에서는
$
를 사용하여wrappedValue
에 간단하게 접근할 수 있다. 예를 들어,$count
는count.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)
}
}
여기를 다시 돌려봤는데 갑자기 된다.
안되었던 이유를 모르겠다.