I have an auction app running. I need several timers counting down in the UI, these timers have different end Dates, and the end seconds can be updated when SignalR receives a new value.
I have implemented running timers in my current solution, but sometimes and suddenly, they start having delays between counting down a second.
The timers are inside these components called LotCard within the ForEach
ForEach($lotService.getLotListDto) { $item in
LotCard(lotCardViewModel: $item.lotCardViewModel,
lotDto: item,
fnStartLotConnection: { value in lotService.initSingleLotCard(uID: value)})
}
This is the necessary code within these components:
//MARK: Timer
#State var timeRemaining = 9999
let timerLotCard = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
HStack(spacing: 3){
Image(systemName: "stopwatch")
if(lotCardViewModel.showTimer){
Text("\(timeRemaining.toTimeString())")
.font(.subheadline)
.onReceive(timerLotCard){ _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
if(self.timeRemaining <= 0){
self.timerLotCard.upstream.connect().cancel()
}
}else{
self.timerLotCard.upstream.connect().cancel()
}
}
}
}
I guess it's a problem with threads and using many timers simultaneously with the same Instance but I am not an experienced developer using SwiftUI/Swift.
This is how my interface looks like:
Thanks for your help.
I came up with this approach thanks to the comments in my question.
I hope it works and suits your problems.
First, I created a Published Timer, meaning every component will run the same Timer.
import Foundation
import Combine
class UIService : ObservableObject {
static let shared = UIService()
//MARK: Timer to be used for any interested party
#Published var generalTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}
Finally, I am using that Timer wherever I want to use it. In this case, I use it in every LotCard component.
struct LotCard: View {
//MARK: Observable Class
#EnvironmentObject var UISettings: UIService
//MARK: Time for the counting down
#State var timeRemaining = 9999
var body: some View {
HStack{
Text(timeRemaining.toTimeString())
.onReceive(UISettings.generalTimer){ _ in
if self.timeRemaining > 0 {
self.timeRemaining -= 1
}
}
}
}
}
Related
I am building a SwiftUI App and have all my UI related stuff within a global observable class UILogic. That class itself has a #Published variable called bp which is of type BoxParameters (struct).
My SwiftUI View observes this published variable, which has a lot of components: aspectRatio, frameWidth, xOffset, yOffset, etc. If I want my View to be wider for example, I just call the setWidth() function like this:
struct BoxParameters {
private(set) var frameWidth: CGFloat = 175
mutating func setWidth(newWidth: Double) {
self.frameWidth = newWidth
}
}
class UILogic: ObservableObject {
#Published var bp = BoxParameters
func doubleWidth() {
bp.setWidth(bp.frameWidth * 2)
}
}
This works fine: because it’s mutating, it creates a new struct instance, which triggers #Published to send an update and the view changes with the new width.
What I'm struggling to do is to change the frameWidth (or any other struct variable) with a timer.
So let’s say I don’t want to change the value instantly, but want to change it by incrementing the value 10 times every second.
My first guess was to use timer directly:
mutating func setWidth(newWidth: Double, slow: Bool = false) {
if !slow {
self.frameWidth = newWidth
} else {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
self.frameWidth += 1
if self.frameWidth >= newWidth {
self.frameWidth = newWidth
timer.invalidate()
}
}
}
}
This code doesn't compile and throws an error: Escaping closure captures mutating 'self' parameter
This has already made me scratch my head a bit, so I started digging around for solutions:
https://stackoverflow.com/a/47173607/12596719\
https://developer.apple.com/forums/thread/652094\
Those two threads sparked a hope that my problem might be finally solved didn’t change anything: the compiler was still complaining.
What seemed to solve my problem was this thread, so I tried to adapt it in my code (just to test out it is a void function just increases the frameWidth by 50):
struct BoxParameters {
...
var timerLogic: TimerLogic!
class TimerLogic {
var structRef: BoxParameters!
var timer: Timer!
init(_ structRef: BoxParameters){
self.structRef = structRef;
self.timer = Timer.scheduledTimer(
timeInterval: 0.1,
target: self,
selector: #selector(timerTicked),
userInfo: nil,
repeats: true)
}
func stopTimer(){
self.timer?.invalidate()
self.structRef = nil
}
#objc private func timerTicked(){
self.structRef.timerTicked()
}
}
mutating func startTimer(){
print("Start Timer")
self.timerLogic = TimerLogic(self)
}
mutating func stopTimer() {
print("Stop Timer")
self.timerLogic.stopTimer()
self.timerLogic = nil
}
mutating func timerTicked(){
self.frameWidth += 50
print("Timer: new frame width: \(self.frameWidth)")
}
}
Expected behavior: it increased the frameWidth by 50
What happens: it prints that the frame width has been increased by 50 (printing value is correct), but nothing changes.
BUT: if I call the function timerTicked manually, the frameWidth changes by 50 as expected! ugh!
What I think is happening is that the timer is changing the frameWidth of a copy of the struct without changing the real struct, but then again, the timerTicked function should change the parent struct itself. (because of self.)
Anyone knows a way to solve this issue? Changing the struct to an observed class would've been an option but due to Swift's design, a change of a #Published variable inside a #Published class doesn’t notify SwiftUI of a change...
Why are you putting classes and any code logic in a struct? I think you need to work the logic into classes and just use the struct for simple variable usage.
A struct is better used to call variables around the app .
struct AllstructFittings {
static var collectedWorks: Bool = false
static var collected: String = "notcollected"
static var failclicked: Bool = false
}
https://www.appypie.com/struct-vs-class-swift-how-to
Scenario
A simple SwiftUI App that consists of a TabView with two tabs. The App struct has a #StateObject property, which is being repeatedly and very quickly (30 times per second) updated by simulateFastStateUpdate.
In this example, simulateFastStateUpdate is not doing any useful work, but it closely resembles a real function that quickly updates the app's state. The function does some work on a background queue for a short interval of time and then schedules a state update on the main queue. For example, when using the camera API, the app might update the preview image as frequently as 30 times per second.
Question
When the app is running, the TabView does not respond to taps. It's permanently stuck on the first tab. Removing liveController.message = "Nice" line fixes the issue.
Why is TabView stuck?
Why is updating #StateObject causing this issue?
How to adapt this simple example, so that the TabView is not stuck?
import SwiftUI
class LiveController: ObservableObject {
#Published var message = "Hello"
}
#main
struct LiveApp: App {
#StateObject var liveController = LiveController()
var body: some Scene {
WindowGroup {
TabView() {
Text(liveController.message)
.tabItem {
Image(systemName: "1.circle")
}
Text("Tab 2")
.tabItem {
Image(systemName: "2.circle")
}
}
.onAppear {
DispatchQueue.global(qos: .userInitiated).async {
simulateFastStateUpdate()
}
}
}
}
func simulateFastStateUpdate() {
DispatchQueue.main.async {
liveController.message = "Nice"
}
// waits 33 ms ~ 30 updates per second
usleep(33 * 1000)
DispatchQueue.global(qos: .userInitiated).async {
simulateFastStateUpdate()
}
}
}
You are blocking the main thread with these constant updates and the app is busy processing your UI updates and can't handle touch inputs (also received on the main thread).
Whatever creates this rapid event stream needs to be throttled. You can use Combine's throttle or debounce functionality to reduce the frequency of your UI updates.
Look at this sample, I added the class UpdateEmittingComponent producing updates with a Timer. This could be your background component updating rapidly.
In your LiveController I'm observing the result with Combine. There I added a throttle into the pipeline which will cause the message publisher to fiere once per second by dropping all in-between values.
Removing the throttle will end up in an unresponsive TabView.
import SwiftUI
import Combine
/// class simulating a component emitting constant events
class UpdateEmittingComponent: ObservableObject {
#Published var result: String = ""
private var cancellable: AnyCancellable?
init() {
cancellable = Timer
.publish(every: 0.00001, on: .main, in: .default)
.autoconnect()
.sink {
[weak self] _ in
self?.result = "\(Date().timeIntervalSince1970)"
}
}
}
class LiveController: ObservableObject {
#Published var message = "Hello"
#ObservedObject var updateEmitter = UpdateEmittingComponent()
private var cancellable: AnyCancellable?
init() {
updateEmitter
.$result
.throttle(for: .seconds(1),
scheduler: RunLoop.main,
latest: true
)
.assign(to: &$message)
}
}
#main
struct LiveApp: App {
#StateObject var liveController = LiveController()
var body: some Scene {
WindowGroup {
TabView() {
Text(liveController.message)
.tabItem {
Image(systemName: "1.circle")
}
Text("Tab 2")
.tabItem {
Image(systemName: "2.circle")
}
}
}
}
}
Normally, I would use an optional variable to hold my Timer reference, as it's nice to be able to invalidate and set it to nil before recreating.
I'm trying to use SwiftUI and want to make sure I'm correctly doing so...
I declare as:
#State var timer:Publishers.Autoconnect<Timer.TimerPublisher>? = nil
Later I:
self.timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
To drive a UI text control I use:
.onReceive(timer) { time in
print("The time is now \(time)")
}
What is the right way with this Combine typed Timer to invalidate and recreate?
I've read one should call:
self.timer.upstream.connect().cancel()
However, do I also need to invalidate or simply then nil out?
There is no need to throw away the TimerPublisher itself. Timer.publish creates a Timer.TimerPublisher instance, which like all other publishers, only starts emitting values when you create a subscription to it - and it stops emitting as soon as the subscription is closed.
So instead of recreating the TimerPublisher, you just need to recreate the subscription to it - when the need arises.
So assign the Timer.publish on declaration, but don't autoconnect() it. Whenever you want to start the timer, call connect on it and save the Cancellable in an instance property. Then whenever you want to stop the timer, call cancel on the Cancellable and set it to nil.
You can find below a fully working view with a preview that starts the timer after 5 seconds, updates the view every second and stops streaming after 30 seconds.
This can be improved further by storing the publisher and the subscription on a view model and just injecting that into the view.
struct TimerView: View {
#State private var text: String = "Not started"
private var timerSubscription: Cancellable?
private let timer = Timer.publish(every: 1, on: .main, in: .common)
var body: some View {
Text(text)
.onReceive(timer) {
self.text = "The time is now \($0)"
}
}
mutating func startTimer() {
timerSubscription = timer.connect()
}
mutating func stopTimer() {
timerSubscription?.cancel()
timerSubscription = nil
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
var timerView = TimerView()
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
timerView.startTimer()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
timerView.stopTimer()
}
return timerView
}
}
With a view model, you don't even need to expose a TimerPublisher (or any Publisher) to the view, but can simply update an #Published property and display that in the body of your view. This enables you to declare timer as autoconnect, which means you don't manually need to call cancel on it, you can simply nil out the subscription reference to stop the timer.
class TimerViewModel: ObservableObject {
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private var timerSubscription: Cancellable?
#Published var time: Date = Date()
func startTimer() {
timerSubscription = timer.assign(to: \.time, on: self)
}
func stopTimer() {
timerSubscription = nil
}
}
struct TimerView: View {
#ObservedObject var viewModel: TimerViewModel
var body: some View {
Text(viewModel.time.description)
}
}
struct TimerView_Previews: PreviewProvider {
static var previews: some View {
let viewModel = TimerViewModel()
let timerView = TimerView(viewModel: viewModel)
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
viewModel.startTimer()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 30) {
viewModel.stopTimer()
}
return timerView
}
}
I have a simple app that is intended to work like this:
App loads with a default image
Once a tap is received a random image is loaded from list
If no tap is received for 5 seconds we reset back to default image
I'm attempting to accomplish this using DispatchQueue and DispatchWorkItem. I have managed to get the above functionality working - except when a user is tapping multiple times.
I'm assuming this is because we are initiating the "resetToOff" DispatchWorkItem each time we tap without resetting the 5 seconds.
How do I reset the image back to the default image if there is no touch received for 5 seconds while resetting the DispatchQueue?
Here is what I have so far:
import SwiftUI
struct PlayView : View {
#ObservedObject var viewRouter: ViewRouter
#State var imageName : String = "smiley"
var body: some View {
ZStack {
Color.black
.edgesIgnoringSafeArea(.all)
Image(imageName)
}
.gesture(
TapGesture()
.onEnded {
let resetToOff = DispatchWorkItem {
self.imageName = "smiley"
}
self.changeImage()
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: resetToOff)
}
)
// Activate the options menu
.onLongPressGesture(minimumDuration: 3) {
self.viewRouter.currentPage = "menuView"
}
}
func changeImage() {
let tempImageName : String = self.imageName
let list : Array = [
"smileyPink",
"smileyGreen",
"smileyRed",
"smileyBlue",
"smileyYellow"
]
self.imageName = list.randomElement() ?? ""
// Ensure that new image selection is not the same as previous image
while tempImageName == self.imageName {
self.imageName = list.randomElement() ?? ""
}
}
}
struct PlayView_Previews : PreviewProvider {
static var previews: some View {
PlayView(viewRouter: ViewRouter())
}
}
Any help with this would be much appreciated.
I think it's really easy to modify your code so that resetToOff only does something if 5 or more seconds passed from last tap:
var lastTapped: DispatchTime
...
.gesture(
TapGesture()
.onEnded {
lastTapped = DispatchTime.now() // remember the time of last tap
and then:
let resetToOff = DispatchWorkItem {
if self.lastTapped + .seconds(5) <= DispatchTime.now() { // 5 sec passed from last tap
self.imageName = "smiley"
} // otherwise do nothing
}
Since both setting and modifying lastTapped happens on main thread, it's thread safe. Of course it means you potentially adding "no work needed" item to main queue, but it's likely a very low impact.
It would be simpler to use a Timer, because that is something that is easy to cancel (invalidate) and start the timer again (Timer.scheduledTimer) if the user taps before the Timer fires at the end of 5 seconds.
For example, that is how my LinkSame app works. There is a 10-second "move" timer. If the user doesn't make a valid move within 10 seconds of the previous move, the user loses 10 points and the timer starts over. If the user does make a valid move within 10 seconds of the previous move, the user gets a score based on where we are in the timer and the timer starts over. That is all accomplished with a Timer.
[28/10/2019: I have recreated the same test project with UIKit... unsurprisingly, the timer does not make any of the view controller views reload!!! So this looks like a SwiftUI bug. UIKIt version: https://github.com/DominikButz/DispatchSourceTimerUIKit.git]
[19/10/2019: Please check out https://github.com/DominikButz/DispatchTimerSwiftUIReloadBug.git for better understanding of the issue. When you launch this in the simulator, just tap on "Launch timer". Check the debug console, you will see that all views are reloaded every second.]
Original post:
I'm creating an iOS app in SwiftUI for which I need to use a timer. I'm posting the class below.
The timer works fine (except for not continuing when the app is in the background, but that is another story...). As you can see in the resumeTimer function, the timer is set to repeat every second.
There is a view in which a Text label gets updated every time the timer fires. This works fine. However, the problem is that other views that are loaded after the timer starts all get reloaded every time the timer fires, that means every second. e.g. an unrelated list (where the timer text label is not placed) gets reloaded every second, which makes it unusable.
I know this because when I pause or cancel the timer through the user interface, the debugger shows that the views don't get reloaded every second anymore. I placed a breakpoint (with the option "continue after evaluation") in the body of each of those views and the debugger shows that the views gets reloaded every time the timer fires.
The timer class below is created in the SceneDelegate and then passed to the Content view as environment object. I had it as ObservedObject before only in the view where the timer Text label is placed, but the result is the same. See below a view that gets reloaded when the timer fires... (gif screenshot).
Could this be a bug in SwiftUI? or might there be an error in my timer class? thanks for any hints in the right direction
Here is the content view
struct ContentView: View {
#EnvironmentObject var workoutModel: WorkoutModel
#EnvironmentObject var workoutWatch: WorkoutStopWatchTimer
#State var selectedMenuIndex = UserDefaults.standard.integer(forKey: UserDefaultKeys.selectedMenuIndex)
#State private var workoutViewPresenting = false
func setMenuIndex() {
UserDefaults.standard.set(selectedMenuIndex, forKey: UserDefaultKeys.selectedMenuIndex)
}
var body: some View {
TabView(selection:$selectedMenuIndex) {
WorkoutSelectionView(workoutViewPresenting: self.$workoutViewPresenting).onAppear(perform: self.setMenuIndex)
.tabItem {
Image(systemName: "1.square.fill")
Text("Start Workout")
}.tag(0)
Text("History").onAppear(perform: self.setMenuIndex) // I set a breakpoint here (with continue after evaluation checked)
.tabItem {
Image(systemName: "2.square.fill")
Text("History")
}.tag(1)
ExerciseListView().onAppear(perform: self.setMenuIndex) // I set a breakpoint here (with continue after evaluation checked)
.tabItem {
Image(systemName: "3.square.fill")
Text("Exercises")
}.tag(2)
Text("Profile").onAppear(perform: self.setMenuIndex)
.tabItem {
Image(systemName: "person.fill")
Text("Profile")
}.tag(3)
}.sheet(isPresented: self.$workoutViewPresenting) {
// the timer is passed to this view in which a Text label is updated every second to show the time
WorkoutView().environmentObject(self.workoutModel).environmentObject(self.workoutWatch)
}
}
}
debugger console: see the comments I put in the content view above to see where I placed the breakpoints. I did not open and close those views that many times, they are reloaded automatically every second when the timer is running.
...
3 loading History
3 loading Exercise List View
4 loading History
4 loading Exercise List View
5 loading History
5 loading Exercise List View
6 loading History
6 loading Exercise List View
7 loading History
7 loading Exercise List View
...
class WorkoutStopWatchTimer: ObservableObject {
private var sourceTimer: DispatchSourceTimer?
private let queue = DispatchQueue.init(label: "stopwatch.timer", qos: .background, attributes: [], autoreleaseFrequency: .never, target: nil)
private var counter: Int = 0 // seconds
var endDate: Date?
var duration: TimeInterval?
#Published var timeDisplayString = "0:00"
var paused = false
var isActive: Bool {
return self.sourceTimer != nil
}
func start() {
self.paused = false
guard let _ = self.sourceTimer else {
self.startTimer()
return
}
self.resumeTimer()
}
func finish() {
guard self.sourceTimer != nil else {return}
self.endDate = Date()
self.duration = TimeInterval(exactly: Double(self.counter))
self.sourceTimer?.setEventHandler {}
self.sourceTimer?.cancel()
if self.paused == true {
self.sourceTimer?.resume()
}
self.sourceTimer = nil
self.reset()
}
func pause() {
self.paused = true
self.sourceTimer?.suspend()
}
private func reset() {
self.timeDisplayString = "0:00"
self.counter = 0
}
private func startTimer() {
self.sourceTimer = DispatchSource.makeTimerSource(flags: DispatchSource.TimerFlags.strict,
queue: self.queue)
self.resumeTimer()
}
private func resumeTimer() {
self.sourceTimer?.setEventHandler { [weak self] in
// self.eventHandler = {
self?.updateTimer()
// }
}
self.sourceTimer?.schedule(deadline: .now(),
repeating: 1)
self.sourceTimer?.resume()
}
private func updateTimer() {
self.counter += 1
DispatchQueue.main.async {
self.timeDisplayString = WorkoutStopWatchTimer.convertCountToTimeString(counter: self.counter)
}
}
}