SwiftUI Tutorials - Landmarks(2)
지난 시간에는 스택과 View들을 구성하는 방법과 함께 서로 다른 view를 합치는 작업을 해보았다.
이번 시간에는 리스트와 내비게이션을 이용하여 랜드 마크의 전체 목록을 확인할 수 있고, 해당 랜드마크의 세부 정보를 확인할 수 있도록 해보았다.
1. Create a Landmark Model
튜토리얼에서 제공하는 Resources파일 중 랜드마크 데이터.json을 현재 진행 중인 프로젝트 탐색 창으로 들고 온다.
그러고 나서 Landmark.swift 파일을 하나 생성해서 json파일 내부에 있는 키의 이름과 일치하는 몇 가지의 속성을 이용하여 Landmark구조를 정의한다.
import Foundation
struct Landmark: Hashable, Codable {
var id: Int
var name: String
var park: String
var state: String
var description: String
}
그리고 추가적으로 JPG파일 또한 프로젝트의 asset으로 추가한다. 이전 시간에 추가했던 Turtle Rock의 이미지와 같은 작업이다.
여기서 추가하는 요소들은 다음과 같다.
- 데이터에서 이미지를 이름을 읽어내기 위해 imagename속성을 String 형태로 추가한다.
- JSON 파일을 반영하는 중첩 좌표 유형을 사용해서 struct에 좌표 속성을 추가한다.
- Mapkit 프레임워크와 상호 작용하는데 좋은 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
}
}
이제 다시 새로운 Model.swift을 생성하고 앱의 기본 번들에서 지정된 이름의 JSON 데이터를 가져오는 load(_:)메서드를 만든다.
여기서 load메서드는 Codable 프로토콜의 한 구성요소인 Decodable 프로토콜에 대한 반환 유형에 의존한다.
import Foundation
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 파일에서 초기화 하는 랜드마크 배열을 만들어 주는 코드를 추가한다.
var landmarks: [Landmark] = load("landmarkData.json")
2. Create the Row View
이번 챕터에서는 각 랜드마크에 대한 세부 정보를 표시하는 행을 만들어 보는것이다. 나중에 여기서 만든 행을 랜드마크 목록으로 결합하게 된다.
그럼 바로 LandmarkRow.swift라는 새로운 ui뷰를 만들어 주고 저장 속성으로 landmark를 추가해준다.
랜드마크 속성의 이름을 사용할 수있도록 Text view를 수정한다. (기존 Hello world에서 landmark.name으로) 그리고 텍스트 앞에 이미지를 추가한다. 이 뷰를 HStack으로 감싸주고 Spacer()를 추가해준다.
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 {
LandmarkRow(landmark: landmarks[0])
}
}
3. Create the List of Landmarks
이 챕터에서는 개발자가 swiftui에서 list 유형을 사용할때 list의 요소를 지금까지 만들었던 스택의 view와 같이 정적으로 만들 수 있고, 동적으로도 생성할 수 있다는 것을 가르쳐준다. 이뿐 아니라 정적 뷰와 동적 뷰를 서로 혼합할 수도 있다.
struct LandmarkList: View {
var body: some View {
List {
LandmarkRow(landmark: landmarks[0])
LandmarkRow(landmark: landmarks[1])
}
}
}
4. Make the List Dynamic
list를 이용하기 용도는 위에서 보았듯 요소들을 나열 할 수 있는 것인데, 더 편한 방법은 없을까? 이번 챕터에서는 list에 들어갈 요소를 개별적으로 지정해주는 대신에 집합에서 행을 생성해서 list에서 표시되도록 할 수 있다.
list는 제공된 closure를 이용해 컬렉션에서 각 요소를 view로 변환 할 수 있게 되는 것이다.
바로 위에서 만들었던 두 개의 정적 랜드마크 row를 제거해버리고 모델 데이터의 랜드 마크에 대한 배열을 이니셜 라이저에 전달하는 작업을 해보자.(1) 이번에는 해당 list 내부에 클로저에서 LandmarkRow를 반환하여 동적으로 작동하게끔 완성해보자. (2)
struct LandmarkList: View {
var body: some View {
List(landmarks, id: \.id) { landmark in //(1)
LandmarkRow(landmark: landmark) //(2)
}
}
}
이번에는 landmark.swift파일로 와서 프로토콜에 대한 identifiable을 선언해주자.
landmark의 데이터에는 이제 identifiable 프로토콜에 필요한 id 속성이 이미 있으니 데이터를 들고 올 때 디코딩할 수 있는 속성을 추가만 해주면 된다.
struct Landmark: Hashable, Codable, Identifiable {
이제 identifiable을 추가해주었으니 다시 list파일로 들어와서 id 매개변수를 제거해준다.
struct LandmarkList: View {
var body: some View {
List(landmarks) { landmark in
LandmarkRow(landmark: landmark)
}
}
}
5. Set Up Navigation Between List and Detail
이제 list가 제대로 렌더링 되었는데 아직까지는 랜드마크 하나하나를 눌러 해당 랜드마크의 세부 페이지를 볼 수없다.
그래서 이번에는 Navigation View에 내비게이션 기능을 내장한 다음 Navigation Link에 각 행을 넣어서 해당 페이지로의 전환을 설정하면? 아마 가능할것이다. 따라서 이번 챕터에서는 네비게이션 기능을 list에 추가해보자.
또 새로운 swiftui 파일을 하나 만들자 이름은 LandmarkDetail로 하고, 이 페이지에서 랜드마크의 세부 사항을 확인할 수 있다.
Contentview에 있던 body 속성 내부 내용을 그대로 다 복사해서 Detail페이지에 붙여 넣자.
import SwiftUI
struct LandmarkDetail: View {
var body: some View {
VStack {
MapView()
.ignoresSafeArea(edges: .top)
.frame(height: 300)
CircleImage()
.offset(y: -130)
.padding(.bottom, -130)
VStack(alignment: .leading) {
Text("Turtle Rock")
.font(.title)
HStack {
Text("Joshua Tree National Park")
Spacer()
Text("California")
}
.font(.subheadline)
.foregroundColor(.secondary)
Divider()
Text("About Turtle Rock")
.font(.title2)
Text("Descriptive text goes here.")
}
.padding()
Spacer()
}
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
LandmarkDetail()
}
}
Contentview에는 landmarkList를 표시하도록 변경해준다.
그리고 LandmarkList파일에도 변화가 발생하는데 그 내용은 다음과 같다.
- NavigationView에 동적으로 생성된 랜드 마크 목록을 넣어준다.
- Navigation을 사용했으니 NavigationTitle() 수정자 메서드를 호출해서 list를 표시할 때 제목을 설정해준다.
- list 클로저 내부에서 detail뷰를 목적지로 지정하여 NavigationLink에서 반환된 행을 감싸주자.
import SwiftUI
struct LandmarkList: View {
var body: some View {
NavigationView {
List(landmarks) { landmark in
NavigationLink {
LandmarkDetail()
} label: {
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
6. Pass Data into Child Views
랜드마크 세부 뷰에서 이제 출력되긴 하나 아직까지는 하드 코딩된 세부 정보를 사용하여 랜드마크를 표시한다. 따라서 CircleImage, Mapview, LandmarkDetail을 변환하여 각 행을 하드 코딩하지 않고 전달받은 데이터를 표시할 수 있도록 해보자.
먼저 CircleImage 뷰부터 변경해보자.
여기서는 저장된 이미지 속성을 var형태로 추가해준다.
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"))
}
}
이번에는 MapView 차례다.
Mapview에 좌표 속성을 추가하고 고정 좌표를 전달하게 preview를 업데이를 해준다.
그리고 현재 좌표를 기반으로 하는 지역 계산을 트리거해주는 opAppear 수정자를 map에 추가한다.
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뷰와 List뷰에 각각 Landmark 속성과 현재 랜드 마크를 대상 LandmarDetail에 전달하도록 한다.
var landmark: Landmark // Detailview 에 추가
LandmarkDetail(landmark: landmark) // Listview 에 추가
마지막으로 LandmarkDetail 파일에서 필요한 데이터를 사용자 정의 유형에 전달해보자. 그리고 vstack이었던 ScrollView로 변경하여 사용자가 콘텐츠를 스크롤할 수 있도록 해 주고 NavigtionTitle() 수정자를 호출해서 세부 정보 보기를 표시할 때 제목을 출력하게 뜸하고
navigationBarTitleDisplayMode() 수정자를 또 추가적으로 호출해 제목을 inline형태로 표시한다.
ScrollView { //변경 된 내용
MapView(coordinate: landmark.locationCoordinate)
.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) // 추가 된 내용
}
}
여기까지가 오늘의 개발 내용이다. 여기서부터는 기본 뷰를 다룬다기보다는 JSon파일을 이용하여 데이터를 다루는 방법부터 List와 Navigation을 통해 이미지와 데이터를 출력하는 방식을 학습하였다. 생각보다 꼼꼼히 보면 공부할 내용이 많기 때문에 지속적인 학습이 필요해 보인다.
learning by repetition