Reputation: 41
I'm creating a flashcard view for language education. The first iteration consisted of flipping cards to front and back. The second iteration consisted of an addition of a swipe-based interface. The third iteration was meant to combine the previous two; however I'm having animation visualization issues (i.e. the third flip animation doesn't look like the first iteration).
first and third iteration gif displaying issue
first iteration code:
struct CardBack : View {
let width : CGFloat
let height : CGFloat
let firLanguage: String
let wordClass: String
let accuracy: String
let definition: String
@Binding var degree : Double
var body: some View {
ZStack {
VStack {
HStack {
Text(wordClass)
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.horizontal, 5)
.padding(.top, 5)
.font(.system(size: 15))
Text(firLanguage)
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.trailing, 5)
.padding(.top, 5)
.font(.system(size: 15))
Text(accuracy)
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.trailing, 5)
.padding(.top, 5)
.font(.system(size: 15))
Spacer()
Image(systemName: "command.circle.fill")
.font(.system(size: 29))
.padding(.horizontal, 5)
.padding(.top, 5)
.foregroundColor(Color.gray)
}
Spacer()
HStack {
Text("word analysis subview")
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.horizontal, 5)
.padding(.bottom, 5)
.font(.system(size: 15))
Spacer()
}
}
.frame(width: 350, height: 190)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
VStack {
Spacer()
Text(definition)
.font(.title)
Spacer()
}
.frame(width: 350, height: 90)
.background(Color.white)
.cornerRadius(0)
.shadow(radius: 1)
}
.rotation3DEffect(Angle(degrees: degree), axis: (x: 1, y: 0, z: 0))
}
}
struct CardFront : View {
let width : CGFloat
let height : CGFloat
let secLanguage: String
let wordClass: String
let accuracy: String
let term: String
@Binding var degree : Double
var body: some View {
ZStack {
VStack {
HStack {
Text(wordClass)
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.horizontal, 5)
.padding(.top, 5)
.font(.system(size: 15))
Text(secLanguage)
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.trailing, 5)
.padding(.top, 5)
.font(.system(size: 15))
Text(accuracy)
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.trailing, 5)
.padding(.top, 5)
.font(.system(size: 15))
Spacer()
Image(systemName: "command.circle.fill")
.font(.system(size: 29))
.padding(.horizontal, 5)
.padding(.top, 5)
.foregroundColor(Color.gray)
}
Spacer()
HStack {
Text("word analysis subview")
.padding(10)
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
.padding(.horizontal, 5)
.padding(.bottom, 5)
.font(.system(size: 15))
Spacer()
}
}
.frame(width: 350, height: 190)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
VStack {
Spacer()
Text(term)
.font(.title)
Spacer()
}
.frame(width: 350, height: 90)
.background(Color.white)
.cornerRadius(0)
.shadow(radius: 1)
}
.rotation3DEffect(Angle(degrees: degree), axis: (x: 1, y: 0, z: 0))
}
}
struct CardFlipView: View {
//MARK: Variables
@State var backDegree = 0.0
@State var frontDegree = -90.0
@State var isFlipped = true
let width : CGFloat = 200
let height : CGFloat = 250
let durationAndDelay : CGFloat = 0.3
let firLanguage: String = "English"
let secLanguage: String = "Spanish"
let wordClass: String = "Verb"
let accuracy: String = "63%"
let term: String = "hablar"
let definition: String = "to talk"
//MARK: Flip Card Function
func flipCard () {
isFlipped = !isFlipped
if isFlipped {
withAnimation(.linear(duration: durationAndDelay)) {
backDegree = 90
}
withAnimation(.linear(duration: durationAndDelay).delay(durationAndDelay)){
frontDegree = 0
}
} else {
withAnimation(.linear(duration: durationAndDelay)) {
frontDegree = -90
}
withAnimation(.linear(duration: durationAndDelay).delay(durationAndDelay)){
backDegree = 0
}
}
}
//MARK: View Body
var body: some View {
ZStack {
CardBack(width: width, height: height, firLanguage: firLanguage, wordClass: wordClass, accuracy: accuracy, definition: definition, degree: $frontDegree)
CardFront(width: width, height: height, secLanguage: secLanguage, wordClass: wordClass, accuracy: accuracy, term: term, degree: $backDegree)
}.onTapGesture {
flipCard ()
}
}
}
third iteration code:
struct Word: Hashable, CustomStringConvertible {
var id: Int
let front: String
let back: String
let term: String
let definition: String
let accuracy: Int
let wordclass: String
var description: String {
return "\(term), id: \(id)"
}
}
struct ContentView3: View {
/// List of words
@State var words: [Word] = [
Word(id: 0, front: "Spanish", back: "English", term: "to speak", definition: "hablar", accuracy: 63, wordclass: "Verb"),
Word(id: 1, front: "Spanish", back: "English", term: "to eat", definition: "comer", accuracy: 63, wordclass: "Verb"),
//Word(id: 2, front: "Spanish", back: "English", term: "to think", definition: "pensar", accuracy: 63, wordclass: "Verb"),
//Word(id: 3, front: "Spanish", back: "English", term: "to live", definition: "vivir", accuracy: 63, wordclass: "Verb"),
//Word(id: 4, front: "Spanish", back: "English", term: "to write", definition: "escribir", accuracy: 63, wordclass: "Verb"),
]
/// Return the CardViews width for the given offset in the array
/// - Parameters:
/// - geometry: The geometry proxy of the parent
/// - id: The ID of the current user
private func getCardWidth(_ geometry: GeometryProxy, id: Int) -> CGFloat {
let offset: CGFloat = CGFloat(words.count - 1 - id) * 10
return geometry.size.width - offset
}
/// Return the CardViews frame offset for the given offset in the array
/// - Parameters:
/// - geometry: The geometry proxy of the parent
/// - id: The ID of the current user
private func getCardOffset(_ geometry: GeometryProxy, id: Int) -> CGFloat {
return CGFloat(words.count - 1 - id) * 10
}
private var maxID: Int {
return self.words.map { $0.id }.max() ?? 0
}
var body: some View {
VStack {
GeometryReader { geometry in
LinearGradient(gradient: Gradient(colors: [Color.init(#colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1)), Color.init(#colorLiteral(red: 1, green: 0.9882352941, blue: 0.862745098, alpha: 1))]), startPoint: .bottom, endPoint: .top)
.frame(width: geometry.size.width * 1.5, height: geometry.size.height)
.background(Color.yellow)
.clipShape(Circle())
.offset(x: -geometry.size.width / 4, y: -geometry.size.height / 6)
VStack(spacing: 24) {
Spacer()
ZStack {
ForEach(self.words, id: \.self) { word in
Group {
// Range Operator
if (self.maxID - 3)...self.maxID ~= word.id {
CardView(word: word, onRemove: { removedWord in
// Remove that user from our array
self.words.removeAll { $0.id == removedWord.id }
})
.animation(.spring())
.frame(width: self.getCardWidth(geometry, id: word.id), height: 400)
.offset(x: 0, y: self.getCardOffset(geometry, id: word.id))
}
}
}
}
Spacer()
}
}
}.padding()
}
}
//MARK: CardView
struct CardView: View {
@State private var translation: CGSize = .zero
@State private var swipeStatus: LikeDislike = .none
@State var backDegree = 0.0
@State var frontDegree = -90.0
@State var isFlipped = true
let durationAndDelay : CGFloat = 0.3
var word: Word
private var onRemove: (_ word: Word) -> Void
private var thresholdPercentage: CGFloat = 0.5 // when the user has draged 50% the width of the screen in either direction
private enum LikeDislike: Int {
case like, dislike, none
}
init(word: Word, onRemove: @escaping (_ word: Word) -> Void) {
self.word = word
self.onRemove = onRemove
}
private func getGesturePercentage(_ geometry: GeometryProxy, from gesture: DragGesture.Value) -> CGFloat {
gesture.translation.width / geometry.size.width
}
func flipCard () {
isFlipped = !isFlipped
if isFlipped {
withAnimation(.linear(duration: durationAndDelay)) {
backDegree = 90
}
withAnimation(.linear(duration: durationAndDelay).delay(durationAndDelay)){
frontDegree = 0
}
} else {
withAnimation(.linear(duration: durationAndDelay)) {
frontDegree = -90
}
withAnimation(.linear(duration: durationAndDelay).delay(durationAndDelay)){
backDegree = 0
}
}
}
var body: some View {
GeometryReader { geometry in
VStack(alignment: .leading) {
ZStack(alignment: self.swipeStatus == .like ? .topLeading : .topTrailing) {
if self.swipeStatus == .like {
Text("YES")
.font(.headline)
.padding()
.cornerRadius(10)
.foregroundColor(Color.green)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.green, lineWidth: 3.0)
).padding(24)
.rotationEffect(Angle.degrees(-45))
} else if self.swipeStatus == .dislike {
Text("NO")
.font(.headline)
.padding()
.cornerRadius(10)
.foregroundColor(Color.red)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.red, lineWidth: 3.0)
).padding(.top, 45)
.rotationEffect(Angle.degrees(45))
}
ZStack {
CardFront2(word: word, degree: $frontDegree)
CardBack2(word: word, degree: $backDegree)
}.onTapGesture {
flipCard ()
}
}
}
.animation(.interactiveSpring())
.offset(x: self.translation.width, y: 0)
.rotationEffect(.degrees(Double(self.translation.width / geometry.size.width) * 1), anchor: .bottom)
.gesture(
DragGesture()
.onChanged { value in
self.translation = value.translation
if (self.getGesturePercentage(geometry, from: value)) >= self.thresholdPercentage {
self.swipeStatus = .like
} else if self.getGesturePercentage(geometry, from: value) <= -self.thresholdPercentage {
self.swipeStatus = .dislike
} else {
self.swipeStatus = .none
}
}.onEnded { value in
// determine snap distance > 0.5 (half the width of the screen)
if abs(self.getGesturePercentage(geometry, from: value)) > self.thresholdPercentage {
self.onRemove(self.word)
} else {
self.translation = .zero
}
}
)
}
}
}
struct CardFront2 : View {
var word: Word
@Binding var degree : Double
var body: some View {
ZStack {
HStack {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("\(self.word.front)") ///add toggleability
.font(.subheadline)
.foregroundColor(.gray)
.padding(7)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
.cornerRadius(10)
Text("\(self.word.wordclass)") ///add toggleability
.font(.subheadline)
.foregroundColor(.gray)
.padding(7)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
.cornerRadius(10)
Text("\(self.word.accuracy)%") ///add toggleability
.font(.subheadline)
.foregroundColor(.gray)
.padding(7)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
.cornerRadius(10)
Spacer()
Image(systemName: "info.circle")
.foregroundColor(.gray)
}.padding(.top, 15)
HStack {
Spacer()
Text("\(self.word.definition)")
.font(.title)
.bold()
Spacer()
}.padding(.vertical, 15)
}
Spacer()
}
.padding(.horizontal, 10)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
}
.rotation3DEffect(Angle(degrees: degree), axis: (x: 0, y: 1, z: 0))
}
}
struct CardBack2 : View {
var word: Word
@Binding var degree : Double
var body: some View {
ZStack {
HStack {
VStack(alignment: .leading, spacing: 6) {
HStack {
Text("\(self.word.back)") ///add toggleability
.font(.subheadline)
.foregroundColor(.gray)
.padding(7)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
.cornerRadius(10)
Text("\(self.word.wordclass)") ///add toggleability
.font(.subheadline)
.foregroundColor(.gray)
.padding(7)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
.cornerRadius(10)
Text("\(self.word.accuracy)%") ///add toggleability
.font(.subheadline)
.foregroundColor(.gray)
.padding(7)
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.gray, lineWidth: 1))
.cornerRadius(10)
Spacer()
Image(systemName: "info.circle")
.foregroundColor(.gray)
}.padding(.top, 15)
HStack {
Spacer()
Text("\(self.word.term)")
.font(.title)
.bold()
Spacer()
}.padding(.vertical, 15)
}
Spacer()
}
.padding(.horizontal, 10)
.background(Color.white)
.cornerRadius(10)
.shadow(radius: 5)
}
.rotation3DEffect(Angle(degrees: degree), axis: (x: 0, y: 1, z: 0))
}
}
I tried moving the .onTapGesture onto different parent views. I have really tried playing with all things animation for a few hours now, and I haven't been able to crack it. I want the card flip on iteration 3 to mirror that of iteration 1.
Upvotes: 3
Views: 181
Reputation: 41
At first I was clueless, then I identified the GeometryReader{} (GR) under CardView() as potentially the issue; however, it was actually the unnecessary VStack{} directly under the GR that it was reading from, instead of the ZStack{}. Outlined below.
GeometryReader { geometry in
VStack { // <-REMOVED
ZStack(alignment: self.swipeStatus == .yes ? .topLeading : .topTrailing) {
if self.swipeStatus == .yes {...} else if self.swipeStatus == .no {...}
ZStack {
CardFront2(word: word, degree: $frontDegree)
CardBack2(word: word, degree: $backDegree)
}.onTapGesture {
flipCard ()
}
}
After removing the above outlined code I was able to see more of the animation take place; however it still wasn't complete. I identified an animation in the ContentView() causing it. Outlined below.
ZStack {
ForEach(self.words, id: \.self) { word in
Group {
if (self.maxID - 3)...self.maxID ~= word.id {
CardView2(word: word, onRemove: { removedWord in
self.words.removeAll { $0.id == removedWord.id }
})
.animation(.spring()) // <-REMOVED
.frame(width: self.getCardWidth(geometry, id: word.id), height: 400)
.offset(x: 0, y: self.getCardOffset(geometry, id: word.id))
}
}
}
}
I'm quite self-taught, so I consistently need shoving in the right direction. Thank you both for your pointers!
Upvotes: 1