Explore how @State properties and buttons work to update the UI of your app by creating an app to roll virtual dice. Add the functionality to increase or decrease the number of dice on the screen to play different kinds of games.
Apple Tutorial
@State
프로퍼티와 Button
을 이용해 가상의 주사위를 굴리는 게임을 만들고 화면을 업데이트하기.
주사위를 추가하고 감소할 수 있게 만들기.
Tutorial View | My View |
---|---|
처음으로 코드를 쓰기 전 필요한 기능을 정리해보았다.
처음 코드를 작성할 땐 한 번에 굴리는 버튼을 만들 생각이 없었다.
자연스레 Roll버튼을 DiceView에 넣었는데 이렇게 하니 한 번에 굴리는 버튼을 만들기가 어려웠다. 추가로 모든 눈금의 합을 표시하기에도 각 View마다 값이 따로 저장되어있기 때문에 문제가 있었다.
그래서 상위 View에 눈금 값 배열을 만들고 각 요소값을 DiceView로 전달하는 방식으로 수정했다.
이를 통해서 ContentView에 모든 주사위 값이 저장되어 주사위 눈금 총합을 계산할 수도 있었다.
//
// ContentView.swift
// Dice
//
// Created by 박진홍 on 9/16/24.
//
HStack{
ForEach(1...someDice, id: \.self) {_ in
DiceView(someDice: $someDice)
}
}
//아래와 같이 바꿨다.
@State private var dice: [Int] = [1]
HStack{
ForEach(0...someDice, id: \.self) {num in
VStack{
DiceView(someDice: $someDice, diceNum: $dice[num])
Button {
dice[num] = Int.random(in:1...6)
} label: {
Text("Roll")
}
}
}
}
//이외에도 주사위 추가, 삭제 시 배열에 요소를 넣고 빼는 코드를 추가했다.
Cannot convert value of type 'ClosedRange<Int>' to expected argument type 'Range<Int>'
@State private var someDice: Int = 1 ForEach(1...someDice)
위 오류는 ForEach의 범위에 들어갈 수 있는 Type 차이에서 발생했다. id: \.self
를 넣어 해결할 수 있지만 내가 참고한 답변에선 메모리 누수가 발생할 수 있다는 의견이 있었다.
추가적으로 ...
대신 ..<
를 사용할 수 있었으나 변수를 최대값으로 넣었을 때 Non-constant range: argument must be an integer literal라는 경고가 나왔다.
.foregroundStyle()
에 2~3개의 색상을 설정할 수 있음을 알게 되었다. 처음엔 어떻게 흰 배경의 주사위를 만들어야 할지 몰라서 늘 하던 대로 ZStack
으로 배경을 쌓았는데 iPhone15로 구동되는 Preview에선 딱 맞아보였으나 테스트용 폰인 12 mini에선 배경과 주사위의 위치가 알맞지 않았었다.
modifier은 사용예시는 다음과 같다.
.foregroundStyle(<#T##primary: ShapeStyle##ShapeStyle#>, <#T##secondary: ShapeStyle##ShapeStyle#>, <#T##tertiary: ShapeStyle##ShapeStyle#>)
.white | .white, .black | .black, .white |
---|---|---|
이번 주사위 만들기 튜토리얼은 주사위가 늘어남에 따라 크기가 바뀌도록 하는 코드에 대해 고민했다.
주사위의 개수를 DiceView에서 전달받고 개수에 따른 frame의 크기를 switch-case
문으로 지정하였다.
이후 튜토리얼을 보니 .aspectRatio()
라는 modifier를 사용하여 간단하게 구현할 수 있는 코드였다.
심지어 더욱 깔끔하고 화면 크기가 달라도 자연스럽게 적용되었다.
//switch문과 연산프로퍼티를 이용
@Binding var someDice: Int
var diceSize: CGFloat {
switch someDice {
case 0...2: return 100
case 3: return 73
case 4: return 65
case 5: return 55
default: return 100
}
}
@Binding var diceNum: Int
var body: some View {
VStack{
Image(systemName: "die.face.\(diceNum).fill")
.resizable()
.foregroundStyle(.black, .white)
.frame(minWidth: 50,idealWidth: diceSize,maxWidth: diceSize,minHeight:50,idealHeight: diceSize,maxHeight: diceSize)
}
}
//.aspectRatio()이용
@Binding var someDice: Int
@Binding var diceNum: Int
var body: some View {
VStack{
Image(systemName: "die.face.\(diceNum).fill")
.resizable()
.foregroundStyle(.black, .white)
.aspectRatio(1, contentMode: .fit)
}
}
역시 많이 알아야 더 간단하게 코드를 작성할 수 있다.
aspectRatio()
는 추후에 Image
의 modifier들과 함께 다시 알아보겠다.
나는 버튼을 비활성화 하기 위해 if문을 이용하여 다음과 같은 코드를 작성했었다.
Button() {
subDice()
} label: {
Image(systemName: "minus")
.foregroundStyle(someDice == 0 ? .gray : .white)
}
private func subDice() {
if someDice > 0 {
dice.removeLast()
someDice -= 1
} else if someDice == 0{
someDice = 0
}
}
삼항연산자로 foregroundStyle()
에 색을 설정하고 함수는 일정 개수 이후로는 변화가 없도록 코드를 짰다.
튜토리얼에서 사용한 .disabled()
를 알고 있다면 다음과 같이줄일 수 있다.
Button() {
someDice -= `
} label {
Image(systemName: "minus")
}.disabled(someDice==0)
자주 사용했더라도 내가 모르는 modifier들이 많이 있을 것이 분명하기에 Image
와 더불어 여러 View들의 수정자를 따로 정리하는 시간을 가져야겠다.
추가적으로 빠르게 만들어보겠다고 변수 이름을 비슷하게 많이 지었다. 영어로 바로 표현하지 못하는 부분도 있지만 충분히 고민하거나 찾아보지 않기도 했다. 다음 튜토리얼부터는 변수 이름에도 어느정도 시간을 써서 다시 읽어볼 때 헷갈리지 않도록 노력을 기울이자.