Mozahler
Mozahler

Reputation: 5303

In SwiftUI how do I animate changes one at a time when they occur in a called method?

Although I get an animation when I tap the button, it's not the animation I want. The entire view is being replaced at once, but I want to see each element change in sequence. I tried in both the parent view and in the called method. Neither produces the desired result.

(this is a simplified version of the original code)

import SwiftUI

struct SequencedCell: Identifiable {
   let id = UUID()
   var value: Int

   mutating func addOne() {
      value += 1
   }

}

struct AQTwo: View {
   @State var cells: [SequencedCell]

   init() {
      _cells = State(initialValue: (0 ..< 12).map { SequencedCell(value: $0) })
   }

   var body: some View {
      VStack {
         Spacer()
         Button("+") {
            sequencingMethod(items: $cells)
         }
         .font(.largeTitle)
         Spacer()

         HStack {
            ForEach(Array(cells.enumerated()), id: \.1.id) { index, item in
 //              withAnimation(.linear(duration: 4)) {
               Text("\(item.value)").tag(index)
 //              }
            }
         }
         Spacer()
      }
   }

   func sequencingMethod(items: Binding<[SequencedCell]>) {
      for cell in items {
         withAnimation(.linear(duration: 4)) {
            cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
           // cell.wrappedValue.addOne()
        }
      }
   }
}


struct AQTwoPreview: PreviewProvider {
   static var previews: some View {
      AQTwo()
   }
}

So I want the 0 to turn into a 1, the 1 then turn into a 2, etc.

Screencast

Edit: Even though I have accepted an answer, it answered my question, but didn't solve my issue.

I can't use DispatchQueue.main.asyncAfter because the value I am updating is an inout parameter and it makes the compiler unhappy:

Escaping closure captures 'inout' parameter 'grid'

So I tried Malcolm's (malhal) suggestion to use delay, but everything happens immediately with no sequential animation (the entire block of updated items animate as one)

Here's the recursive method I am calling:

   static func recursiveAlgorithm(targetFill fillValue: Int, in grid: inout [[CellItem]],
                                  at point: (x: Int, y: Int), originalFill: Int? = nil,  delay: TimeInterval) -> [[CellItem]] {
      /// make sure the point is on the board (or return)
      guard isValidPlacement(point) else { return grid }
      /// the first time this is called we don't have `originalFill`
      /// so we read it from the starting point
      let tick = delay + 0.2
      //AnimationTimer.shared.tick()
      let startValue = originalFill ?? grid[point.x][point.y].value
      if grid[point.x][point.y].value == startValue {
         withAnimation(.linear(duration: 0.1).delay(tick)) {
            grid[point.x][point.y].value = fillValue
                  }
         _ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x, point.y - 1), originalFill: startValue, delay: tick)
         _ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x, point.y + 1), originalFill: startValue, delay: tick)
         _ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x - 1, point.y), originalFill: startValue, delay: tick)
         _ = recursiveAlgorithm(targetFill: fillValue, in: &grid, at: (point.x + 1, point.y), originalFill: startValue, delay: tick)
      }
      return grid
   }

Further comments/suggestions are welcome, as I continue to wrestle with this.

Upvotes: 0

Views: 740

Answers (2)

malhal
malhal

Reputation: 30627

You could use delay(_:) for that, e.g.

func sequencingMethod(items: Binding<[SequencedCell]>) {
       var delayDuration = 0.0
       for cell in items {
           withAnimation(.linear(duration: 4).delay(delayDuration)) {
              cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
        }
        delayDuration += 0.5
      }
   }

Upvotes: 1

jnpdx
jnpdx

Reputation: 52416

As mentioned in the comments, the lowest-tech version is probably just using a DisatpchQueue.main.asyncAfter call:

func sequencingMethod(items: Binding<[SequencedCell]>) {
    var wait: TimeInterval = 0.0
    
    for cell in items {
        DispatchQueue.main.asyncAfter(deadline: .now() + wait) {
            withAnimation(.linear(duration: 1)) {
                cell.wrappedValue = SequencedCell(value: cell.wrappedValue.value + 1)
            }
        }
        wait += 1.0
    }
}

Upvotes: 1

Related Questions