Post

SwiftUI 실습 3

SwiftUI 실습 3

List와 NavigationStack을 이용해서 각 행마다 이미지와 텍스트 정보를 가진 리스트를 만들고 추가/삭제/행 순서 변경이 가능하게 만든다 여기에 좀더 확장해서 Observable 객체를 추가해서 변동사항이 있다면 반영하도록 만든다

 

실습을 위한 데이터

실습을 위해 차사진 carAssets을 추가하고 차량 정보를 사진 carData.json데이터를 프로젝트에 추가한다

Car.swift

1
2
3
4
5
6
7
8
9
10
11
12
// Car.swift
import SwiftUI

struct Car : Codable, Identifiable {
    var id: Int
    var name: String
    
    var description: String
    var isHybrid: Bool
    
    var imageName: String
}

json에 있는 차량의 정보를 토대로 구조체를 만들고 Idenrifiable 프로토콜을 통해서 각 인스턴스들은 List에서 식별 가능하다

Json 파일 읽기

파일에서 차량 데이터를 읽고 위 Car객체로 변환한 다음 배열에 넣어야한다 파일을 읽기 위해서 CarData.swift 파일 생성

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
// CarData.swift
import SwiftUI
import UIKit

var carData: [Car] = loadJson("carData.json")

func loadJson<T: Decodable>(_ filename: String) -> T {
    let data: Data
    
    guard let file = Bundle.main.url(forResource: filename, withExtension: nil) else {
        fatalError("Couldn't find \(filename) in main bundle.")
    }
    
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(filename): \(error)")
    }
    
    do {
        return try JSONDecoder().decode(T.self, from: data)
    } catch {
        fatalError("Couldn't decode \(filename): \(error)")
    }
}

여기서 작성한 loadJson() 함수는 JSON파일을 로드하는데 사용하는 표준 방식이다

데이터 저장소 추가

사용자 인테페이스에서 항상 최신 데이터만 보여주기 위해서 Observable 객체를 사용한다 앱에서 데이터를 최신으로 유지하기 위해서는 데이터 저장소 구조체를 추가해야 한다 이 구조체는 List뷰를 최신으로 유지하기 위해서 게시된 프로퍼티(published property)를 포함해야한다

1
2
3
4
5
6
7
8
9
10
// CarStore.swift
import SwiftUI

class CarStore: ObservableObject { // ObservableObject를 사용해서 관찰할 수 있게함
    @Published var cars: [Car] = [] // Published가 변화를 감지하고 알려줌
    
    init(cars: [Car]) {
        self.cars = cars
    }
}

이 부분을 데이터 저장소라고 부르는 이유는 이 클래스가 데이터를 저장하기 때문이다

 

콘텐츠 뷰

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
import SwiftUI

struct ContentView: View {
    
    @StateObject var carStore: CarStore = CarStore(cars: carData)
    
    var body: some View {
        List {
            ForEach(0..<carStore.cars.count, id: \.self) { index in
                ListCell(car: carStore.cars[index])
            }
        }
    }
}
struct ListCell: View {
    var car: Car
    
    var body: some View {
        HStack {
            Image(car.imageName)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 100, height: 60)
            Text(car.name)
        }
    }
}

@StateObject이 부분은 CarStore 클래스에는 생성자(init(cars: [Car]))가 정의되어 있어서 이 생성자는 CarStore 인스턴스를 만들 때 자동차 목록을 전달받아 이를 초기화한다 carData는 CarData.swift에서 값을 설정했다

상세뷰 설정

리스트에서 해당 항목을 클릭하면 Car객체를 새로운 뷰에 전달한다

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
//CarDetail
import SwiftUI

struct CarDetail: View{
    let selectedCar: Car
    
    var body: some View{
        Form {
            Section(header: Text("Car Details")){
                Image(selectedCar.imageName)
                    .resizable()
                    .cornerRadius(10)
                    .aspectRatio(contentMode: .fit)
                    .padding()
                
                Text(selectedCar.name)
                    .font(.headline)
                    
                Text(selectedCar.description)
                
                HStack{
                    Text("Hybrid")
                    Spacer()
                    Image(systemName: selectedCar.isHybrid ?
                    "Checkmark.circle" : "xmark.circle.fill")
            }
        }
    }
    }
}

아직 이동하는 링크를 설정하지 않아 이동할 수는 없다

리스트에 네비게이션 추가

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
import SwiftUI

struct ContentView: View {
    
    @StateObject var carStore: CarStore = CarStore(cars: carData)
    
    var body: some View {
        NavigationStack {
            List {
                ForEach(0..<carStore.cars.count, id: \.self) { index in
                    NavigationLink(value: index){
                        ListCell(car: carStore.cars[index])
                    }
                }
            }
            .navigationDestination(for: Int.self){ i in
                CarDetail(selectedCar: carStore.cars[i])
            }
        }
    }
}

struct ListCell: View {
    var car: Car
    
    var body: some View {
        HStack {
            Image(car.imageName)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width: 100, height: 60)
            Text(car.name)
        }
    }
}

 

자동자 정보 추가 뷰 생성

AddNewCar.swift 파일을 생성

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
//AddNewCar.swift
import SwiftUI

struct AddNewCar: View {
    @StateObject var carStore: CarStore
    @State private var isHybrid = false
    @State private var name: String = ""
    @State private var description: String = ""
    
    var body: some View {
        Form {
            Section(header: Text("CarDetail")){
                Image(systemName: "car.fill")
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .padding()
                
                DataInput(title: "Model", userInput: $name)
                DataInput(title: "Description", userInput: $description)
                
                Toggle("Hybrid", isOn: $isHybrid)
            }
            Button(action: addNewCar) {
                Text("Add New Car")
            }
        }
    }
    func addNewCar() {
        let newCar = Car(id: UUID().uuidString, name: name, description: description, isHybrid: isHybrid, imageName: "tesla_model_3")
        carStore.cars.append(newCar)
    }
}

struct DataInput: View { // 사용자가 데이터를 입력하는 입력창 정의
    var title: String
    @Binding var userInput: String
    var body: some View {
        VStack(alignment: HorizontalAlignment.leading) {
            Text(title)
                .font(.headline)
                
            TextField("Enter \(title)", text: $userInput)
                .textFieldStyle(RoundedBorderTextFieldStyle())
        }
        .padding()
    }
}

@Binding 프로퍼티 래퍼는 SwiftUI에서 데이터를 양방향으로 전달할 때 사용된다 즉, 하위 뷰에서 전달받은 데이터를 수정할 수 있고, 그 수정 사항이 상위 뷰에도 반영되도록 한다 @Binding하위 뷰에서 상위 뷰의 상태를 수정할 수 있게 한다

추가, 수정 버튼 추가

만들었던 추가 뷰로 이동할 수 있는 링크를 연결해야한다

1
2
3
4
5
6
7
8
9
10
//ContentView.swift
List{
    //생략
}
.navigationDestination(for: String.self){ _ in
                AddNewCar(carStore: self.carStore)
            }
            .navigationTitle("Cars")
            .navigationBarItems(leading: NavigationLink(value: "add") {Text("Add")}, trailing: EditButton())

네비게이션 경로 추가하기

차를 추가할때 데이터를 넣고 add New Car를 누르면 메인화면으로 나가야한지만 위 코드에서는 그런 기능이 없다 이 기능을 만들기 위해서는 네이비게이션 경로를 이용한다

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
import SwiftUI

struct ContentView: View {
    
    @StateObject var carStore: CarStore = CarStore(cars: carData)
    @State private var stackPath = NavigationPath() // 변수 추가
    
    var body: some View {
        
        NavigationStack(path: $stackPath) { //추가
            List {
                ForEach(0..<carStore.cars.count, id: \.self) { index in
                    NavigationLink(value: index){
                        ListCell(car: carStore.cars[index])
                    }
                }
            }
            .navigationDestination(for: Int.self){ i in
                CarDetail(selectedCar: carStore.cars[i])
            }
            .navigationDestination(for: String.self){ _ in
                AddNewCar(carStore: self.carStore, path: $stackPath) //경로를 넘겨준다
            }
            .navigationTitle("Cars")
            .navigationBarItems(leading: NavigationLink(value: "add") {Text("Add")}, trailing: EditButton())
        }
    }
}

AddNewCar.swift 수정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//AddNewCar.swift
import SwiftUI

struct AddNewCar: View {
    @StateObject var carStore: CarStore
    @Binding var path: NavigationPath //바인딩 매개변수
    @State private var isHybrid = false
    @State private var name: String = ""
    @State private var description: String = ""
    
    var body: some View {
        Form {
            //생략
            }
        }
    }
    func addNewCar() {
        let newCar = Car(id: UUID().uuidString, name: name, description: description, isHybrid: isHybrid, imageName: "tesla_model_3")
        carStore.cars.append(newCar)
        path.removeLast() // 버튼을 누른다면 경로를 초기화 시키고 돌아간다
    }
}

This post is licensed under CC BY 4.0 by the author.