Observe singleton timer value changes with Publisher in Combine - swift

One of the requirements of my application is the ability to start multiple timers, for reporting purposes.
I've tried to store the timers and seconds passed in an #EnvironmentObject with #Published variables, but every time the object refreshes, any view that observes the #EnvironmentObject refreshes too.
Example
class TimerManager: ObservableObject {
#Published var secondsPassed: [String: Int]
var timers: [String:AnyCancellable]
func startTimer(itemId: String) {
self.secondsPassed[itemId] = 0
self.timers[itemId] = Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.secondsPassed[itemId]! += 1
})
}
func isTimerValid(itemId: String) -> Bool {
return self.timers[itemId].isTimerValid
}
// other code...
}
So for example, if in any other view I need to know if a particular timer is active by calling a function isTimerValid, I need to include this #EnvironmentObject in that view, and it won't stop refreshing it because the timer changes secondsPassed which is Published, causing lags and useless redrawings.
So one thing I did was to cache the itemId of the active timers somewhere else, in a static struct that I update every time I start or stop a timer.
It seemed a bit hacky, so lately I've been thinking to move all this into a Singleton, like this for example
class SingletonTimerManager {
static let singletonTimerManager = SingletonTimerManager()
var secondsPassed: [String: Int]
var timers: [String:AnyCancellable]
func startTimer(itemId: String) {
self.secondsPassed[itemId] = 0
self.timers[itemId] = Timer
.publish(every: 1, on: .main, in: .default)
.autoconnect()
.sink(receiveValue: { _ in
self.secondsPassed[itemId]! += 1
})
}
// other code...
}
and only let some Views observe the changes to secondsPassed. On the plus side, I can maybe move the timer on the background thread.
I've been struggling how to do this properly.
These are my Views (albeit a very simple extract)
struct ContentView: View {
// set outside the ContentView
var selectedItemId: String
// timerValue: set by a publisher?
var body: some View {
VStack {
ItemView(seconds: Binding.constant(timerValue))
}
}
}
struct ItemView: View {
#Binding var seconds: Int
var body: some View {
Text("\(self.seconds)")
}
}
I need to somehow observe the SingletonChronoManager.secondsPassed[selectedItemId] so the ItemView updates in real time.

By putting the timer publisher results into Environment, you are propagating change notifications to all views within the tree that define that environment object, which I'm sure will cause un-needed redraws and performance issues (and as you've seen).
A better mechanism is strongly limiting the views (or subviews) that need to display the constantly updating time, and pass in a reference to a timer publisher directly to them rather than layering it into the environment. Putting the timer itself into a singleton is one option but not critical to this, and won't effect the cascading redraws you're seeing.
How to use a timer with SwiftUI has a "shoving a timer into the view itself", which may work for what you're trying to do, but slightly better is the video here: https://www.hackingwithswift.com/books/ios-swiftui/triggering-events-repeatedly-using-a-timer
In Paul's example, he's stuffing the timer into the view itself - wouldn't be my choice, but for a simple real-time clock view it's not bad. You could just as easily pass in the timer publisher from an external object - like your singleton for example.

I've ended up using the following solution, combining #heckj suggestion and this one from #Mykel.
What I did was separating the AnyCancellable from the TimerPublishers by saving them in specific dictionaries of SingletonTimerManager.
Then, every time an ItemView is declared, I instantiate an autoconnected #State TimerPublisher. Every Timer instance now runs in the .common RunLoop, with a 0.5 tolerance to better help the perfomance as suggested by Paul here: Triggering events repeatedly using a timer
During the .onAppear() call of the ItemView, if a publisher with the same itemId already exists in SingletonTimerManager, I just assign that publisher to the one of my view.
Then I handle it like in #Mykel solution, with start and stopping both ItemView's publisher and SingletonTimerManager publisher.
The secondsPassed are shown in a text stored inside #State var seconds, which gets updated with a onReceive() attached to the ItemView's publisher.
I know that I'm probably creating too many publishers with this solution and I can't pinpoint exactly what happens when copying a publisher variable into another, but overall perfomance is much better now.
Sample Code:
SingletonTimerManager
class SingletonTimerManager {
static let singletonTimerManager = SingletonTimerManager()
var secondsPassed: [String: Int]
var cancellables: [String:AnyCancellable]
var publishers: [String: TimerPublisher]
func startTimer(itemId: String) {
self.secondsPassed[itemId] = 0
self.publisher[itemId] = Timer
.publish(every: 1, tolerance: 0.5, on: .main, in: .common)
self.cancellables[itemId] = self.publisher[itemId]!.autoconnect().sink(receiveValue: {_ in self.secondsPassed[itemId] += 1})
}
func isTimerValid(_ itemId: String) -> Bool {
if(self.cancellables[itemId] != nil && self.publishers[itemId] != nil) {
return true
}
return false
}
}
ContentView
struct ContentView: View {
var itemIds: [String]
var body: some View {
VStack {
ForEach(self.itemIds, id: \.self) { itemId in
ItemView(itemId: itemId)
}
}
}
}
struct ItemView: View {
var itemId: String
#State var seconds: Int
#State var timerPublisher = Timer.publish(every: 1, tolerance: 0.5, on: .main, in: .common).autoconnect()
var body: some View {
VStack {
Button("StartTimer") {
// Call startTimer in SingletonTimerManager....
self.timerPublisher = SingletonTimerManager.publishers[itemId]!
self.timerPublisher.connect()
}
Button("StopTimer") {
self.timerPublisher.connect().cancel()
// Call stopTimer in SingletonTimerManager....
}
Text("\(self.seconds)")
.onAppear {
// function that checks if the timer with this itemId is running
if(SingletonTimerManager.isTimerValid(itemId)) {
self.timerPublisher = SingletonTimerManager.publishers[itemId]!
self.timerPublisher.connect()
}
}.onReceive($timerPublisher) { _ in
self.seconds = SingletonTimerManager.secondsPassed[itemId] ?? 0
}
}
}
}

Related

Modifying user input in Swift

I'm trying to learn Swift and have gone through several tutorials, however, I seem to be going in circles and need some direction on how to solve this problem.
My goal is to create a decrementing timer, given a user-input (in seconds), in this case I've chosen a Stepper to -/+ the value. Then begin decrementing the timer on a button press, in this case "Decrement". The counter is displayed on the label.
This problem is super easy if I hard code the starting value, but what purpose would that serve for a UI Test. So, this was the "challenging" task I was able to think of to help understand how SwiftUI works.
The problem I'm encountering is the variable passed by the user is immutable. I have tried making copies of it or assigning it to other variables to manipulate but seem to be going in circles. A nudge in the right direction or a potential solution would go a long way.
struct ContentView: View {
#State private var timeInput: Int = 0
var timer = Timer()
var timeInputCopy: Int {
timeInput
}
var body: some View {
Stepper("Input", value: $timeInput, in: 0...150)
Button("Decrement", action: decrementFunction)
Label(String(timeInputCopy), image: "")
.labelStyle(TitleOnlyLabelStyle())
}
func decrementFunction() {
timer.invalidate()
timer = Timer.schedulerTimer(timeInterval: 1,
target: self,
selector: #selector(ContentView.timerClass),
userInfo: nil,
repeats: true)
}
func timerClass() {
timeInputCopy -= timeInputCopy
if (timeInputCopy == 0) {
timer.invalidate()
}
}
> Cannot assign to property: 'self' is immutable
> Mark method 'mutating' to make 'self' mutable
Attempting to auto-fix as Xcode recommends does not lead to a productive solution. I feel I am missing a core principle here.
As mentioned in my comments above:
timeInputCopy doesn't have a point -- it's not really a copy, it's just a computed property that returns timeInput
You won't have much luck with that form of Timer in SwiftUI with a selector. Instead, look at the Timer publisher.
Here's one solution:
import Combine
import SwiftUI
class TimerManager : ObservableObject {
#Published var timeRemaining = 0
private var cancellable : AnyCancellable?
func startTimer(initial: Int) {
timeRemaining = initial
cancellable = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
self.timeRemaining -= 1
if self.timeRemaining == 0 {
self.cancellable?.cancel()
}
}
}
}
struct ContentView: View {
#StateObject private var timerManager = TimerManager()
#State private var stepperValue = 60
var body: some View {
Stepper("Input \(stepperValue)", value: $stepperValue, in: 0...150)
Button("Start") {
timerManager.startTimer(initial: stepperValue)
}
Label("\(timerManager.timeRemaining)", image: "")
.labelStyle(TitleOnlyLabelStyle())
}
}
This could all be done in the View, but using the ObservableObject gives a nice separation of managing the state of the timer vs the state of the UI.

View don't update in real time when running a cycle

I'm making a card game in SwiftUI and having the following problem: when running a cycle, the view updates only on cycle stop, but don't show any changes when running. UI part of code is:
//on the table
ScrollView(.horizontal) {
HStack(alignment: .center, spacing: 0) {
if game.gameStarted {
ForEach ((0..<game.onTheTable.count), id: \.self) {number in
VStack {
Image(game.onTheTable[number].pic)
.resizable()
.modifier(CardStyle())
Text("\(ai.getPower(card: game.onTheTable[number]))")
}
}
}
}
}
It actually shows card images "on the table" when I move an item to the game.onTheTable array. But when I run a while loop like "while true" it behaves as I mentioned above. So I've created a simple code with a delay to be able to se how card images one by one appears on the table but it just doesn't work as expected. There's the code for the cycle:
func test() {
gameStarted = true
while deck.cardsInDeck.count > 0 {
onTheTable.append(deck.cardsInDeck[0])
deck.cardsInDeck.remove(at: 0)
usleep(100000)
}
}
Yes, it appends cards, but visually I see the result just when the whole cycle has finished. Any ideas how to fix that to see the cards being added in real time one by one?
SwiftUI is declarative, so it doesn't mesh well with imperative control flow like while loops or system timers. You don't have control over when layout happens. Instead, you need to modify the underlying state which is driving the view, and those updates must happen on the main thread.
Here's one approach, which starts the timer when the view appears. You could also trigger the timer based on user interaction.
Note that you can attach transitions to views, and those transitions can take advantage of .matchedGeometryEffect... So you could have cards animate from their position on the deck to their place on the table, and that could happen automatically as you move items from one array to another—as long as the deck and table views use the same namespace and a consistent ID for each unique card.
struct GameView: View {
#State var deckCards: [Card] = Card.standardDeck
#State var tableCards: [Card] = []
#State var timer: Timer? = nil
var body: some View {
VStack {
DeckView(cards: deckCards)
TableView(cards: tableCards)
}.onAppear {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
moveCard()
}
}
}
func moveCard() {
DispatchQueue.main.async {
guard deckCards.count > 0 else {
self.timer?.invalidate()
return
}
tableCards.append(deckCards.removeFirst())
}
}
}

UI Test for SwiftUI app does't find element with set accessibilityIdentifier after a timer has passed

The app:
The app has a tag cloud that adds and removes tags as Text views every few seconds to a ZStack using ForEach triggered by an ObservableObject. The ZStack has an accessibilityIdentifier set.
The UI test:
In the UI Test I have set a XCTWaiter first. After a certain period of time has passed I then check if the XCUIElement (ZStack) with the accessibilityIdentifier exists.
After that I query the ZStack XCUIElement for all descendants of type .staticText
I also query the XCUIApplication for its descendants of type .staticText
The following issues:
When the XCTWaiter is set too wait too long. It does not find the ZStack XCUIElement with its identifier anymore.
If the XCTWaiter is set to a low wait time or removed the ZStack XCUIElement will be found. But it will never find its descendants of type .staticText. They do exists though because I can find them as descendant of XCUIApplication itself.
My assumption:
I assume that the ZStack with its identifier can only be found by the tests as long as it does not have descendants. And because it doesn't have any descendants at this moment yet, querying the ZStack XCUIElement for its descendants later also fails because the XCUIElement seems to only represent the ZStack at the time it was captured.
Or maybe I have attached the accessibilityIdentifier for the ZStack at the wrong place or SwiftUI is removing it as soon as there are descendants and I should add identifiers to the descendants only. But that would mean I can only query descendants from XCUIApplication itself and never from another XCUIElement? That would make the .children(matching:) quite useless.
Here is the code for a single view iOS app in SwiftUI with tests enabled.
MyAppUITests.swift
import XCTest
class MyAppUITests: XCTestCase {
func testingTheInitialView() throws {
let app = XCUIApplication()
app.launch()
let exp = expectation(description: "Waiting for tag cloud to be populated.")
_ = XCTWaiter.wait(for: [exp], timeout: 1) // 1. If timeout is set higher
let tagCloud = app.otherElements["TagCloud"]
XCTAssert(tagCloud.exists) // 2. The ZStack with the identifier "TagCloud" does not exist anymore.
let tagsDescendingFromTagCloud = tagCloud.descendants(matching: .staticText)
XCTAssert(tagsDescendingFromTagCloud.firstMatch.waitForExistence(timeout: 2)) // 4. However, it never finds the tags as the descendants of the tagCloud
let tagsDescendingFromApp = app.descendants(matching: .staticText)
XCTAssert(tagsDescendingFromApp.firstMatch.waitForExistence(timeout: 2)) // 3. It does find the created tags here.
}
}
ContentView.swift:
import SwiftUI
struct ContentView: View {
private let timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
#ObservedObject var tagModel = TagModel()
var body: some View {
ZStack {
ForEach(tagModel.tags, id: \.self) { label in
TagView(label: label)
}
.onReceive(timer) { _ in
self.tagModel.addNextTag()
if tagModel.tags.count > 3 {
self.tagModel.removeOldestTag()
}
}
}.animation(Animation.easeInOut(duration: 4.0))
.accessibilityIdentifier("TagCloud")
}
}
class TagModel: ObservableObject {
#Published var tags = [String]()
func addNextTag() {
tags.append(String( Date().timeIntervalSince1970 ))
}
func removeOldestTag() {
tags.remove(at: 0)
}
}
struct TagView: View {
#State private var show: Bool = true
#State private var position: CGPoint = CGPoint(x: Int.random(in: 50..<250), y: Int.random(in: 100..<200))
let label: String
var body: some View {
let text = Text(label)
.position(position)
.opacity(show ? 0.0 : 1.0)
.onAppear {
show.toggle()
}
return text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
From what I can see it looks like you are NOT fulfilling the XCTestExpectation you set up with:
let exp = expectation(description: "Waiting...")
In the past, I have used the XCTestExpecation for asynchronous code to signal completion. So like in this example where I set up the XCTestExpectation and provide a description, then made a network call, and on completion of the network call ran exp.fulfill().
let exp = expectation(description: "Recieved success message after uploading onboarding model")
_ = UserController.upsertUserOnboardingModel().subscribe(onNext: { (response) in
expectation.fulfill()
}, onError: { (error) in
print("\(#function): \(error)")
XCTFail()
})
wait(for: [exp], timeout: 10)
Steps should be: Create Expectation -> Fulfil Expectation within X seconds.
I don't see any exp.fulfill() in your provided code so looks like that is a missing step.
Proposed Solutions:
A. So what you could do is fulfill your exp at some point during the number of
seconds specified in the timeout: x.
B. Or you want just wanting a delay you could use sleep(3)
C. Or if you want to wait for existence pick an element and use .waitForExistence(timeout: Int)
Example: XCTAssert(app.searchFields["Search for cars here"].waitForExistence(timeout: 5))

Escaping closure captures mutating self in Swift Combine Subscriber [duplicate]

GOAL: I'm trying to make a general struct that can take an array of Ints and go through and set a timer for each one (and show a screen) in succession.
Problem: I get Escaping closure captures mutating 'self' parameter error as shown in the code.
import SwiftUI
struct ContentView: View {
#State private var timeLeft = 10
#State private var timers = Timers(timersIWant: [6, 8, 14])
// var timersIWantToShow: [Int] = [6, 8, 14]
var body: some View {
Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
VStack {
Text("Hello, World! \(timeLeft)")
.foregroundColor(.white)
.background(Color.blue)
.font(.largeTitle)
}
}
}
struct Timers {
var countDownTimeStart: Int = 0
var currentTimer = 0
var timersIWant: [Int]
mutating func startTimer(with countDownTime: Int) {
var timeLeft = countDownTime
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in //Escaping closure captures mutating 'self' parameter
if timeLeft > 0 {
timeLeft -= 1
} else {
timer.invalidate()
self.currentTimer += 1
if self.currentTimer < self.timersIWant.count {
self.startTimer(with: self.timersIWant[self.currentTimer])
} else {
timer.invalidate()
}
}
})
}
}
}
I'm not sure if this has to do with my recursvie function (maybe this is bad form?) and I'm guessing the escaping closure is the func startTimer and the offending the 'self' parameter is the countDownTime parameter, but I'm not really sure what is happening or why it's wrong.
Escaping closure captures mutating 'self' parameter
The escaping closure is the Button's action parameter, and the mutating function is your startTimer function.
Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
A simple solution is to change Times to be a class instead of a struct.
Also notice that timeLeft is defined in two places. I don't think this is what you want.
As Gil notes, this needs to be a class because you are treating it as a reference type. When you modify currentTimer, you don't expect that to create a completely new Timers instance, which is what happens with a value type (struct). You expect it to modify the existing Timers instance. That's a reference type (class). But to make this work, there's quite a bit more you need. You need to tie the Timers to the View, or the View won't update.
IMO, the best way to approach this is let Timers track the current timeLeft and have the view observe it. I've also added an isRunning published value so that the view can reconfigure itself based on that.
struct TimerView: View {
// Observe timers so that when it publishes changes, the view is re-rendered
#ObservedObject var timers = Timers(intervals: [10, 6, 8, 14])
var body: some View {
Button(action: { self.timers.startTimer()} ) {
Text("Hello, World! \(timers.timeLeft)")
.foregroundColor(.white)
.background(timers.isRunning ? Color.red : Color.blue) // Style based on isRunning
.font(.largeTitle)
}
.disabled(timers.isRunning) // Auto-disable while running
}
}
// Timers is observable
class Timers: ObservableObject {
// And it publishes timeLeft and isRunning; when these change, update the observer
#Published var timeLeft: Int = 0
#Published var isRunning: Bool = false
// This is `let` to get rid of any confusion around what to do if it were changed.
let intervals: [Int]
// And a bit of bookkeeping so we can invalidate the timer when needed
private var timer: Timer?
init(intervals: [Int]) {
// Initialize timeLeft so that it shows the upcoming time before starting
self.timeLeft = intervals.first ?? 0
self.intervals = intervals
}
func startTimer() {
// Invalidate the old timer and stop running, in case we return early
timer?.invalidate()
isRunning = false
// Turn intervals into a slice to make popFirst() easy
// This value is local to this function, and is captured by the timer callback
var timerLengths = intervals[...]
guard let firstInterval = timerLengths.popFirst() else { return }
// This might feel redundant with init, but remember we may have been restarted
timeLeft = firstInterval
isRunning = true
// Keep track of the timer to invalidate it elsewhere.
// Make self weak so that the Timers can be discarded and it'll clean itself up the next
// time it fires.
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else {
timer.invalidate()
return
}
// Decrement the timer, or pull the nextInterval from the slice, or stop
if self.timeLeft > 0 {
self.timeLeft -= 1
} else if let nextInterval = timerLengths.popFirst() {
self.timeLeft = nextInterval
} else {
timer.invalidate()
self.isRunning = false
}
}
}
}

Why can't I mutate a variable initially set to a certain parameter when the func was called?

GOAL: I'm trying to make a general struct that can take an array of Ints and go through and set a timer for each one (and show a screen) in succession.
Problem: I get Escaping closure captures mutating 'self' parameter error as shown in the code.
import SwiftUI
struct ContentView: View {
#State private var timeLeft = 10
#State private var timers = Timers(timersIWant: [6, 8, 14])
// var timersIWantToShow: [Int] = [6, 8, 14]
var body: some View {
Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
VStack {
Text("Hello, World! \(timeLeft)")
.foregroundColor(.white)
.background(Color.blue)
.font(.largeTitle)
}
}
}
struct Timers {
var countDownTimeStart: Int = 0
var currentTimer = 0
var timersIWant: [Int]
mutating func startTimer(with countDownTime: Int) {
var timeLeft = countDownTime
Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: { timer in //Escaping closure captures mutating 'self' parameter
if timeLeft > 0 {
timeLeft -= 1
} else {
timer.invalidate()
self.currentTimer += 1
if self.currentTimer < self.timersIWant.count {
self.startTimer(with: self.timersIWant[self.currentTimer])
} else {
timer.invalidate()
}
}
})
}
}
}
I'm not sure if this has to do with my recursvie function (maybe this is bad form?) and I'm guessing the escaping closure is the func startTimer and the offending the 'self' parameter is the countDownTime parameter, but I'm not really sure what is happening or why it's wrong.
Escaping closure captures mutating 'self' parameter
The escaping closure is the Button's action parameter, and the mutating function is your startTimer function.
Button(action: {self.timers.startTimer(with: self.timeLeft)}) {
A simple solution is to change Times to be a class instead of a struct.
Also notice that timeLeft is defined in two places. I don't think this is what you want.
As Gil notes, this needs to be a class because you are treating it as a reference type. When you modify currentTimer, you don't expect that to create a completely new Timers instance, which is what happens with a value type (struct). You expect it to modify the existing Timers instance. That's a reference type (class). But to make this work, there's quite a bit more you need. You need to tie the Timers to the View, or the View won't update.
IMO, the best way to approach this is let Timers track the current timeLeft and have the view observe it. I've also added an isRunning published value so that the view can reconfigure itself based on that.
struct TimerView: View {
// Observe timers so that when it publishes changes, the view is re-rendered
#ObservedObject var timers = Timers(intervals: [10, 6, 8, 14])
var body: some View {
Button(action: { self.timers.startTimer()} ) {
Text("Hello, World! \(timers.timeLeft)")
.foregroundColor(.white)
.background(timers.isRunning ? Color.red : Color.blue) // Style based on isRunning
.font(.largeTitle)
}
.disabled(timers.isRunning) // Auto-disable while running
}
}
// Timers is observable
class Timers: ObservableObject {
// And it publishes timeLeft and isRunning; when these change, update the observer
#Published var timeLeft: Int = 0
#Published var isRunning: Bool = false
// This is `let` to get rid of any confusion around what to do if it were changed.
let intervals: [Int]
// And a bit of bookkeeping so we can invalidate the timer when needed
private var timer: Timer?
init(intervals: [Int]) {
// Initialize timeLeft so that it shows the upcoming time before starting
self.timeLeft = intervals.first ?? 0
self.intervals = intervals
}
func startTimer() {
// Invalidate the old timer and stop running, in case we return early
timer?.invalidate()
isRunning = false
// Turn intervals into a slice to make popFirst() easy
// This value is local to this function, and is captured by the timer callback
var timerLengths = intervals[...]
guard let firstInterval = timerLengths.popFirst() else { return }
// This might feel redundant with init, but remember we may have been restarted
timeLeft = firstInterval
isRunning = true
// Keep track of the timer to invalidate it elsewhere.
// Make self weak so that the Timers can be discarded and it'll clean itself up the next
// time it fires.
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
guard let self = self else {
timer.invalidate()
return
}
// Decrement the timer, or pull the nextInterval from the slice, or stop
if self.timeLeft > 0 {
self.timeLeft -= 1
} else if let nextInterval = timerLengths.popFirst() {
self.timeLeft = nextInterval
} else {
timer.invalidate()
self.isRunning = false
}
}
}
}