첫 튜토리얼에서는 사용자에게 보여줄 정보를 하드코딩 했다.
이번 튜토리얼에서는 데이터를 저장하는 모델을 만들어서 사용한다.
https://developer.apple.com/tutorials/swiftui/building-lists-and-navigation
우선 프로젝트 파일을 다운받아 리소스폴더의 landmarkData.json을 프로젝트에 드래그 앤 드랍한다.
그리고 Landmark라는 이름의 Swift파일을 생성하고 구조체를 만들어 준다.
struct Landmark: Hashable, Codable{
var id: Int
var name: String
var park: String
var state: String
var description: String
private var imageName: String
//imageName으로부터 이미지를 로드하는 연산 프로퍼티
var image: Image{
Image(imageName)
}
// locationCoordinate 연산프로퍼티를 위해서만 사용되기 때문에, 직접 접근할 일이 없으므로 private
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파일로 부터 데이터를 읽어와 Landmark구조체 형식의 배열로 반환해주는 ModelData파일을 생성한다.
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 let error{
fatalError("Couldn't load \(filename) from main bundle: \n\(error)")
}
do{
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch let error{
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
코드 내부에는 json데이터를 가져와 제네릭 타입으로 반환하는 load메서드가 있다.
load메서드는 반환타입이 Codable prodocol을 채택하는 형태로 반환 되어야 한다. 그래서 Landmark구조체가 Codable을 채택한 것.
이제 load메서드 위에 landmark를 파싱한 결과물을 담을 landmarks: [Landmark]를 만들어 load를 호출한다.
List의 RowView를 만든다
LandmarkRow.swift 파일을 생성한다.
body 위에 var landmark: Landmark 프로퍼티를 추가한다.
프로퍼티를 추가하고 나면 Preview코드에서 에러가 난다.
프리뷰 코드 내부에서 LandmarkRow를 생성해서 사용해야 하는데 방금 landmark프로퍼티를 추가했으니 landmark내용이 들어가야해서 생기는 에러다.
row의 UI는 보통 수평으로 컨텐츠가 놓이므로 H스택을 추가해주고 내용으론 landmark의 이미지와 name을 넣어준다
마지막엔 공간이 생기도록 Spacer()을 추가한다.
Customize the Row Preview
프리뷰를 Customizing을 통해 row사이즈로 보여지도록 수정한다.
프리뷰 코드에서 previewLayout을 설정해주면 원하는사이즈로 볼 수 있다. fixed를 통해 컨테이너의 사이즈를 고정한다.
하지만 프리뷰의 코드를 변경한 것이므로 프리뷰 화면에서만 적용된다.
LandmarkList만들기
LandmarkList라는 이름의 SwiftUI파일 하나를 생성한다.
아까 만든 LandmarkRow를 여러개 만들어 List로 감싸준다.
struct LandmarkList: View {
var body: some View {
List{
LandmarkRow(landmark: landmarks[0])
LandmarkRow(landmark: landmarks[1])
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
LandmarkList()
}
}
list의 요소를 개별적으로 지정하는 대신 컬렉션으로부터 직접 행을 생성할수 있다. 데이터 컬렉션과 컬렉션의 각 요소에 대한 view를 제공하는 클로저를 전달하여 컬렉션 요소를 표시하는 list를 만들 수 있다. 이 list는 제공된 클로저를 사용하여 컬렉션의 각 요소를 child view로 변환한다.
무슨말 이냐면 아래와 같이 구현이 가능하다는 말이다. 리스트의 요소를 landmarks[0], landmarks[1], landmarks[2] 처럼 하나 하나 구현하지 않고 아래와 같이 만들 수 있다는 말이다.
struct LandmarkList: View {
var body: some View {
List(landmarks){ landmark in
LandmarkRow(landmark: landmark)
}
}
}
이렇게 사용하려면 Landmark모델로 돌아가서 Identifiable프로토콜을 추가해야 한다.
Identifiable 프로토콜은 Hashable을 준수하는 연관타입(associatedtype)인 ID가 있고 이 ID를 타입으로 하는 id가 있는 단순한 구조로, identifiable프로토콜의 목적은 Hashable을 준수하는 id(식별자)를 구현하도록 하는 데 있다.
Landmark모델에는 이미 id 프로퍼티가 구현되어 있고 타입이 Int이기 때문에 Identifiable프로토콜을 채택만 해주면 된다.
struct Landmark: Hashable, Codable, Identifiable{
이제 각 행을 눌렀을 때 상세 뷰를 보여줄 것.
디테일한 정보를 보여주기 위한 LandmarkDetail.swift파일을 하나 만들어 준다
그 후 ContentView내용을 카피한 후 LandmarkDetail 프로퍼티에 넣어준다.
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) {
VStack {
Text("Turtle Rock")
.font(.title)
.fontWeight(.medium)
.foregroundColor(.green)
HStack {
Text("Josua Tree National Park")
.font(.subheadline)
Spacer()
Text("California")
.font(.subheadline)
}
.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는 다음과 같이 바꿔준다. ContentView에서 LandmarkList를 보여줄 것이기 때문이다.
import SwiftUI
struct ContentView: View {
var body: some View {
LandmarkList()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
다시 LandmarkList로 돌아와서 각 행을 눌렀을 때 LandmarkDetail뷰로 전환할 건데, push방식으로 전환하려면 기존에는 NavigationController를 embeded하고 있어야 했다. SwiftUI도 NavigationView를 embeded할 수 있고 modifier를 통해 타이틀 등 여러가지를 설정할 수 있다.
struct LandmarkList: View {
var body: some View {
NavigationView{
List(landmarks){ landmark in
NavigationLink(destination: LandmarkDetail()){
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
이제 detaiol view로 데이터를 넘겨주고 해당 데이터를 detailview에서 띄워주는 작업이 필요하다.
먼저 CircleImage를 다음과 같이 수정한다.
import SwiftUI
struct CircleImage: View {
var image: Image
var body: some View {
image
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
.shadow(radius: 7)
}
}
struct CircleImage_Previews: PreviewProvider {
static var previews: some View {
CircleImage(image: Image("turtlerock"))
}
}
MapView도 다음과 같이 수정한다.
import SwiftUI
import MapKit
struct MapView: View {
var coordinate: CLLocationCoordinate2D
@State private var region = MKCoordinateRegion()
var body: some View {
Map(coordinateRegion: $region)
//뷰가 보일 때 setRegion 메서드 실행
.onAppear{
setRegion(coordinate)
}
}
// mapView를 해당 좌표로 업데이트 하기 위한 메서드, 외부에서 접근할 일이 없기 때문에 private
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: -166.166_688))
}
}
Map의 ONappear(perform:)에 대한 설명은 다음과 같다.
Adds an action to perform when this view appears.
뷰가 보일 때 매번 실행되는 메서드로, 뷰 컨트롤러에서 ViewDidAppear()과 비슷한 역할을 하는 메서드라고 생각하면 된다.
LandmarkDetail에 Landmark프로퍼티를 하나 추가해주고,
struct LandmarkDetail: View {
var landmark: Landmark
...
...
}
LandmarkList로 가서 LandmarkDetail에 Landmark프로퍼티가 생겼으니 다음과 같이 이니셜라이저를 변경해준다.
struct LandmarkList: View {
var body: some View {
NavigationView{
List(landmarks){ landmark in
NavigationLink(destination: LandmarkDetail(landmark: landmark)){
LandmarkRow(landmark: landmark)
}
}
.navigationTitle("Landmarks")
}
}
}
마지막으로 LandmarkDetail로 돌아가서, 데이터를 바인딩 해주기 위해 다음과같이 코드를 변경해준다.
import SwiftUI
struct LandmarkDetail: View {
var landmark: Landmark
var body: some View {
// 사용자가 스크롤 가능하도록 VStack -> ScrollView로 변경
// MapView, CircleImage, Text 등등.. 데이터 넘겨주기
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)
.foregroundColor(.primary)
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)
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static var previews: some View {
LandmarkDetail(landmark: landmarks[0])
}
}