WWDC2022 student challenge (2)
먼저 다른 앱들에서도 많이 사용되는 카테고리 버튼이 있는 스크롤뷰를 구현해야겠다고 생각했다.
사용자 입장에서 당연하게 사용하던 기능들을 막상 코드로 작성해서 내가 구현해보려고 하면 은근히 어렵다는걸 다들 느꼈을 것이다.
나도 스위프트 언어는 접해본지 한달밖에 안되서 당연히 사용하던 기능을들 구현하는데 많은 어려움이 있었다.
이런 기능들을 깔끔한 코드로 작성해서 튜토리얼 같은 느낌으로 WWDC를 제출하고 싶었다.
어떤 주제로 카테고리 리스트를 만들지 많이 고민했다.
1. 처음엔 애플의 제품들(iPad, Mac, iPhone, Accessory등)을 카테고리 버튼의 주제로 선정했는데, 버튼 아래에 표시될 이미지를 구하고 사용하는데 어려움이 있어서 주제를 바꿨다.
2. 다음으로 세계의 다양한 음식문화들에 대해 만들어 봐야겠다 생각이 들었다. 대륙을 카테고리 버튼의 주제로 잡고 리스트에 각 나라의 전통 음식들을 알려줄 생각이었다. 근데 이거도 마찬가지로 다양한 음식사진들의 저작권이 문제였다. 3~40개 나라를 정리하고 대표음식 이름을 다 정리해놨는데, 세번째 나라의 음식을 찾던중 나시고렝 사진을 구하는 과정에서 GG를 치게됬다.
최종적으로 나라별 전통음식에서 조금 바뀐 각 나라별 인구나 면적, 주 언어, 화폐단위를 알려주는 앱을 만들기로 했다.
핵심은 정보전달이라기 보다 이런 기능은 SwiftUI에서 이렇게 사용하면 되는구나 를 알려주는게 목적이었기 때문에 최대한 클린코드로 작성하려고 노력했다.
첫 화면은 대륙별 정보를 알아볼 수 있게 LazyVGrid를 사용했다.
import SwiftUI
struct MainView: View {
private static let initialColumns = 2
@State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns)
@State private var continentList = ["Asia", "Europe", "South America", "North America", "Africa", "Middle East"]
@State var currentIndex = 0
@State var isListViewActive = false
var body: some View {
LazyVGrid(columns: gridColumns) {
ForEach(continentList, id: \.self) {
value in
Button(action: {
currentIndex = continentList.firstIndex(of: value)!
isListViewActive = true
}) {
Text(value)
.foregroundColor(.white)
.font(.system(size: 35))
.frame(width: 250, height: 250)
.background(RoundedRectangle(cornerRadius: 20)
.fill(.blue))
NavigationLink(destination: ListView(continent: value, currentIndex: $currentIndex, continentList: $continentList),
isActive: $isListViewActive)
{ EmptyView() }
}
.padding()
}
}
}
}
currentIndex를 설정하고 값을 바인딩하는 이유는 다른 앱들과 달리 리스트화면으로 넘어갈 방법이 많이 때문이다. 예를들어 메인뷰에서 아시아를 누르면 바로 아시아 리스트가 선택되고 보여지게끔 구현했다. 아프리카를 눌러서 리스트뷰로 들어가도 아프리카가 선택되고 아프리카 리스트가 보여진다.
struct CategoryScrollView: View {
@Binding var currentIndex: Int
@Binding var continentList: [String]
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(continentList, id: \.self) { value in
Button(action: {
currentIndex = continentList.firstIndex(of: value)!
}) {
continentList.firstIndex(of: value)! == currentIndex ? Text(value)
.asCategoryModifier()
.foregroundColor(.white)
.background(RoundedRectangle(cornerRadius: 30).fill(.blue))
: Text(value)
.asCategoryModifier()
.foregroundColor(.blue)
.background(RoundedRectangle(cornerRadius: 30).fill(.white))
}
.padding(EdgeInsets(.init(top: 5, leading: 10, bottom: 5, trailing: 10)))
}
}
}
}
}
Text modifier로 중복되는 코드를 방지하기 위해 asCategoryModifier를 새로운 modifier로 만들어 사용했다.
struct CategoryTextModifier: ViewModifier {
func body(content: Content) -> some View {
content
.font(.system(size: 30))
.padding()
.overlay(RoundedRectangle(cornerRadius: 30)
.stroke(.blue, lineWidth: 3))
}
}
extension View {
func asCategoryModifier() -> some View {
modifier(CategoryTextModifier())
}
}
아래에 표시되는 리스트도 카테고리 버튼을 클릭했을때 currentIndex의 값이 바뀌면서 같이 바뀌게 된다.
import SwiftUI
struct CustomGroupBox: GroupBoxStyle {
func makeBody(configuration: Configuration) -> some View {
VStack(alignment: .leading) {
configuration.label
configuration.content
}
.padding(.vertical, 20)
}
}
struct CountryListView: View {
var countryList = Country.all()
@Binding var currentIndex: Int
@Binding var continentList: [String]
var body: some View {
List(self.countryList, id: \.name) { country in
country.continent == continentList[currentIndex] ?
NavigationLink(destination: CountryInfoView(countryInfo: country)) {
GroupBox(label: Text(country.name).font(.system(size: 30)), content: {
Image(country.imageURL)
.resizable()
.scaledToFit()
})
.groupBoxStyle(CustomGroupBox())
}
: nil
}
}
}
전체 데이터는 따로 파일을 만들어 extension으로 정리해뒀다.