Lecture 1과 Lecture 2를 시청하고 주어진 조건에 맞게 Memorize 앱을 작성하면 된다. 열 가지 Required Tasks와 두 가지 Extra Credit이 주어진다.
정확한 조건은 CS193p 홈페이지에 올라와있다.
Required Tasks
주 내용은 세 개의 테마 버튼을 생성하여 테마 버튼을 누를 때마다 해당 테마의 이모지로 카드를 바꾸는 것이다.
여기서 포인트는 이모지의 순서를 랜덤 하게 배치하는 것.
따라서 3개의 이모지 배열을 생성. 버튼을 누를 때마다 @State 선언된 emojis 배열을 해당 배열로 교체하는 것으로 구성했다.
코드의 재사용을 위해 함수를 작성하였다.
@State var emojis = vehicles
@State var emojiCount = Int.random(in: 4..<vehicles.count)
func replaceCard(to arr:[String]){
emojis = arr.shuffled()
emojiCount = Int.random(in: 4..<arr.count)
}
함수가 수행될 때마다 emojis 배열이 변경이 되고, 변경이 될 때마다 swift는 교체된 배열을 가지고 새로운 View를 그리게 된다.
여기서 Tasks에 주어진 랜덤 한 순서의 이모지는 Array에 정의되어 있는 shuffled() 함수를 통해 만족하였다.
emojiCount에서 Int.random(in: ) 메서드를 사용했는데, 이 부분은 Extra Credit 부분에서 설명하겠다.
HStack{
Button(action: { replaceCard(to: vehicles) }) {
Theme(title: "Vehicles", symbol: "car")
}
Button(action: { replaceCard(to: flowers) }) {
Theme(title: "Flowers", symbol: "camera.macro")
}
Button(action: { replaceCard(to: animals) }) {
Theme(title: "Animals", symbol: "pawprint")
}
}
struct Theme: View{
let title: String
let symbol: String
var body: some View{
VStack{
Image(systemName: symbol)
.font(.largeTitle)
Text(title)
}
.font(.title3)
}
}
다음으로 작성한 테마 View 구조체. 처음엔 3가지 버튼을 따로 작성하였다가 코드가 지저분해져서 Theme 구조체를 따로 만들었다.
title에는 화면에 보일 테마 이름, symbol은 화면에 보일 테마 이미지를 SF Symbol에 해당하는 이름을 입력받았다.
Extra Credit
첫 번째 조건은 테마선택 시 카드의 개수를 랜덤으로 배치하는 것이다.
이때 Int 타입에 정의된 Random(in: ) 타입 메서드를 사용하라고 명시되어 있다.
random(in:)
Returns a random value within the specified range.
범위를 입력받는 인자를 통해서 명시된 범위 내의 랜덤한 Int를 반환하게 된다.
명시된 조건은 최소 4개의 카드를 보이는 것이기에 4.. <배열크기를 넘겨주었다.
두 번째 조건은 바로 화면 크기에 맞추어 카드의 크기를 결정하는 것.
중요한 점은 모든 카드가 배치되었을 때 화면 범위를 벗어나지 않게 끔 적절한 카드 크기를 찾아내는 것이다. (화면이 portrait 상태)
여기서 이번 Memorize 앱을 작성하는 데에 가장 긴 시간을 고민했다.
Lecture 2에서 작성된 코드는 LazyVgrid를 통해 고정된 크기의 카드를 나열하였다. 여기서 카드의 수량이 추가되면 카드목록이 화면에 한 번에 잘릴 수 있으니 스크롤 뷰를 통해 카드목록을 탐색할 수 있게 구현하였다.
이번에는 카드의 수량에 맞춰 카드의 크기를 조절해 스크롤 없이 카드목록을 볼 수 있게끔 카드의 크기를 계산하는 widthThatBestFits(cardCount: Int) -> CGFloat 함수를 작성하는 것이 두 번째 조건이었다.
기기가 portrait(세로 방향) 상태일 경우에만 맞추면 된다고 명시되어 있길래 여기서 큰 힌트를 얻었다.
카드의 비율은 2:3, 여태 출시된 모든 아이폰의 화면 비율은 카드의 비율보다 세로비율이 더 길게 되어있다. 즉 상단의 제목과 하단의 테마버튼 영역을 제외하고서라도 카드를 정방 배열 형태(2*2, 3*3, 4*4)로 그리드 뷰를 구성한다면 카드와 동일한 2:3의 비율의 그리드뷰를 얻을 수 있고, 해당 뷰를 화면에 넘치지 않게끔 배치가 가능할 것이라 생각하였다.
따라서 LazyVgird의 너비를 랜덤 하게 주어지는 (카드목록의 수의 제곱근)으로 나눈 값을 LazyVgird에 들어갈 GridItem의 최소 크기로 지정한다면 SwiftUI는 정방배열의 LazyVgird를 그려내게 된다.
func widthThatBestFits(for width: CGFloat, cardCount: Int) -> CGFloat{
return width/ceil(sqrt(CGFloat(cardCount)))-10
}
따라서 특정 너비와 카드의 수를 입력받아 (카드 수의 제곱근)의 반올림 값으로 특정 너비를 나누는 widthThatBestFits 함수를 구현하였다. 저기서 10을 빼준 것은 카드 간의 간격의 여유를 위해 뺀 것이다.
이어서 LazyVgrid의 너비값을 어떻게 가져와야 할지 검색을 해본 결과 GeometryReader라는 Container View를 찾게 되었다.
GeometryReader
A container view that defines its content as a function of its own size and coordinate space.
GeometryReader{ geometry in
ScrollView{
let width = geometry.size.width
}
}
하위뷰의 좌표 정보와 크기 정보를 가져오는 Container View이며 예시와 같은 형태로 size에 정의된 width와 height 속성을 통해 하위 뷰의 너비와 높이에 접근할 수 있다.
GeometryReader{ geometry in
ScrollView{
let width = widthThatBestFits(for: geometry.size.width, cardCount: emojiCount)
LazyVGrid(columns: [GridItem(.adaptive(minimum: width))], spacing: 7){
ForEach(emojis[0..<emojiCount], id: \.self) { emoji in
CardView(content:emoji)
.aspectRatio(2/3, contentMode: .fit)
}
}
}
.foregroundColor(.red)
}
따라서 다음과 같이 GridItem의 최소 크기를 지정하였고, 예상대로 정방 형태의 그리드 뷰를 생성함으로써 portrait 상태에서는 어떠한 기기이든 스크롤 없이 주어진 카드의 전체를 확인할 수 있게 되었다.
이렇게 작성된 ContentView.swift 파일의 전체 코드는 다음과 같다.
'iOS > Stanford' 카테고리의 다른 글
[Lecture 3] MVVM and the Swift type system - 2 (0) | 2023.07.12 |
---|---|
[Lecture 3] MVVM and the Swift type system - 1 (0) | 2023.07.08 |
[Lecture 2] Learning more about SwiftUI (0) | 2023.07.05 |
[Lecture 1] Getting Started with SwiftUI - 2 (0) | 2023.06.22 |
[Lecture 1] Getting Started with SwiftUI - 1 (0) | 2023.06.16 |