HP Trivia (6)
Change Book Status
이번 강의에서는 책(Book)의 상태를 변경할 수 있는 기능을 구현한다.
책 상태는 .active
, .inactive
, .locked
세 가지로 구분되며, 사용자의 탭 제스처에 따라 상태를 변경하고 뷰에 반영한다.
상태 변경 시도와 오류 발생
처음에는 아래처럼 .onTapGesture
를 통해 상태를 바꾸려 했지만, 다음과 같은 오류가 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ZStack(alignment: .bottomTrailing) {
Image(book.image)
.resizable()
.scaledToFit()
.shadow(radius: 7)
Image(systemName: "checkmark.circle.fill")
.font(.largeTitle)
.imageScale(.large)
.foregroundStyle(.green)
.shadow(radius: 1)
.padding(3)
}
.onTapGesture {
book.status = .inactive // 오류 발생
}
에러:
Cannot assign to property: 'book' is a 'let' constant
이는 ForEach { book in }
구문에서 book
이 복사된 상수이기 때문이다.
직접 값을 바꾸는 것이 불가능하므로, BookQuestions 클래스 내부에서 처리해야 한다.
BookQuestions에 상태 변경 메서드 추가
1
2
3
func changeStatus(of id: Int, to status: BookStatus) {
books[id - 1].status = status
}
배열은 0부터 시작하므로, 책 ID에서 1을 빼준다.
View에서 상태 변경 적용
1
2
3
.onTapGesture {
game.bookQuestions.changeStatus(of: book.id, to: .inactive)
}
탭 시 BookQuestions 내부의 books
배열이 업데이트된다.
Inactive 상태 표현
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
else if book.status == .inactive {
ZStack(alignment: .bottomTrailing) {
Image(book.image)
.resizable()
.scaledToFit()
.shadow(radius: 7)
.overlay {
Rectangle().opacity(0.33)
}
Image(systemName: "circle")
.font(.largeTitle)
.imageScale(.large)
.foregroundStyle(.green.opacity(0.5))
.shadow(radius: 1)
.padding(3)
}
.onTapGesture {
game.bookQuestions.changeStatus(of: book.id, to: .active)
}
}
이제 다시 테스트를 해보면
overlay를 통해 inActive 상태의 표현을 좀 더 부각시켰다.
Locked 상태 표현 및 Alert 표시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ZStack {
Image(book.image)
.resizable()
.scaledToFit()
.shadow(radius: 7)
.overlay {
Rectangle().opacity(0.75)
}
Image(systemName: "lock.fill")
.font(.largeTitle)
.imageScale(.large)
.shadow(color: .white, radius: 2)
}
.onTapGesture {
showTempAlert.toggle()
game.bookQuestions.changeStatus(of: book.id, to: .active)
}
여기도 역시 overlay를 통해 lock 상태의 표현을 좀 더 부각시켰다.
추가로 @State private var showTempAlert = false
를 선언하고, 아래처럼 ZStack 바깥에 alert를 붙여준다.
1
2
3
4
5
6
7
8
var body: some View {
ZStack{
// 생략
}
.alert("You purchased a new question pack. Yay!", isPresented: $showTempAlert) {
// 비어있는 alert 버튼
}
}
Disable Dismiss
disable을 하는 이유는 모든 책이 inactive 또는 locked일때 게임이 진행이 되면 안되기에 하는 일종의 예외 처리이다.
우선 Computed Property를 하나 만든다.
1
2
3
4
5
6
7
8
9
var activeBooks: Bool {
for book in game.bookQuestions.books {
if book.status == .active {
return true
}
}
return false
}
이 property는 책이 한권이라도 active면 true 그게 아니라면 false를 리턴하도록 한다.
이후 scrollview 하단에 다음과 같이 if문을 하나 추가한다.
1
2
3
4
5
6
7
ScrollView {
// 생략
}
if !activeBooks { //new
Text("You must select at least 1 book.")
}
책이 한권이라도 active아닐경우 Text를 Scrollview하단에 Text를 띄우기 위함이다.
그리고 Button에도 Modifier를 추가한다.
1
2
3
4
5
6
7
8
9
Button("Done") {
dismiss()
}
.font(.largeTitle)
.padding()
.buttonStyle(.borderedProminent)
.tint(.brown.mix(with: .black, by: 0.2))
.foregroundStyle(.white)
.disabled(!activeBooks) // new
바로 false를 감지하는순간 버튼을 disabled하게 만드는것이다.
이러면 한권이라도 Active가 아닐경우 버튼이 비활성화되어 눌리지 않는다.
하지만 sheet로 올린 View라 아래로 드래그하면 내려가기에 이것또한 방지하기위해 Modifier 를 추가.
1
2
3
4
5
6
7
ZStack {
// 생략
}
.interactiveDismissDisabled(!activeBooks) // new
.alert("You purchased a new question pack. Yay!", isPresented: $showTempAlert) {
// blank
}
interactiveDismissDisabled
의 경우 이전글에서 언급을 한적이 있으니 참고
Single Responsibility Principle 적용하기
이전글에서 작성했었기에 별다른 언급은 패스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ScrollView {
LazyVGrid(columns: [GridItem(), GridItem()]) {
ForEach(game.bookQuestions.books) { book in
if book.status == .active {
ActiveBookView(book: book)
.onTapGesture {
game.bookQuestions.changeStatus(of: book.id, to: .inactive)
}
} else if book.status == .inactive {
InactiveBookViews(book: book)
.onTapGesture {
game.bookQuestions.changeStatus(of: book.id, to: .active)
}
} else {
LockedBookView(book: book)
.onTapGesture {
showTempAlert.toggle()
game.bookQuestions.changeStatus(of: book.id, to: .active)
}
}
}
}
}
각각 해당하는 View를 만들어서 옮겨준다.
하나만 예시를 들면
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct LockedBookView: View {
@State var book: Book // new
var body: some View {
ZStack {
Image(book.image)
.resizable()
.scaledToFit()
.shadow(radius: 7)
.overlay {
Rectangle().opacity(0.75)
}
Image(systemName: "lock.fill")
.font(.largeTitle)
.imageScale(.large)
.shadow(color: .white, radius: 2)
}
}
}
@State var book: Book
변수를 만들어 준다.
물론 book은 의존성 주입에 따라 기존 SelectBooksView에서 book을 그대로 전달받는다.