How to dynamically re-render a view when continuously changing its state through an algorithm?

Viewed 356

I have a View displaying the values of an array marked as a @State property. When e.g. I have a Button to sort the array how can I achieve that SwiftUI updates the screen for each step in the sorting algorithm that mutates the values of the array?

In this test example I made a button sorting the array with bubble sort:

struct ContentView: View {
    
    @State private var array = [5,3,6,9,1,2,4,13,3,4,16]
    
    var body: some View {
        
        HStack {
            ForEach(0..<array.count, id: \.self) { i in
                Text("\(array[i])").padding()
            }
        }
        
        Button("Sort") {
            var j = array.count
            while j >= 1 {
                for i in 1..<j {
                    if array[i] < array[i-1] {
                        let temp = array[i-1]
                        array[i-1] = array[i]
                        array[i] = temp
                    }
                }
                sleep(1) // to compensate for the short runtime in this example 
                j -= 1
            }
        }
    }
}

Why is SwiftUI re-rendering the view only once although the @State property gets mutated with each sorting step? Is it possible to visualize each step on-screen somehow and to not only see the end result of the algorithm?

I want this behavior to visualize the backtracking steps when running something similar to a sudoku solver. Thanks in advance for any help!

PS: I'm quite new to Swift/SwiftUI and this is also my first question on Stack Overflow. Apologies if I wasn't specific enough or missed something obvious!

1 Answers

By using sleep, you'd block the main thread and prevent UI updates. Instead, you could using DispatchQueue.main.asyncAfter to run each successive step of the sorting:


struct ContentView: View {
    
    @State private var array = [5,3,6,9,1,2,4,13,3,4,16]
    @State private var currentElement = 0
    
    var body: some View {
        
        HStack {
            ForEach(array, id: \.self) { i in
                Text("\(i)").padding()
            }
        }
        
        Button("Sort") {
            currentElement = array.count
            withAnimation {
                runSortStep()
            }
        }
    }
    
    func runSortStep() {
        for i in 1..<currentElement {
            if array[i] < array[i-1] {
                let temp = array[i-1]
                array[i-1] = array[i]
                array[i] = temp
            }
        }
        currentElement -= 1
        if currentElement >= 1 {
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                withAnimation {
                    runSortStep()
                }
            }
        }
    }
}

Note that I've also changed your ForEach so that each element is identified by the array value and not the index -- otherwise, you won't see the animations.

Related