90: Flashzilla, part 5

그루두·2024년 8월 19일
0

100 days of SwiftUI

목록 보기
98/108

Project 17, part 5

개선하기

제일 위의 카드만 swipe할 수 있도록 설정하기

제일 위의 카드를 제외하고 다른 카드는 swipe할 수 없도록 설정했다.

                    ForEach(0..<cards.count, id: \.self) { index in
                        CardView(card: cards[index]) {
                            withAnimation {
                                removeCard(at: index)
                            }
                        }
                        .stacked(at: index, in: cards.count)
                        .allowsHitTesting(index == cards.count - 1)
                    }

커밋 링크

accessibiliy를 위한 이미지 숨기기

voice over에서 배경 이미지나 제일 위에 있지 않는 카드도 읽을 수 있는데, 이를 방지하고자 불필요한 요소는 accessibility를 위해 voice over에서 감췄다.

Image(decorative: "background")

// 카드 스택
.accessibilityHidden(index < cards.count - 1)

커밋 링크

카드의 voice over 설정하기

카드가 button처럼 터치가 가능하다고 알리고, 문제나 답을 적절하게 읽을 수 있도록 설정했다.

.accessibilityAddTraits(.isButton)
@Environment(\.accessibilityVoiceOverEnabled) var accessibilityVoiceOverEnabled
// ...
            VStack {
                if accessibilityVoiceOverEnabled {
                    Text(isHidingAnswer ? card.prompt : card.answer)
                        .font(.largeTitle)
                        .foregroundStyle(.black)
                } else {
                    Text(card.prompt)
                        .font(.largeTitle)
                        .foregroundStyle(.black)
                    if !isHidingAnswer {
                        Text(card.answer)
                            .font(.title)
                            .foregroundStyle(.secondary)
                    }
                }
            }
            .multilineTextAlignment(.center)

커밋 링크

VoiceOver, DifferentiateWithoutColor일 때 swipe 대신 버튼으로 카드 처리하기

두 가지 모드에서 카드 swipe 형태가 아니라 버튼으로 카드를 삭제할 수 있게 label과 버튼을 설정했다.

                        Button {
                            removeCard(at: cards.count - 1)
                        } label: {
                            Image(systemName: "xmark.circle")
                                .padding()
                                .background(.black.opacity(0.7))
                                .clipShape(.circle)
                        }
                        .accessibilityLabel("Wrong")
                        .accessibilityHint("Mark your answer as being incorrect.")
                        Spacer()
                        Button {
                            removeCard(at: cards.count - 1)
                        } label: {
                            Image(systemName: "checkmark.circle")
                                .padding()
                                .background(.black.opacity(0.7))
                                .clipShape(.circle)
                        }
                        .accessibilityLabel("Correct")
                        .accessibilityHint("Mark your answer as being correct.")

❗️ 버튼으로 삭제할 때 카드가 다 사라지면 -1의 인덱스의 카드를 삭제하는 오류가 생겨서 이를 방지할 필요가 있다.

    func removeCard(at index: Int) {
        guard index >= 0 else { return }
        cards.remove(at: index)
        if cards.isEmpty {
            isActive = false
        }
    }

커밋 링크

card를 swipe하지 않고 다시 돌려놓을 때 애니메이션 설정하기

card를 넘기지 않고 제스쳐를 그만두면 제일 위의 카드가 다시 원위치로 복귀된다. 이때 애니메이션을 추가해서 카드가 제자리로 돌아오는 것처럼 보여줄 수 있다.

.animation(.snappy, value: offset)

커밋 링크

❗️ 참고로 위 코드를 어디에 작성하느냐에 따라 효과가 달라진다. onTapGesture 다음에 작성해야 사진처럼 실행된다.

카드 수정하기

이젠 카드를 예시가 아니라 직접 추가하고 삭제할 수 있도록 설정해야 한다.

이는 크게 3단계로 나뉜다.

1. ContentView에서 EditCardsView로 접근할 수 있는 버튼 만들기

VStack {
                HStack {
                    Spacer()
                    Button {
                        isShowingEditView = true
                    } label: {
                        Image(systemName: "checkmark.circle")
                            .font(.title)
                            .padding()
                            .foregroundColor(.white)
                            .background(.black.opacity(0.7))
                            .clipShape(.circle)
                    }
                    .offset(y: 90)
                }
                Spacer()
            }

커밋 링크

❗️ 앞으로 카드를 UserDefaults에 저장하고 불러올 거라서, Card를 Codable로 설정해야 한다.

2. EditCardsView 설정하기

해당 뷰에서 새로 카드를 추가할 수 있고, 이미 있는 카드 목록을 확인하고, 삭제할 수 있도록 설정했다. 그리고 모든 카드는 UserDefaults에 저장하고 불러오도록 했다.

    func loadData() {
        if let data = UserDefaults.standard.data(forKey: "Cards") {
            if let decoded = try? JSONDecoder().decode([Card].self, from: data) {
                cards = decoded
            }
        }
    }
    func saveData() {
        if let encoded = try? JSONEncoder().encode(cards) {
            UserDefaults.standard.set(encoded, forKey: "Cards")
        }
    }
    func addCard() {
        let trimmedPrompt = newPrompt.trimmingCharacters(in: .whitespacesAndNewlines)
        let trimmedAnswer = newAnswer.trimmingCharacters(in: .whitespacesAndNewlines)
        guard !trimmedPrompt.isEmpty && !trimmedAnswer.isEmpty else { return }
        cards.insert(Card(prompt: trimmedPrompt, answer: trimmedAnswer), at: 0)
        saveData()
    }
    func removeCards(at offsets: IndexSet) {
        cards.remove(atOffsets: offsets)
        saveData()
    }
    func done() {
        dismiss()
    }

커밋 링크

3. EditCardsView 적용하기

이제 ContentView에서 EditCardsView를 보여주고 수정한 카드를 적용해서 보여줘야 한다. 이는 sheet로 연결하고, cards는 UserDefaults에서 불러오면 된다.

        .sheet(isPresented: $isShowingEditView, onDismiss: resetCards, content: EditCardsView.init)
        .onAppear(perform: loadData)

커밋 링크

.sheet(isPresented: $isShowingEditView, onDismiss: resetCards) {
	EditCardsView()
}

💡 참고로 위 코드는 구조체를 함수처럼 사용하고 있는 예시이다. EditCards.init()의 짧은 방식이다. 그래서 EditCardsView의 initializer를 호출하는 closure를 만드는 대신 sheet에 EditCardsView의 initializer를 직접 전달하는 방법이 커밋의 방식이다.

결과물

profile
계속 해보자

0개의 댓글

관련 채용 정보