chudin26
chudin26

Reputation: 1172

Implement slide animation of view when model changes in SwiftUI

I have several cards with translations of word. ChooseTrainingView is my view which get word as model and success handler. When I click on translation it changes training.currentWord so I expect animation between two views. First view should slide to left and then second view shows from right with slide. But my code below has strange behaviour:

    Group {
        ForEach(training.words) { w in
            if w == training.currentWord {
                ChooseTrainingView(word: training.currentWord) { success in
                    withAnimation(.easeIn(duration: 0.5)) {
                        _ = training.goToNext(success: success)
                    }
                }
            }
        }
    }
    .transition(.slide)

Result of code

Animation has only a part of views to be animated and only in view appearing. There is no disappearing view animation.

I tried to not use ForEach, but I cannot understand how to animate full view when I change currentWord - the view just change word at top and safe its state instead of creating new view with new values from currentWord

Also I cannot find examples how to animate sliding right-to-left views by replacing the model for view

Upvotes: 0

Views: 828

Answers (2)

chudin26
chudin26

Reputation: 1172

According to @J W answer I found the solution. We should to add .id modifier to our view. Besides we don't need ForEach anymore and it means that we don't need to use array of words. Just one word (which conforms to Identifiable) and when we change this word our view will be fully recreated (reset its state). There is a code with the solution:

Group {
    ChooseTrainingView(word: training.currentWord) { success in
        withAnimation(.easeIn(duration: 0.5)) {
            _ = training.goToNext(success: success)
        }
    }
    .id(training.currentWord.id)
}
.transition(.slide)

In addition, I found very good article which explains how .id modifier works:

https://swiftui-lab.com/swiftui-id/

Upvotes: 0

J W
J W

Reputation: 924

If you just want to change the animation, it's very easy ୧(๑•̀⌄•́๑)૭

change the .transition(.slide) to .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))

Showing Image

If there is any other problem, please refer to the following content. If it really doesn’t work, please feel free to ask me again.

Example Code:

import SwiftUI

struct ChooseTrainingView: View {
  var word: String
  var successHandler: (Bool) -> Void

  var body: some View {
    ZStack {
      RoundedRectangle(cornerRadius: 25.0)
        .shadow(radius: 10)
        .frame(width: 300, height: 400)
      
      VStack {
        HStack {
          Text(word)
            .font(.title)
            .foregroundColor(.white)
          Image(systemName: "speaker.wave.2")
            .foregroundColor(.white)
        }
        
        Divider().background(Color.white)
        
        VStack(alignment: .leading, spacing: 10) {
          ForEach(0..<5) { index in
            Text("Option \(index)")
              .padding(.vertical, 8)
              .padding(.horizontal)
              .foregroundColor(.white)
              .cornerRadius(10)
          }
        }
        .padding()
      }
      .padding()
    }
    .onTapGesture {
      self.successHandler(true)
      print("successHandler")
    }
  }
}

struct ContentView: View {
  @ObservedObject var training = TrainingModel(words: (0...5).map{Word(translation: String($0))})
  
  var body: some View {
    ZStack {
      ForEach(training.words, id: \.self) { word in
        if word == training.words[training.currentWordIndex] {
          ChooseTrainingView(word: word.translation, successHandler: { success in
            withAnimation {
              _ = training.goToNext(success: success)
            }
          })
          .id(word.id)
          .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .trailing)))
        }
      }
    }
  }
}

struct Word: Hashable, Identifiable {
  let id = UUID()
  var translation: String
}

class TrainingModel: ObservableObject {
  @Published var words: [Word]
  @Published var currentWordIndex: Int
  
  init(words: [Word]) {
    self.words = words
    self.currentWordIndex = 0
  }
  
  func goToNext(success: Bool) -> Bool {
    if currentWordIndex < words.count - 1 {
      currentWordIndex += 1
      return true
    } else {
      return false
    }
  }
}

#Preview {
  ContentView()
}

Upvotes: 2

Related Questions