[SwiftUI] SwiftUI 기초 -공식 튜토리얼(2)

2023. 5. 4. 00:24SWIFT/SwiftUI

728x90
반응형

🧸 이전 글

 

[SwiftUI] SwiftUI 기초 -공식 튜토리얼(1)

🧸 시작 SwiftUI 로 앱 만들기 파일 만들기 Interface -> SwiftUI 로 변경하고 Next 🧸 Inspector 로 수정하기 command + 클릭해서 Show SwiftUI Inspector... 를 클릭하면 해당 화면이 나오게 되는 데 여기에서 간편하

nlestory.tistory.com

 

 

🧸 시작

✔️  목록 및 네비게이션 구축

이전 글에 이어지는 내용으로 이번 튜토리얼에서는 모든 랜드마크에 대한 정보를 표시할 수 있는 보기를 만들고 사용자가 랜드마크에 대한 상세 보기를 보기 위해 누를 수 있는 스크롤 목록을 동적으로 생성한다.

 

 

🧸 랜드마크 모델 만들기

이전에는 하드코딩으로 정보를 입력했다면 이번에는 데이터를 저장할 모델을 만들어 사용한다.

landmarkData.json 파일을 다운로드한다. 이 파일은 튜토리얼 프로젝트 파일 -> Resources 에 있다.

다운로드한 파일을 추가한다.

landmarkData.json
0.02MB

 

이미지 파일도 추가한다.

Images.zip
2.25MB

 

Landmark.swift 파일을 생성한다.

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
    }
}

데이터 파일(json)에 존재하는 속성으로 구조를 생성한다.

이미지에 필요한 이름과 위도, 경도를 포함한 위치정보를 추가한다.

 

ModelData.swift 파일을 생성한다.

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)")
    }
}

json 데이터를 가져오는 load 메소드를 생성한다.

그리고 landmark.json에서 초기화하는 랜드마크 배열을 생성한다.

 

 

🧸 Row View 만들기

각 랜드마크에 대한 세부 정보를 표시하는 행을 만든다.

이 행에는 랜드마크의 속성에 대한 정보를 저장한다.

하나의 뷰에 모든 랜드마크를 표시하고 여러 행을 랜드마크 목록으로 결합한다.

 

SwiftUI파일 (파일명 : LandmarkRow)을 생성한다.

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()
        }
    }
}

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        //LanmarkRowView의 생성자
        LandmarkRow(landmark: landmarks[0])
    }
}

lanmarkd[0]으로 0번째의 인덱스가 가진 정보를 가져온다.

 

 

🧸 Row View 미리보기 사용자 지정

PreviewProvider의 사이즈를 조정할 수 있다.

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        //LanmarkRowView의 생성자
        LandmarkRow(landmark: landmarks[0])
            .previewLayout(.fixed(width: 300, height: 70))
    }
}

캔버스의 사이즈를 정해서 프리뷰를 볼 수 있다. 해당 사이즈를 (300, 70)으로 변경하여 본다.

프리뷰 아래쪽에 화살표버튼을 누르면 변경된 프리뷰의 사이즈로 볼 수 있다.

Group 을 이용하면 여러 개의 프리뷰를 볼 수 있다.

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {
        //LanmarkRowView의 생성자        
        Group {
            LandmarkRow(landmark: landmarks[0])
                    .previewLayout(.fixed(width: 300, height: 70))
            LandmarkRow(landmark: landmarks[1])
                    .previewLayout(.fixed(width: 300, height: 70))
            LandmarkRow(landmark: landmarks[2])
                    .previewLayout(.fixed(width: 300, height: 70))
        }
    }
}

위의 여러개의 프리뷰가 생성되고 여러 개의 프리뷰를 볼 수 있다. 

struct LandmarkRow_Previews: PreviewProvider {
    static var previews: some View {                
        //LanmarkRowView의 생성자        
        Group {
            LandmarkRow(landmark: landmarks[0])
            LandmarkRow(landmark: landmarks[1])
            LandmarkRow(landmark: landmarks[2])
        }
        .previewLayout(.fixed(width: 300, height: 70))
    }
}

그룹으로 묶어서 라이아웃 사이즈를 한 번에 설정할 수도 있다.

 

 

🧸 랜드마크 목록 만들기

위에서 만든 rowView를 가지고 리스트를 만든다.

SwiftUI파일 (파일명 : LandmarkList)을 생성한다.

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

 

 

 

🧸 동적인 리스트 만들기

목록의 요소를 개별적으로 지정하는 대신에 컬렉션에서 직접 행을 생성할 수 있다. 컬렉션의 각 요소에 대한 뷰를 제공하는 클로저와 데이터 컬렉션을 전달하여 컬렉션의 요소를 표시하는 목록을 만들 수 있다.

그니까 위에 코드 처럼 인덱스를 작성해서 넣어주지 않고 데이터를 가지고 리스트를 만들 수 있다는 것이다.

먼저 Landmark 모델에서 Identifiable 를 추가해준다.

struct Landmark: Hashable, Codable, Identifiable {
    var id: Int
    var name: String
    var park: String
    var state: String
    var description: String
    
	... 
}

 

Identifiable 프로토콜은 이렇게 구성되어 있다.

public protocol Identifiable<ID> {

    /// A type representing the stable identity of the entity associated with
    /// an instance.
    associatedtype ID : Hashable

    /// The stable identity of the entity associated with this instance.
    var id: Self.ID { get }
}

Hashable을 준수하는 ID값이 있고 그 ID를 타입으로 하는 id 값으로 구성되어 있다.

Identifiable을 채택하여 해당 구조체에 id값을 생성하면 식별자로 사용할 수 있는 것이다.

 

모델에서 Identifiable를 채택하여 id 값을 식별자로 사용하고 있기 때문에 다른 id 값의 데이터를 모두 리스트에 사용하는 것이다.

struct LandmarkList: View {
    var body: some View {
//        List {
//            LandmarkRow(landmark: landmarks[0])
//            LandmarkRow(landmark: landmarks[1])
//        }
        
        //식별가능한 identifiable로 설정하여 그에 맞는 데이터를 목록에 나열한다.
        List(landmarks) { landmark in
            LandmarkRow(landmark: landmark)
        }
    }
}

 

 

이제 반복문을 사용하지 않고도 식별자를 사용하여 빠르게 리스트를 만들 수 있다.

 

 

 

🧸 목록과 세부 정보간의 네비게이션 설정 

여기까지는 목록은 데이터와 렌더링이 되었지만 아직 개별 랜드마크의 세부 정보 페이지를 볼 수 없다.

목록에서 각 행을 클릭했을 때 상세 정보 페이지가 나타나게 설정한다. 이를 네비게이션을 사용하여 연결한다.

 

세부 정보를 보여주기 위한 SwiftUI파일 (파일명 : LandmarkDetail)을 생성한다.

기존에 작성했던 ContentView의 내용을 복사하여 LandmarkDetail 에 붙여넣는다.

struct LandmarkDetail: View {
    var body: some View {
        VStack {
            
            //지도뷰 추가
            MapView()
                //safeArea 위쪽 부분 무시하고 채우기
                .ignoresSafeArea(edges: .top)
                .frame(height: 300)
            
            //원이미지 추가
            CircleImage()
                .offset(y: -130)
                .padding(.bottom, -130)
            
            VStack(alignment: .leading) {
                Text("Turtle Rock")
                    .font(.largeTitle)
                
                HStack {
                    Text("Joshua Tree National Park")
                    
                    Spacer()

                    Text("California")
                }
                //부제목의 텍스트 크기와 색상 설정하기
                .font(.subheadline)
                .foregroundColor(.secondary)
                
                //구분선넣기
                Divider()
                
                Text("어바웃 터틀 락")
                    .font(.title2)
                Text("터틀 락에 대한 설명입니다.")
            }
            .padding()
            
            //밑의 공간을 확보해서 위의 내용 제일 위로 올리기
            Spacer()
        }
    }
}

그리고 ContentView 에는 LandmarkList 를 보여준다.

struct ContentView: View {
    var body: some View {
        LandmarkList()
    }
}

 

이제 목록화면에서 세부화면으로 넘어가는 것을 해야한다.

각 행을 눌렀을 때 세부화면으로 화면 전환을 해야하는데 네비게이션을 사용할 것이다.

import SwiftUI

struct LandmarkList: View {
    var body: some View {
        
        //식별가능한 identifiable로 설정하여 그에 맞는 데이터를 목록에 나열한다.
        NavigationView {
            List(landmarks) { landmark in
                
                //NavigationLink = push 와 같은 역할
                //destination = 클릭했을 때의 화면 = 이동할 화면
                NavigationLink(destination: LandmarkDetail()) {
                    //내용
                    LandmarkRow(landmark: landmark)
                }
            }
            //타이틀
            .navigationTitle("Landmarks")
        }
    }
}

NavigationView 를 사용하여 전체화면을 네비게이션컨트롤러 임베디드 시킨 것처럼 변경한다.

그러면 이제 타이틀을 사용할 수 있다. navigationTitle로 제목을 설정한다.

NavigationLink를 통해서 다음 넘어갈 화면을 정한다. destination 속성에 이동할 화면을 넣는다. 네비게이션 링크 자체가 버튼처럼 작동한다. 클릭시 다음으로 넘어간다.

리스트 안에 네비게이션 링크가 있기에 리스트에 모든 행이 네비게이션과 연결되어 있다. 그리고 { } 안에 행안의 내용을 넣어주면 된다.

NavigationLink(destination: /*목적지*/) {
	/*나타낼 뷰*/
}

공식 홈페이지에서 나타는 방식은

NavigationLink {
	/*목적지*/
} label: {
	/*나타낼 뷰*/
}

이렇게도 가능하다.

이제 목록의 각 행을 클릭하면 상세 화면으로 넘어가게 된다.

LandmarkList View 행을 클릭한 후 LandmarkDetail View

 

 

 

🧸 하위 뷰에 데이터 전달

상세화면은 여전히 하드코딩된 세부정보를 사용하여 랜드마크를 나타낸다.

세부 정보를 받아서 상세화면에도 각 행의 정보를 나타내야한다.

 

일단 원이미지의 파일을 수정한다.

import SwiftUI

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

struct CircleImage_Previews: PreviewProvider {
    static var previews: some View {
        CircleImage(image: Image("turtlerock"))
    }
}

 

지도파일도 수정한다.

import SwiftUI
import MapKit

struct MapView: View {
    var coordinate: CLLocationCoordinate2D
    
    @State
    private var region = MKCoordinateRegion()
    
    var body: some View {
        Map(coordinateRegion: $region)
            //좌표기반으로 지역 계산
            .onAppear {
                setRegion(coordinate)
            }
    }
    
    //좌표 값을 기반으로 지역을 업데이트하는 메소드
    private func setRegion(_ coordinate: CLLocationCoordinate2D) {
        region = MKCoordinateRegion(
            center: coordinate,
            span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
        )
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(coordinate: CLLocationCoordinate2D(latitude: 34.011_286, longitude: -116.166_868))
    }
}

 

LandmarkDetail 파일도 수정한다.

struct LandmarkDetail: View {
    var landmark: Landmark
    
    ...
    
}

struct LandmarkDetail_Previews: PreviewProvider {
    static var previews: some View {
        LandmarkDetail(landmark: landmarks[0])
    }
}

 

LandmarkList파일에서 LandmarkDetail의 init을 설정해준다.

NavigationLink(destination: LandmarkDetail(landmark: landmark)) {
    LandmarkRow(landmark: landmark)
}

 

 

마지막으로 상세화면에서 위에서 생성한 프로퍼티들을 추가하여 데이터와 연결해주면 된다.

struct LandmarkDetail: View {
    var landmark: Landmark
    
    var body: some View {
        
        ScrollView {
            //지도뷰 추가
            MapView(coordinate: landmark.locationCoordinate)
                //safeArea 위쪽 부분 무시하고 채우기
                .ignoresSafeArea(edges: .top)
                .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)
                .foregroundColor(.secondary)
                
                //구분선넣기
                Divider()
                
                Text("About \(landmark.name)")
                    .font(.title2)
                Text(landmark.description)
            }
            .padding()
        }
        //네비게이션으로 넘겨받았으므로 뒤로가기버튼이 생기는데 그 부분의 타이틀 생성
        .navigationTitle(landmark.name)
        .navigationBarTitleDisplayMode(.inline)
    }
}

 

 

 

VStack으로 쌓았던 내용은 스크롤이 불가능하므로 ScrollView 로 변경하여 스크롤이 가능하게 해준다.

그리고 네비게이션의 타이틀을 설정해준다.

 

 

✔️  완성

   

 

 

 

 

🧸 다음 글

 

[SwiftUI] SwiftUI 기초 -공식 튜토리얼(3)

🧸 이전 글 [SwiftUI] SwiftUI 기초 -공식 튜토리얼(2) 🧸 이전 글 [SwiftUI] SwiftUI 기초 -공식 튜토리얼(1) 🧸 시작 SwiftUI 로 앱 만들기 파일 만들기 Interface -> SwiftUI 로 변경하고 Next 🧸 Inspector 로 수정하

nlestory.tistory.com

 

 

 

 

 

 

 

 

 

 

[예제 소스코드 깃허브 링크]

https://github.com/HANLeeeee/SwiftUITest/tree/main/SwiftUITest2

 

GitHub - HANLeeeee/SwiftUITest

Contribute to HANLeeeee/SwiftUITest development by creating an account on GitHub.

github.com

[참고자료]

Apple_Developer_Tutorials_SwiftUI(2)

 

Building Lists and Navigation | Apple Developer Documentation

With the basic landmark detail view set up, you need to provide a way for users to see the full list of landmarks, and to view the details about each location.

developer.apple.com

 

 

 

 

 

728x90
반응형

'SWIFT > SwiftUI' 카테고리의 다른 글

[SwiftUI] SwiftUI 기초 -공식 튜토리얼(3)  (0) 2023.05.04
[SwiftUI] SwiftUI 기초 -공식 튜토리얼(1)  (1) 2023.05.03