SwiftUI로 코로나 현황 앱 만들기 Part 8 - CountryDetailView
이 글에 사용된 버전: Xcode 13.4, Swift 5.6
Part 7에서는 국가 리스트의 각 줄을 별도의 타입인 CountriesListRow
로 만들어서 표시하였다. Part 7까지의 코드는 여기에서 찾을 수 있다.
이번 파트에서는 앞 파트에서 만든 리스트의 줄을 터치하여 각 국가별 디테일한 정보를 제공하는 화면으로 이동하는 기능을 추가하고 그리고 국가 디테일 화면을 만들어 볼 것이다.
NavigationLink
내비게이션 뷰의 가장 큰 기능은 내비게이션 바를 통해서 여러 화면 간 왔다 갔다 할 수 있는, 즉 말 그대로 내비게이션 기능이다. 이 내비게이션 기능을 이용하여 이동할 수 있는 화면들의 집합을 내비게이션 스택이라고 한다. SwiftUI에서는, 다른 대부분의 기능들이 그러했듯, 내비게이션 기능도 간단하게 구현할 수 있다.
내비게이션 기능 구현을 위해서는 탭 제스처를 받는 뷰가 해당하는 부분 코드를 NavigationLink
블록으로 감싸기만 하면 된다. 우리 앱에서는 국가 리스트의 줄에 해당하는 CountriesListRow
를 터치하여 이동할 것이므로 이 뷰를 NavigationLink
블록 안으로 넣으면 된다. CountriesView.swift
파일로 가서 ForEach
블록 안을 아래와 같이 수정한다.
struct CountriesView: View {
...
List {
ForEach(viewModel.searchedResults) { country in
NavigationLink {
Text(country.country)
} label: {
CountriesListRow(
name: country.country,
continent: country.continent,
population: country.population,
url: country.countryInfo.flag
)
}
}
}
.listStyle(.plain)
.searchable(text: $viewModel.searchTerm)
...
}
NavigationLink
의 첫 번째 블록은 이동하고자 하는 곳, 즉 destination
에 해당하고, 두 번째 블록은 내비게이션 링크 버튼이 어떻게 보일지에 해당하는 레이블이 되며, 여기서는 CountriesListRow
가 된다. 목적지 뷰는 아직 만들지 않았기에 우선은 화면에 나라 이름이 표시되도록 Text(country.country)
를 건넸다.
이제 앱을 실행하면 리스트 각 줄 끝에 더 이동할 곳이 있다는 표시로 >
마크가 보인다. 줄을 터치하면 새로운 화면으로 이동하고, 내비게이션 바에 되돌아오는 버튼을 터치하면 두 화면 간 이동이 가능한 것을 확인할 수 있다.
CountryDetailView
이제 각 나라의 디테일이 표시되는 화면을 만들 차례이다. 먼저 CountryDetail
이라는 새로운 그룹(폴더)을 만들고 그 안에 CountryDetailView.swift
라는 새로운 SwiftUI View 파일을 생성한다. 그런 다음 생성한 파일을 다음과 같이 수정한다.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
var body: some View {
Text(country.country)
}
}
이 뷰가 이니셜라이징 될 때 받을 파라미터로 Country
를 설정했다. 프리뷰 코드에서 에러가 발생할 텐데 이 에러를 해결하기 위해서 Country
의 더미 데이터 인스턴스를 하나 생성하도록 하자. Country.swift
파일로 가서 파일 끝에다가 아래 익스텐션을 추가한다.
// Country.swift
...
extension Country {
static var example: Country {
Country(
updated: 1653276487775,
country: "S. Korea",
countryInfo: CountryInfo(
iso2: "KR",
iso3: "KOR",
lat: 37,
long: 127.5,
flag: "https://disease.sh/assets/img/flags/kr.png"),
cases: 17967672,
todayCases: 9975,
deaths: 23987,
todayDeaths: 22,
recovered: 17414103,
todayRecovered: 42223,
population: 51352317,
continent: "Asia"
)
}
}
이제 CountryDetailView.swift
파일로 가서 프리뷰 코드를 아래와 같이 수정한다.
// CountryDetailView.swift
struct CountryDetailView_Previews: PreviewProvider {
static var previews: some View {
CountryDetailView(country: .example)
}
}
이렇게 하면 에러가 사라진 것을 확인할 수 있다. 이제 새롭게 만든 이 뷰로 이동할 수 있도록 앞에서 만든 NavigationLink 코드의 첫 번째 블록을 수정한다.
// CountriesView.swift
struct CountriesView: View {
...
List {
ForEach(viewModel.searchedResults) { country in
NavigationLink {
CountryDetailView(country: country)
} label: {
CountriesListRow(
name: country.country,
continent: country.continent,
population: country.population,
url: country.countryInfo.flag
)
}
}
}
...
이제 앱이 이전과 동일하게 작동하지만 이동하는 뷰는 우리가 별도로 만든 뷰로 이동하게 됐다.
MapKit
Swift에서 앱을 만들 때 애플 지도를 사용할 수 있도록 프레임워크를 제공하고 있는데 그 프레임워크가 바로 MapKit
이다. 이번에 우리가 만들 뷰는 한 나라의 디테일한 정보를 보여주는 뷰이므로, 그 나라가 위치한 지도를 보여주는 것도 의미가 있을 것으로 보인다. 마침 API에서 CountryInfo
부분에 위도(latitude)와 경도(longitude)도 제공하고 있기도 하니, MapKit
을 이용해서 지도를 표시해 보자.
지도를 만들 때 건네주어야 하는 파라미터가 복잡한 편이어서 지도 뷰를 별도의 타입으로 정의하는 것이 더 나을 것으로 보인다. CountryMapView.swift
라는 이름으로 새로운 SwiftUI View 파일을 만들고 다음과 같이 작성하자.
// CountryMapView.swift
import SwiftUI
import MapKit
struct CountryMapView: View {
let country: Country
@State private var region: MKCoordinateRegion
init(country: Country) {
self.country = country
let meters: Double = 1_500_000
let coordinate = CLLocationCoordinate2D(
latitude: country.countryInfo.lat,
longitude: country.countryInfo.long
)
_region = State(
initialValue: MKCoordinateRegion(
center: coordinate,
latitudinalMeters: meters,
longitudinalMeters: meters
)
)
}
var body: some View {
Map(coordinateRegion: $region)
}
}
struct CountryMapView_Previews: PreviewProvider {
static var previews: some View {
CountryMapView(country: .example)
.frame(height: 200)
}
}
SwiftUI에서 지도를 추가하는 것은 매우 간단하다. MapKit
을 import하고(위쪽에 MapKit을 import 하는 것을 잊지 말자), Map
뷰를 생성하기만 하면 된다. 다만 이 과정에서 Map
이 MKCoordinateRegion
타입의 파라미터를 받는데 바인딩 형태로 받기 때문에 @State
가 붙은 프로퍼티가 필요하다. MKCoordinateRegion
은 이니셜라이저에서 CLLocationCoordinate2D
타입의 파라미터를 받아야 하는데, 이 타입을 이니셜라이징 하려면 latitude
와 longitude
값을 알아야 한다. 문제는 이 맵 뷰 타입이 이니셜라이징 되기 전까지는 해당 값을 모르기 때문에, 지금까지와 같이 @State
프로퍼티에 초깃값을 부여하는 형태로 할 수가 없다는 것이다. 그래서 이 문제를 해결하기 위해 커스텀 이니셜라이저를 만들고, 그 안에서 초깃값을 부여하였다.
이렇게 만든 맵 뷰는 MKCoordinateRegion
에서 설정한 위도와 경도에 해당하는 지역을 지도 한가운데 표시하며 화면에 나타난다. latitudalMeters
와 longitudalMeters
는 지도가 보여주는 남북 간 그리고 동서 간 거리를 나타내는 것으로 적당한 크기로 보여주기 위해서 1500km(1,500,000미터) 값을 부여했다. 이 값이 너무 크면 작은 나라는 너무 작게 보일 테고, 너무 작으면 큰 나라는 일부만 보이게 된다. 만약 API에서 면적이나 영토 크기에 대한 정보도 제공해 준다면 그 값을 기반으로 저 값을 조정할 수도 있겠지만 해당 정보는 API에서 제공하지 않기 때문에 임의의 값을 사용할 수밖에 없다. 1,500km 정도라면 중국이나 러시아와 같은 큰 나라는 한눈에 들어오지 않겠지만 대부분의 나라를 커버할 수 있을 것으로 생각된다.
이제 우리가 만든 맵 뷰가 잘 작동하는지 보려면 프리뷰를 통해 확인하면 된다. 맵 뷰는 프리뷰를 실행해야만 지도가 제대로 나타난다.
지도는 잘 나타나는데 조금 밋밋한 느낌이 있다. 그리고 여러 국가가 섞여 있는 지역이라면 우리가 확인 중인 나라가 정확히 어디에 위치해 있는지 파악하기 힘들 수도 있다. 이럴 때 사용할 수 있는 것이 어노테이션(annotation)이다.
어노테이션은 간단히 말해서 지도에 표시를 하는 것이다. 핀으로 표시한다든지, 혹은 노트가 될 수도 있다. 애플 맵에서 사용하는 MapMarker
, MapPin
등의 타입이 이에 해당하고, 커스텀으로 만들 수도 있는데 여기서는 그냥 풍선 모양의 MapMarker
를 사용하겠다.
MapMarker
를 사용하기 위해서는 Map
타입의 annotationItems
파라미터에 어레이 형태로 Identifiable
프로토콜을 채택한 타입을 건네주어야 한다. 파라미터를 어레이 형태로 받는 건 여러 장소를 동시에 표시할 수도 있기 때문이다.
일단 annotationItems
를 위해 여기에 사용할 별도의 타입을 하나 만들자. CountryMapView.swift
에서 CountryMapView
와 프리뷰 코드 사이에 아래와 같이 익스텐션을 작성한다. 이 타입은 이 뷰 외에 다른 곳에서는 사용하지 않을 것이므로, 타입 안에 타입을 정의하는 Nested Type 형태로 정의하였다.
// CountryMapView.swift
...
extension CountryMapView {
struct Location: Identifiable {
let id = UUID()
let coordinate: CLLocationCoordinate2D
}
}
이 타입을 CountryMapView
타입 내에서 사용하기 위해 프로퍼티를 하나 새로 생성한다.
// CountryMapView.swift
struct CountryMapView: View {
...
let locations: [Location]
@State private var region: MKCoordinateRegion
...
}
이제 이니셜라이저를 다음과 같이 수정한다.
// CountryMapView.swift
struct CountryMapView: View {
...
init(country: Country) {
self.country = country
let meters: Double = 1_500_000
let coordinate = CLLocationCoordinate2D(
latitude: country.countryInfo.lat,
longitude: country.countryInfo.long
)
locations = [Location(coordinate: coordinate)]
_region = State(
initialValue: MKCoordinateRegion(
center: coordinate,
latitudinalMeters: meters,
longitudinalMeters: meters
)
)
}
...
}
마지막으로 body
안을 아래와 같이 수정한다.
// CountryMapView.swift
struct CountryMapView: View {
...
var body: some View {
Map(coordinateRegion: $region, annotationItems: locations) { location in
MapMarker(coordinate: location.coordinate)
}
}
}
이제 마커가 제대로 표시되는 것을 확인할 수 있다.
GeometryReader
이제 앞에서 만든 지도뷰를 가지고 CountryDetailView
의 헤더 부분을 만들어보자. 헤더 부분은 그 전체를 지도가 차지할 텐데, 이 지도를 화면의 약 1/4 크기 정도로만 배치하려고 한다. 하지만 아이폰SE의 크기와 13 프로 맥스의 크기가 각각 다른데, 어떻게 하면 각기 다른 화면 크기에서도 동일하게 높이가 1/4이 되도록 할 수 있을까? 이 경우 사용할 수 있는 것이 GeometryReader
타입이다.
GeometryReader
는 그 안에 뷰를 배치할 수 있는 컨테이너 뷰에 해당하는데, GeometryReader
가 속해 있는 뷰, 즉 parent 뷰에서의 좌표와 GeometryView
의 사이즈 데이터를 제공한다. 우리는 좌표 값은 사용하지 않고 크기 정보만 이용할 예정이고, 이 크기 정보를 이용해서 지도가 화면에서 세로로 1/4 크기만 차지하도록 할 수 있다. 이제 CountryDetailView.swift
파일로 가서 아래와 같이 수정한다.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
CountryMapView(country: country)
.frame(height: geometry.size.height / 4)
}
}
}
.navigationTitle(country.country)
.navigationBarTitleDisplayMode(.inline)
}
}
CountryDetailView
가 전체 화면을 차지할 것이기 때문에 body
프로퍼티 안을 GeometryReader
로 시작하여 전체 화면의 크기를 알 수 있도록 하였다. 화면 내의 정보가 화면 크기를 초과할 수도 있기 때문에 스크롤이 가능하도록 ScrollView
도 포함하고, 마지막에 CountryMapView
에 .frame(height:)
모디파이어로 GeometryReader
가 제공하는 높이 값의 1/4 값을 적용하였다. 이렇게 함으로써 지도가 화면의 1/4 높이로 표시되는 것을 확인할 수 있다.
.navigationBarTitleDisplayMode(.inline)
부분은 내비게이션 바 제목 부분이 크게 나오지 않고 작게 나오도록 조절하는 코드이다. 애플에서는 내비게이션 바 제목을 활용할 때 첫 화면은 큰 제목으로 그 안에서 이동해 가는 화면들은 연속성을 위해 특별한 경우가 아니면 작은 제목을 활용하도록 권하고 있다.
이제 이 지도 위에 국기가 표시되도록 꾸며보자. VStack
안을 아래와 같이 수정한다.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
ZStack(alignment: .bottomTrailing) {
CountryMapView(country: country)
AsyncImage(url: URL(string: country.countryInfo.flag)) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.scaledToFit()
default:
Image(systemName: "flag")
}
}
.frame(maxWidth: geometry.size.width / 5)
.padding(24)
.shadow(radius: 4)
}
.frame(height: geometry.size.height / 4)
}
}
}
.navigationTitle(country.country)
.navigationBarTitleDisplayMode(.inline)
}
}
지도 위에 겹쳐서 국기가 표시되도록 ZStack
을 사용했다. AsyncImage
는 앞에서 사용한 것과 조금 다른 방식으로 사용하였는데, AsyncImage
의 너비를 화면 사이즈의 1/5만 차지하도록 GeometryReader
를 활용한 것을 확인해 보라. 마지막으로 그림자 효과를 조금 줘서 국기가 마치 지도 위에 떠 있는 듯한 분위기를 연출하였다.
이제 ZStack
아래에 국가명 등을 표시하도록 아래와 같이 HStack
을 하나 추가하여 헤더 부분을 마무리하자.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
ZStack(alignment: .bottomTrailing) {
...
}
.frame(height: geometry.size.height / 4)
HStack(alignment: .bottom) {
Text(country.country)
.font(.largeTitle)
.bold()
Spacer()
VStack(alignment: .trailing) {
Text(country.continent)
.font(.headline)
Label("\(country.population)", systemImage: "person.3")
}
.foregroundColor(.secondary)
}
.padding()
}
}
}
.navigationTitle(country.country)
.navigationBarTitleDisplayMode(.inline)
}
}
헤더 부분만으로도 코드가 꽤 길어져서 헤더를 별도의 타입으로 빼는 것이 좋을 것 같다. CountryDetailHeader.swift
라는 이름으로 SwiftUI View 파일을 만들어서 VStack
부분 코드를 복사해서 붙여 넣은 다음 아래와 같은 모습이 되도록 일부 수정한다.
import SwiftUI
struct CountryDetailHeader: View {
let country: Country
let geometry: GeometryProxy
var body: some View {
VStack {
ZStack(alignment: .bottomTrailing) {
CountryMapView(country: country)
AsyncImage(url: URL(string: country.countryInfo.flag)) { phase in
switch phase {
case .empty:
ProgressView()
case .success(let image):
image
.resizable()
.scaledToFit()
default:
Image(systemName: "flag")
}
}
.frame(maxWidth: geometry.size.width / 5)
.padding(24)
.shadow(radius: 4)
}
.frame(height: geometry.size.height / 4)
HStack(alignment: .bottom) {
Text(country.country)
.font(.largeTitle)
.bold()
Spacer()
VStack(alignment: .trailing) {
Text(country.continent)
.font(.headline)
Label("\(country.population)", systemImage: "person.3")
}
.foregroundColor(.secondary)
}
.padding()
}
}
}
struct CountryDetailHeader_Previews: PreviewProvider {
static var previews: some View {
GeometryReader { geometry in
CountryDetailHeader(country: .example, geometry: geometry)
}
}
}
이제 이 타입을 활용하여 CountryDetailView
를 아래와 같이 간단하게 수정한다.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
CountryDetailHeader(country: country, geometry: geometry)
}
}
}
.navigationTitle(country.country)
.navigationBarTitleDisplayMode(.inline)
}
}
복잡한 코드를 별도의 타입으로 빼고, 그걸 이용하여 심플하게 정리하는 이 순간이 코딩을 하면서 가장 즐거운 순간 중 하나가 아닐까 싶다.
LazyVGrid
이제 특정 국가의 구체적인 정보를 어떻게 표시할 것인지가 남았다. WorldwideView
에서처럼 리스트 형태로 표시할 수도 있겠지만, 몇 안 되는 화면으로 이루어진 앱에서는 같은 화면이 반복되면 좀 지루할 것 같다. 그래서 여기서는 좀 다른 방법을 사용해 보려고 한다.
리스트 형태가 아니더라도 비슷한 종류의 데이터 여러 개를 효과적으로 보여주는 방법으로 그리드 형태가 있다. 마침 SwiftUI에서 그리드 형태로 뷰를 배치할 수 있는 컨테이너 뷰가 있는데 바로 LazyVGrid
와 LazyHGrid
이다. 이름에서 유추할 수 있듯이 V는 수직으로 뷰가 추가되는 형태이고, H는 수평으로 뷰가 추가된다. 아이폰 화면에서는 아무래도 수직 스크롤링이 익숙하기 때문에 여기서는 LazyVGrid
를 사용하겠다. 그리고 이름에 lazy가 있는 것은 데이터가 아주 많을 때 모든 데이터를 한 번에 준비하는 것이 아니고 화면에 나타날 때쯤, 즉 필요해질 때 준비한다는 의미이며 이를 통해서 데이터가 많을 때는 디바이스의 자원을 효율적으로 활용할 수 있게 된다. 물론 우리 앱에서는 데이터 개수가 몇 개 되지 않아서 lazy로 작동하지는 않을 것으로 보인다.
이제 CountryDetailView.swift
파일로 가서 아래와 같이 수정한다.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
let columns = [GridItem(), GridItem()]
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
CountryDetailHeader(country: country, geometry: geometry)
LazyVGrid(columns: columns) {
VStack(spacing: 8) {
Text("Cases")
.font(.headline)
Text(country.cases, format: .number)
.font(.title2)
.bold()
Text("+\(country.todayCases)")
.foregroundColor(.secondary)
}
.padding()
.frame(maxWidth: .infinity)
.background(Color(white: 0.95))
.cornerRadius(8)
}
.padding()
}
}
}
.navigationTitle(country.country)
.navigationBarTitleDisplayMode(.inline)
}
}
columns
라는 프로퍼티를 위쪽에 추가하였는데, 이 프로퍼티가 LazyVGrid
에 파라미터로 전달된다. 위에서는 GridItem
이 두 개 담긴 어레이 형태로 정의하였는데 이렇게 하면 세로로 2개 열이 있는 그리드가 된다. GridItem
을 기본 값으로 이니셜라이징하게 되면 .flexible()
이 적용되는데, 이 외에도 다양한 크기를 부여할 수 있는데 실험은 독자 여러분에게 맡긴다. LazyVGrid
안에는 그리드가 표시할 뷰를 넣었다. 이렇게 작성하면 다음과 같이 프리뷰에 표시된다.
이제 그리드 안의 뷰를 더 작성하여야 하는데 이 부분은 코드가 반복되는 부분이기 때문에 별도의 타입으로 정의할 필요가 있다. CountryDetailGridItem.swift
라는 파일 이름으로 새로운 SwiftUI View 파일을 생성한 다음 LazyVGrid
안의 VStack
부분을 복사하여 붙여 넣고, 아래와 같이 수정한다.
// CountryDetailGridItem.swift
import SwiftUI
struct CountryDetailGridItem: View {
let title: String
let number: Int
let today: Int?
var body: some View {
VStack(spacing: 8) {
Text(title)
.font(.headline)
Text(number, format: .number)
.font(.title2)
.bold()
if let today = today {
Text("+\(today)")
.foregroundColor(.secondary)
} else {
Text("No Data")
.foregroundColor(.clear)
}
}
.padding()
.frame(maxWidth: .infinity)
.background(Color(white: 0.95))
.cornerRadius(8)
}
}
struct CountryDetailGridItem_Previews: PreviewProvider {
static var previews: some View {
CountryDetailGridItem(title: "Case", number: 17967672, today: 9975)
.previewLayout(.sizeThatFits)
}
}
여기서 한 가지 생각해 보아야 하는 점은 최종 확진자 수 외에 오늘 추가 확진자 수와 같은 데이터는 항상 존재하지는 않는다는 것이다. 그래서 이 부분은 옵셔널로 정의하고 뷰 또한 언랩을 통해서 데이터가 있을 때만 표시되도록 하였고, 만약 데이터가 없는 경우 높이를 맞추기 위해서 더미 텍스트를 투명한 색깔로 표시되도록 하였다. 이제 이 타입을 이용하여 CountryDetailView
를 아래와 같이 수정한다.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
let columns = [GridItem(), GridItem()]
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
CountryDetailHeader(country: country, geometry: geometry)
LazyVGrid(columns: columns) {
CountryDetailGridItem(
title: "Cases",
number: country.cases,
today: country.todayCases
)
CountryDetailGridItem(
title: "Deaths",
number: country.deaths,
today: country.todayDeaths
)
CountryDetailGridItem(
title: "Recovered",
number: country.recovered,
today: country.todayRecovered
)
}
.padding()
}
}
}
.navigationTitle(country.country)
.navigationBarTitleDisplayMode(.inline)
}
}
화면으로는 이렇게 표시된다.
짝이 맞지 않아서 데이터를 하나 더 추가하도록 하자. Country.swift
파일로 가서 active
라는 Int
프로퍼티를 하나 추가한다.
struct Country: Decodable {
...
let todayRecovered: Int
let active: Int
...
}
이제 CountryDetailView
로 가서 그리드 아이템을 하나 더 추가한다.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
let columns = [GridItem(), GridItem()]
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
CountryDetailHeader(country: country, geometry: geometry)
LazyVGrid(columns: columns) {
...
CountryDetailGridItem(
title: "Active",
number: country.active,
today: nil
)
}
.padding()
}
}
}
.navigationTitle(country.country)
.navigationBarTitleDisplayMode(.inline)
}
}
마지막으로 그리드 뷰도 별도의 타입으로 빼도록 하자. CountryDetailInfoGrid.swift
라는 파일명으로 새로운 SwiftUI View 파일을 생성하고 LazyVGrid
부분을 복사해서 붙여 넣고 아래와 같이 수정한다.
// CountryDetailInfoGrid.swift
import SwiftUI
struct CountryDetailInfoGrid: View {
let country: Country
let columns = [GridItem(), GridItem()]
var body: some View {
LazyVGrid(columns: columns) {
CountryDetailGridItem(
title: "Cases",
number: country.cases,
today: country.todayCases
)
CountryDetailGridItem(
title: "Deaths",
number: country.deaths,
today: country.todayDeaths
)
CountryDetailGridItem(
title: "Recovered",
number: country.recovered,
today: country.todayRecovered
)
CountryDetailGridItem(
title: "Active",
number: country.active,
today: nil
)
}
.padding()
}
}
struct CountryDetailInfoGrid_Previews: PreviewProvider {
static var previews: some View {
CountryDetailInfoGrid(country: .example)
.previewLayout(.sizeThatFits)
}
}
이제 CountryDetailView
로 가서 columns
프로퍼티를 삭제하고 아래와 같이 수정한다.
// CountryDetailView.swift
struct CountryDetailView: View {
let country: Country
var body: some View {
GeometryReader { geometry in
ScrollView {
VStack {
CountryDetailHeader(country: country, geometry: geometry)
CountryDetailInfoGrid(country: country)
}
}
}
.navigationTitle(country.country)
.navigationBarTitleDisplayMode(.inline)
}
}
이렇게 해서 CountryDetailView
화면이 완성되어서 Part 8도 여기에서 마친다. Part 8에서는 지금까지...
NavigationLink
를 활용하여 내비게이션 스택에서 화면 간 이동 구현MapKit
을 활용하여 지도 표시GeometryReader
로 화면의 사이즈를 읽어들이고 이를 UI 구현에 활용LazyVGrid
를 통해 그리드 형태의 UI 구현
을 진행해 보았다. Part 8까지의 파일은 여기에서 찾을 수 있다.
마치며
Part 1에서 8까지 이르는 긴 여정을 통해서 간단하게나마 하나의 앱이 완성되었다. 아이팟 터치와 같은 작은 기기의 화면으로 보면 폰트 크기 조절 등 소소하게 수정해 줘야 하는 부분들이 보이는데 이 부분은 독자 여러분에게 맡기려고 한다. WWDC22에서 그래프를 그릴 수 있는 프레임워크인 Swift Charts가 발표되었는데 이번 가을에 iOS 16이 정식 버전으로 나오면 오늘 만든 화면에 그래프도 넣어볼 예정이다. 그때까지 이 프로젝트는 넣어두고 다시 새로운 포스트를 통해 돌아오도록 하겠다.