iOS 14 weird behavior with Timer.publish

396 Views Asked by At

I did my best to pull out only a small part of my larger project that displays this odd behavior. The intention is for one random number to be added to the array and displayed every 3 seconds. In iOS 13 each number slides in from the left every 3 seconds and everything works as expected. What I see in iOS 14 is that 4 numbers are added every 3 seconds. Does anyone understand why this would be happening? Thanks in advance!

import SwiftUI

struct ContentView: View {

    @State private var calledNumbers = CalledNumbers()
    @State private var timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
    @State private var inProgress = false

    var body: some View {
        Button(action: {
            if !self.inProgress {
                self.calledNumbers.startOver()
                print("Start timer")
                self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
            }
            else {
                print("Stop timer")
                self.timer.upstream.connect().cancel()
            }
            self.inProgress.toggle()
        })
        {
            if(self.inProgress == false) {
                Text("S T A R T")
                    .font(.system(size: 22))
                    .fontWeight(.heavy)
                    .frame(width: 200, height: 35, alignment: .center)
                    .background(Capsule()
                        .fill(Color.green))
                    .cornerRadius(35)
                    .foregroundColor(.white)
                    .padding(.bottom, 2)
            }
            else {
                Text("S T O P")
                    .font(.system(size: 22))
                    .fontWeight(.heavy)
                    .frame(width: 200, height: 35, alignment: .center)
                    .background(Color.red)
                    .foregroundColor(.white)
                    .cornerRadius(35)
                    .onReceive(self.timer) { _ in
                        self.timer.upstream.connect().cancel()
                        print("CALL NEXT NUMBER")
                        self.calledNumbers.callNextNumber()
                        self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
                    }
            }
        }
        ZStack {
            RoundedRectangle(cornerRadius: 35)
                .frame(width: UIScreen.main.bounds.size.width - 19, height: 40, alignment: .center)
                .foregroundColor(.clear)
                .padding(.bottom, 2)
            HStack {
                ForEach(self.calledNumbers.calledNumberList.reversed().filter {self.checkCount(number: $0)}, id: \.self) { number in
                    Text("\(String(number))")
                        .font(.custom("Menlo", size: 20))
                        .fontWeight(.black)
                        .frame(width: 40, height: 40, alignment: .center)
                        .background(Color.red)
                        .clipShape(Circle())
                        .foregroundColor(.white)
                        .transition(AnyTransition.offset(x: (number == self.calledNumbers.calledNumberList.last) ? -250 : 250))
                        .animation(Animation.linear(duration: 1).repeatCount(1))
                }
            }
        }
    }
    
    func checkCount(number: Int) -> Bool {
        let count = self.calledNumbers.calledNumberList.count
        if (count <= 8) {
            return true
        }
        else {
            guard let index = self.calledNumbers.calledNumberList.firstIndex(of: number) else { return false }
            if (count - index > 8) { return false }
            else { return true }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This file was added by xcode 12 (I named this project Test2):

import SwiftUI

@main
struct Test2App: App {
    
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Class for calledNumbers

//
//  class.swift
//  Test2
//

import Foundation

class CalledNumbers {
    
    @Published var calledNumberList: [Int]
            
    init() {
        calledNumberList = [Int]()
    }
    
    func callNextNumber() {
        var tempNumber = Int.random(in: 1...75)
        
        while calledNumberList.contains(tempNumber) {
            tempNumber = Int.random(in: 1...75)
        }
        calledNumberList.append(tempNumber)
        print("Number added \(tempNumber)")
        
    }
    
    func startOver() {
        
        calledNumberList.removeAll()
        
    }
}
1

There are 1 best solutions below

1
On BEST ANSWER

The problem seems to be caused by having the .onReceived() attached to the code inside of the Button. Moving .onReceived() to the Button as a whole solves the issue.

Also, you were doing more timer manipulation than is necessary. I removed stopping and restarting the timer from .onReceive().

calledNumbers should be an @ObservableObject.

struct ContentView: View {

    @ObservedObject var calledNumbers = CalledNumbers()
    @State private var timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
    @State private var inProgress = false

    var body: some View {
        Button(action: {
            if !self.inProgress {
                self.calledNumbers.startOver()
                print("Start timer")
                self.timer = Timer.publish(every: 3, tolerance: 0.5, on: .main, in: .common).autoconnect()
            }
            else {
                print("Stop timer")
                self.timer.upstream.connect().cancel()
            }
            self.inProgress.toggle()
        })
        {
            Text(inProgress ? "STOP" : "START")
                .font(.system(size: 22))
                .fontWeight(.heavy)
                .frame(width: 200, height: 35, alignment: .center)
                .cornerRadius(35)
                .foregroundColor(.white)
                .background(Capsule().fill(inProgress ? Color.red : .green))
                .padding(.bottom, 2)
            }
        }
        .onReceive(self.timer) { _ in
            print("CALL NEXT NUMBER")
            self.calledNumbers.callNextNumber()
        }
        .onAppear {
            // Cancel the initial timer
            self.timer.upstream.connect().cancel()
        }

        ZStack {
            RoundedRectangle(cornerRadius: 35)
                .frame(width: UIScreen.main.bounds.size.width - 19, height: 40, alignment: .center)
                .foregroundColor(.clear)
                .padding(.bottom, 2)
            HStack {
                ForEach(self.calledNumbers.calledNumberList.reversed().filter {self.checkCount(number: $0)}, id: \.self) { number in
                    Text("\(String(number))")
                        .font(.custom("Menlo", size: 20))
                        .fontWeight(.black)
                        .frame(width: 40, height: 40, alignment: .center)
                        .background(Color.red)
                        .clipShape(Circle())
                        .foregroundColor(.white)
                        .transition(AnyTransition.offset(x: (number == self.calledNumbers.calledNumberList.last) ? -250 : 250))
                        .animation(Animation.linear(duration: 1).repeatCount(1))
                }
            }
        }
    }
    
    func checkCount(number: Int) -> Bool {
        let count = self.calledNumbers.calledNumberList.count
        if (count <= 8) {
            return true
        }
        else {
            guard let index = self.calledNumbers.calledNumberList.firstIndex(of: number) else { return false }
            if (count - index > 8) { return false }
            else { return true }
        }
    }
}

Also, your CalledNumbers class should be an ObservableObject so that Published works correctly:

import Foundation

class CalledNumbers: ObservableObject {
    
    @Published var calledNumberList: [Int]
            
    init() {
        calledNumberList = [Int]()
    }
    
    func callNextNumber() {
        var tempNumber = Int.random(in: 1...75)
        
        while calledNumberList.contains(tempNumber) {
            tempNumber = Int.random(in: 1...75)
        }
        calledNumberList.append(tempNumber)
        print("Number added \(tempNumber)")
        
    }
    
    func startOver() {
        
        calledNumberList.removeAll()
        
    }
}