본문 바로가기
iOS/SwiftUI Tutorial

SwiftUI Tutorial 3 - Building lists and navigation

by 6cess 2024. 7. 5.

1. Create a landmark model

먼저 튜토리얼 페이지에서 제공하는 데이터 파일들을 프로젝트에 다운 받은 후 데이터를 담을 구조체를 만든다.

연산 프로퍼티인 image 와 locationCoordinate도 추후에 용이하게 써먹기 위해 만들어 놓는다.

import Foundation
import SwiftUI
import CoreLocation

struct Landmark: Hashable, Codable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    
    private var imageName: String
    var image: Image {
        Image (imageName)
    }
    
    private var coordinates: Coordinates
    var locationCoordinate: CLLocationCoordinate2D {
        CLLocationCoordinate2D(
            latitude: coordinates.latitude,
            longitude: coordinates.longitude)
    }
    
    struct Coordinates: Hashable, Codable {
        var latitude: Double
        var longitude: Double
    }
}

 

당연하겠지만 이는 데이터 형식을 반영한 것.

 

 

다음으로 데이터 파일을 읽어오는 기능을 구현한다.

import Foundation

var landmarks: [Landmark] = load("landmarkData.json")

func load<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) from main bundle:\n\(error)")
    }


    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
    }
}

 

2. Create the row view

Landmark 모델 하나를 담을 Row view를 만든다.

import SwiftUI

struct LandmarkRow: View {
    var landmark: Landmark
    var body: some View {
        HStack {
            landmark.image
                .resizable()
                .frame(width: 50, height: 50)
            Text(landmark.name)
            Spacer()
        }
    }
}

#Preview {
    LandmarkRow(landmark: landmarks[0])
}

 

3. Customize the row preview

preview를  customize 하는 방법이다.

 

여러 개의 preview 를 띄울 수 있고 각각 이름을 붙일 수 있다.

#Preview("Turtle Rock") {
    LandmarkRow(landmark: landmarks[0])
}


#Preview("Salmon") {
    LandmarkRow(landmark: landmarks[1])
}

 

아니면 하나의 preview에 두 view를 side by side 보이게 할 수 있다.

#Preview() {
    Group {
        LandmarkRow(landmark: landmarks[0])
        LandmarkRow(landmark: landmarks[1])
    }
}

 

4. Create the list of landmarks

List 뷰를 사용해보자

struct LandmarkList: View {
    var body: some View {
        List {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
        }
    }
}


#Preview {
    LandmarkList()
}

 

5. Make the list dynamic

동적으로 landmarks 변수의 데이터를 가져와 그려주는 List 를 구현한다.

import SwiftUI


struct LandmarkList: View {
    var body: some View {
        List(landmarks, id: \.id) { landmark in


        }
    }
}

 

여기서 나는 \.id 라는 문법이 생소해서 알아보니 key path expression 이라고..

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/#Key-Path-Expression

 

Documentation

 

docs.swift.org

 

위 코드의 List 생성자의 파라미터 타입을 보면 아래와 같이 되어 있다.

 

첫번째 파라미터로 입력된 Data가 있으면 두번째 파라미터로 KeyPath<Data.Element, ID> 를 받는다.

 

만약 이러한 id 할당이 번거롭다면 모델 구조체에 Identifiable 을 추가하자.

 

그러면 간단하게 아래와 같이 구현할 수 있다.

List(landmarks) { landmark in
    LandmarkRow(landmark: landmark)
}

 

 

6. Set up navigation between list and detail

List 를 NavigationSplitView 로 감싼 후 row 하나 하나를 NaavigationLink 로 감싼 후 LandmarkDetail로 이동하도록 설정

struct LandmarkList: View {
    var body: some View {
        NavigationSplitView {
            List(landmarks) { landmark in
                NavigationLink {
                    LandmarkDetail()
                } label: {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationTitle("Landmarks")
        } detail: {
            Text("Select a Landmark")
        }
    }
}

 

LandmarkDetail 은 이전에 ContentView 로 작성했던 코드를 그대로 가져온 것

 

7.  Pass data int child views

이제 List 초기화 때 입력된 데이터를 각 Row 별로 데이터를 전달하도록 한다

 

struct CircleImage: View {
    var image: Image

    var body: some View {
        image
            .clipShape(Circle())
            .overlay {
                Circle().stroke(.white, lineWidth: 4)
            }
            .shadow(radius: 7)
    }
}
struct MapView: View {
    var coordinate: CLLocationCoordinate2D

    var body: some View {
        Map(position: .constant(.region(region)))
    }

    private var region: MKCoordinateRegion {
        MKCoordinateRegion(
            center: coordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
        )
    }
}

 

이제 List 에서 Row 별로 데이터를 넘겨주고

struct LandmarkList: View {
    var body: some View {
        NavigationSplitView {
            List(landmarks) { landmark in
                NavigationLink {
                    LandmarkDetail(landmark: landmark)
                } label: {
                    LandmarkRow(landmark: landmark)
                }
            }
            .navigationTitle("Landmarks")
        } detail: {
            Text("Select a Landmark")
        }
    }
}

 

 

LandmarkDetail 에서 하위 뷰에 필요한 데이터를 넘겨준다 (전체를 감싸는 Vstack을 ScrollView 로 교체하는 건 덤)

struct LandmarkDetail: View {
    var landmark: Landmark

    var body: some View {
        ScrollView {
            MapView(coordinate: landmark.locationCoordinate)
                .frame(height: 300)

            CircleImage(image: landmark.image)
                .offset(y: -130)
                .padding(.bottom, -130)


            VStack(alignment: .leading) {
                Text(landmark.name)
                    .font(.title)


                HStack {
                    Text(landmark.park)
                    Spacer()
                    Text(landmark.state)
                }
                .font(.subheadline)
                .foregroundStyle(.secondary)

                Divider()

                Text("About \(landmark.name)")
                    .font(.title2)
                Text(landmark.description)
            }
            .padding()

            Spacer()
        }
    }
}

 

8. Generate previews dynamically

Device setting 에 따라 NavigationSplitView가 동적으로 변하는 것을 체크