SwiftUI 리스트와 내비게이션 튜토리얼
Car 구조체 추가하기
자동차 모델을 나타내는 구조체 선언
import SwiftUI
struct Car: Codable, Identifiable {
var id: String
var name: String
var description: String
var isHybrid: Bool
var imageName: String
}
JSON 파일에 있는 각 필드에 대한 프로퍼티를 포함
Identifiable 프로토콜 선언 -> List 뷰에서 식별
Codable 프로토콜 선언 -> JSON 파일 읽어오기
JSON 데이터 로딩하기
JSON 파일을 읽어와서 Car 객체로 변환한 다음 배열에 넣는 작업
import UIKit
import SwiftUI
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)
// JSON 파일의 url 주소 입력
else {
fatalError("\(filename) not found.")
}
do {
data = try Data(contentsOf: file)
// 짧은 파일을 동기적으로 읽는데 사용
} catch {
fatalError("Could not load \(filename): \(error)")
}
do {
return try JSONDecoder().decode(T.self, from: data)
// JSON 파일을 Car 객체로 디코딩
} catch {
fatalError("Unable to parse \(filename): \(error)")
}
}
loadJson() 함수의 코드는 JSON 파일을 로드하는 표준 방식
데이터 저장소 추가하기
List 뷰를 최신 데이터로 유지하기 위해 사용자 인터페이스는 게시된 프로퍼티(published property) 를 포함
Observable 객체에 의존
import SwiftUI
import Combine
class CarStore : ObservableObject {
@Published var cars: [Car]
init (cars: [Car] = []) {
self.cars = cars
}
}
초기화로 전달된 배열을 게시자로 선언
콘텐트 뷰 설계하기
import SwiftUI
struct ContentView: View {
@ObservedObject var carStore : CarStore = CarStore(cars: carData)
.
.
CarStore의 인스턴스에 구독 객체(observed object) 바인딩 추가
carData 배열을 초기화 작업에 전달
var body: some View {
List {
ForEach (carStore.cars) { car in
ListCell(car: car) // 반복문의 car 객체가 전달
}
}
}
}
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)
}
}
}
carStore 프로퍼티를 통해 배열에 접근
상세 뷰 설계하기
상세 뷰: 리스트의 행을 터치하면 선택한 자동차의 추가 정보가 표시
import SwiftUI
struct CarDetail: View {
let selectedCar: Car
var body: some View {
Text("Hello, World")
}
}
struct CarDetail_Previews: PreviewProvider {
static var previews: some View {
CarDetail(selectedCar: carData[0])
}
}
Car 구조체에 대한 프로퍼티 추가
carData 배열에 첫 번째 자동차의 세부 내용이 표시
var body: some View {
Form {
Section(header: Text("Car Details")) {
Image(selectedCar.imageName)
.resizable() // 크기 조절
.cornerRadius(12.0) // 이미지에 라운드 코너 적용
.aspectRatio(contentMode: .fit) // 정비율 조절
.padding()
Text(selectedCar.name)
.font(.headline)
Text(selectedCar.description)
.font(.body)
HStack {
Text("Hybrid")
.font(.headline)
Spacer()
Image(systemName: selectedCar.isHybrid ? "checkmark.circle" : "xmark.circle")
}
}
}
}
Form 컨테이너: 뷰들을 그루핑하고 서로 다른 섹션으로 나눌 수 있는 컨테이너 뷰
리스트에 내비게이션 추가하기
사용자가 선택한 자동차의 상세 정보가 담긴 상세 화면을 표시
struct ContentView: View {
@ObservedObject var carStore : CarStore = CarStore(cars: carData)
var body: some View {
NavigationView {
List {
ForEach (carStore.cars) { car in
ListCell(car: car)
}
}
}
}
}
List 뷰를 NavigationView 로 감쌈
struct ListCell: View {
var car: Car
var body: some View {
NavigationLink(destination: CarDetail(selectedCar: car)) {
HStack {
Image(car.imageName)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 100, height: 60)
Text(car.name)
}
}
}
}
HStack을 CarDetail 뷰를 목적지로 하는 NavigationLink 로 감쌈
선택된 Car 객체를 전달
자동차 추가하는 뷰 설계하기
리스트에 새로운 자동차 정보를 추가하는 뷰
import SwiftUI
struct AddNewCar: View {
@ObservedObject var carStore : CarStore
@State var isHybrid = false
@State var name: String = ""
@State var description: String = ""
.
.
}
struct AddNewCar_Previews: PreviewProvider {
static var previews: some View {
AddNewCar(carStore: CarStore(cars: carData))
}
}
carStore 바인딩에 대한 참조체를 저장하는 선언부 추가
Add 버튼을 터치하면 ContentView에서 뷰로 전달
상세 정보를 사용자가 입력하게 될 TextField 뷰와 Text 뷰 추가
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()
}
}
String 값과 사용자가 입력한 내용이 전달될 상태 프로퍼티 바인딩 전달
상태 프로퍼티가 전달될 경우 @Binding 프로퍼티 래퍼 사용
func addNewCar() {
let newCar = Car(id: UUID().uuidString,
name: name,
description: description,
isHybrid: isHybrid,
imageName: "tesla_model_3" )
carStore.cars.append(newCar) // carStore 배열에 추가
}
식별자를 생성하는 UUID() 메서드를 사용하여 새로운 Car 객체 생성
이미지는 재사용
Add 버튼과 Edit 버튼 구현하기
리스트 컴포넌트 수정자, navigationBarTitle() navigationBarItems() 수정자 사용
var body: some View {
NavigationView {
List {
ForEach (carStore.cars) { car in
ListCell(car: car)
}
}
.navigationBarTitle(Text("EV cars"))
.navigationBarItems(leading: NavigationLink(destination:
AddNewCar(carStore: self.carStore)) {
Text("Add")
.foregroundColor(.blue)
}, trailing: EditButton())
}
}
extra space 발생
Large Title 때문? -> Large Title 이면서 공간을 없애는 방법…
Edit 버튼 메서드 추가하기
EditButton 뷰에 의해 사용될 메서드 추가
List {
ForEach (carStore.cars) { car in
ListCell(car: car)
}
.onDelete(perform: deleteItems)
.onMove(perform: moveItems)
}
리스트의 모든 열에 사용될 것이므로 리스트 셀에 적용
<deleteItems 함수>
func deleteItems(at offsets: IndexSet) {
carStore.cars.remove(atOffsets: offsets)
}
선택된 행의 오프셋이 전달, 배열에서 해당 항목을 삭제하는 데 사용
<moveItems 함수>
func moveItems(from source: IndexSet, to destination: Int) {
carStore.cars.move(fromOffsets: source, toOffset: destination)
}
사용자가 리스트의 행 위치를 이동하면 호출
오프셋과 함께 이동된 위치가 전달