Search code examples
swiftuitimer

Delaying Loop iteration and creating Views


I am very new to SwiftUI. I am trying to create an app for my daughter to help her with learning how to add. I am doing this by using a number line. The number line is created using a horizontal Divider as my base and then vertical Dividers to mark each number on the number line.

For example, if she needs to add 4 + 8, the app will show her how you start at 4 and count up 8 until you get to 12. In the app this is accomplished through a ForEach loop which starts at 4 and runs until 12. Each time the loop iterates, it produces the vertical divider plus a text field making the number.

Here is what the end product looks like:

iOS Screenshot

This is my code:

import SwiftUI

struct GenerateNumberLineView: View {
    var firstNumber: Int
    var secondNumber: Int
    var counter: Int
    var opacityTest: Double
    
    var body: some View {
        
        ZStack{
            Divider()
                .frame(width:300, height: 4)
            
                .overlay(.pink)
            
            ForEach(firstNumber..<(secondNumber+firstNumber+1), id: \.self) { number in
                var count = 0
                
                let offSetter = -150 + (number-firstNumber) * 30
                
                VStack{
                    
                    Divider().frame(width:4, height: 30,
                                    alignment: .leading)
                    .overlay(.pink).offset(x:CGFloat(offSetter))
                    
                    Text(String("\(number)")).offset(x:CGFloat(offSetter))
                    
                }
            }
        }
    }
}

#Preview {
    GenerateNumberLineView(firstNumber: 4, secondNumber: 8,counter: 0,opacityTest: 0.0)
}
var updateTimer: Timer {
    Timer.scheduledTimer(withTimeInterval: 1, repeats: true,
                         block: {_ in
        
        let maximumn = num2 + numb
        
        if self.counter == maximumn - numb {
            self.updateTimer.invalidate()
        } else {
            self.counter += 1
        }
    })
}

The problem is the whole number line shows up at one time and I would like to delay each iteration for a second, so that it looks like the app is doing the counting up to 12 from 4. I tried wrapping the code in the loop in the following dispatch code:

let seconds = 0.25
DispatchQueue.main.asyncAfter(deadline: .now() + seconds)
{...code inside loop}

... but got this error:

No exact matches in reference to static method 'buildExpression' <<errors

My most successful attempt was to call the struct in another view and then implement a timer:

GenerateNumberLineView(firstNumber : minNum, secondNumber: maxNum, counter:counter,opacityTest: opacityTest).onAppear(perform: { let _ = self.updateTimer })

This works 76% of the way. The problem is it only displays one number on the screen. It shows 4 and then 4 disappears then it shows 5.

I want it to show 4. 4 stays on the screen it pauses for a beat then shows 5 pause then shows 6 and so on until 12. So the number line looks like the picture I attached but the process of getting to there was displayed to the user.


Solution

  • Put your timer, as a publisher, in the SwiftUI view. Increment a @State (let's call this currentNumber) each time the timer fires, until it reaches secondNumber.

    Change the range for the ForEach to firstNumber..<(firstNumber + currentNumber + 1), and the numbers now appear one after another.

    You can add transition and animation modifiers to add an animation when a new number appears.

    let firstNumber: Int
    let secondNumber: Int
    
    @State var currentNumber = 0
    
    let timer = Timer.publish (every: 1, on: .current, in: .common).autoconnect()
    
    var body: some View {
        
        ZStack{
            Rectangle()
                .frame(width:300, height: 4)
                .overlay(.pink)
            
            ForEach(firstNumber..<(firstNumber + currentNumber + 1), id: \.self) { number in
                let offSetter = -150 + (number-firstNumber) * 30
                VStack{
                    Rectangle()
                        .frame(width:4, height: 30, alignment: .leading)
                        .overlay(.pink)
                    Text("\(number)")
                }
                .offset(x:CGFloat(offSetter))
                .transition(.opacity)
            }
        }
        .onReceive(timer) { _ in
            currentNumber += 1
            if currentNumber == secondNumber {
                timer.upstream.connect().cancel()
            }
        }
        .animation(.default, value: currentNumber)
    }