JLPT를 준비하면서 문자어휘 파트를 공부할 때는 미리 많은 양의 단어를 외우고 문제를 풀어야 합니다. 그래서 문자어휘 파트 교재는 많은 단어를 포함하고 있습니다. 저는 이 단어를 미리 저장해두고 하루에 50개 씩 외우고 있는데요.
문제는 이 단어를 미리 다른 단어장에 저장해두고 오늘 공부할 50개만 빼오려고 할 때 기존의 기능을 활용해서 이동시키려면 상당히 복잡한 과정을 거쳐야 합니다. 지금 제 앱에서 단어를 이동하고자 할 때는 한 단어장에서 틀린 단어만 일괄적으로 이동시키는 방법 외에는 없습니다.
따라서 일단 임시 단어장에 저장하고 이동 시킬 단어만 빼고는 전부 success 처리하고 나서 원하는 단어만 이동시켜야 합니다.
이번에 구현할 기능은 이런 복잡한 과정 없이 그냥 원하는 단어를 이동하는 기능입니다. 특히 이번 포스팅에 집중할 부분은 단어를 선택했을 때 해당 단어의 Cell에 애니메이션을 적용하는 부분입니다.
우리가 구현하고자 하는 부분은 아래와 같습니다. 단어 선택 모드에 들어가면 단어 Cell 주변에 회색 점선이 생깁니다. 그리고 단어를 터치하면 Cell 주변의 점선이 파란색으로 바뀌고 애니메이션이 시작됩니다. 그리고 해당 단어들을 이동할 단어장을 선택하는 Picker가 있는 Modal을 띄워서 이동할 단어장을 고르고 단어들을 이동합니다.
제가 구현하고자 하는 테두리의 점선이 회전하는 애니메이션을 구현하는 방법은 이 포스팅에 자세하게 정리되어 있습니다.
제가 처음에 계획한 방법은 Cell을 선택하면 애니메이션이 시작되고 Cell을 다시 선택해서 취소하면 애니메이션이 멈추게 하는 식으로 구현하고자 했습니다.
하지만 열심히 찾아봤지만 이런 방식으로 구현하는 것은 쉽지 않더라구요. 특히 대부분 설명이 Bool 값을 통해서 애니메이션이 시작되도록 하는 애니메이션을 멈추는 방법이 있었는데요. 제가 구현한 방식은 dashPhase의 CGFloat 값을 변형 시켜서 구현한 애니메이션이기 때문에 더 어려웠습니다.
결국 제가 구현한 방법은 애니메이션이 있는 뷰와 애니메이션이 없는 뷰 2가지로 나누어서 Cell이 선택이 된 경우에는 애니메이션이 있는 뷰를 보여주고 그렇지 않은 경우는 애니메이션이 없는 뷰를 보여주는 방식으로 구현하는 것으로 했습니다.
private struct SelectableCell: View {
private let isSelected: Bool
init(isSelected: Bool) {
self.isSelected = isSelected
}
var body: some View {
//👉 선택 안되면 회색, 선택되면 파란색
ZStack {
if !isSelected {
Color.gray
.opacity(0.2)
} else {
Color.blue
.opacity(0.2)
}
}
//👉 선택된 경우에만 애니메이션이 있는 mask를 적용한다.
.mask(
AnimatingEdge(isAnimating: isSelected)
)
}
private struct AnimatingEdge: View {
private let isAnimating: Bool
@State private var dashPhase: CGFloat = 0
init(isAnimating: Bool) {
self.isAnimating = isAnimating
}
//👉 isAnimating이 true일 때만 애니메이션을 적용된 View를 적용한다.
var body: some View {
if isAnimating {
Rectangle()
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, dash: [10, 10], dashPhase: dashPhase))
.animation(.linear.repeatForever(autoreverses: false).speed(1), value: dashPhase)
.onAppear { dashPhase = -20 }
} else {
Rectangle()
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, dash: [10, 10], dashPhase: dashPhase))
.onAppear { dashPhase = 0 }
}
}
}
}
이 포스팅에서 다루지 않은 나머지 구현에 대해서는 이 PR을 참고해주세요!