I have not been able to find anything through the standard Google search, but is there any reason why the ContentView is not updating through the ObservableObject? Feel like I am missing something but I am not quite sure what.
import SwiftUI
import PlaygroundSupport
let start = Date()
let seconds = 10.0 * 60.0
func timeRemaining(minutes: Int, seconds: Int) -> String {
return "\(minutes) minutes \(seconds) seconds"
}
class ViewData : ObservableObject {
#Published var timeRemaining: String = "Loading..."
}
// View
struct ContentView: View {
#ObservedObject var viewData: ViewData = ViewData()
var body: some View {
VStack {
Text(viewData.timeRemaining)
}
}
}
let contentView = ContentView()
let viewData = contentView.viewData
let hosting = UIHostingController(rootView: contentView)
// Timer
let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now(), repeating: .seconds(1))
timer.setEventHandler {
let diff = -start.timeIntervalSinceNow
let remaining = seconds - diff
let mins = Int(remaining / 60.0)
let secs = Int(remaining) % 60
let timeRemaning = timeRemaining(minutes: mins, seconds: secs)
viewData.timeRemaining = timeRemaning
print(timeRemaning)
}
timer.resume()
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
timer.cancel()
PlaygroundPage.current.finishExecution()
}
PlaygroundPage.current.setLiveView(contentView)
PlaygroundPage.current.needsIndefiniteExecution = true
The reason is that GCD based timer works on own queue, so here is the fix - view model have to be updated on main, UI, queue as below
DispatchQueue.main.async {
viewData.timeRemaining = timeRemaning
}
The main utility of GCD timers over standard timers is that they can run on a background queue. Like Asperi said, you can dispatch the updates to the main queue if your GCD timer isn’t using the main queue, itself.
But, you might as well just schedule your GCD timer on the main queue from the get go, and then you don’t have to manually dispatch to the main queue at all:
let timer = DispatchSource.makeTimerSource(queue: .main)
But, if you’re going to run this on the main thread, you could just use a Timer, or, in SwiftUI projects, you might prefer Combine’s TimerPublisher:
import Combine
...
var timer: AnyCancellable? = nil
timer = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { _ in
let remaining = start.addingTimeInterval(seconds).timeIntervalSince(Date())
guard remaining >= 0 else {
viewData.timeRemaining = "done!"
timer?.cancel()
return
}
let mins = Int(remaining / 60.0)
let secs = Int(remaining) % 60
viewData.timeRemaining = timeRemaining(minutes: mins, seconds: secs)
}
When you incorporate your timer within your SwiftUI code (rather than a global like here), it’s nice to stay within the Combine Publisher paradigm.
I also think it’s probably cleaner to cancel the timer when it expires in the timer handler, rather than doing a separate asyncAfter.
Unrelated, but you might consider using DateComponentsFormatter in your timeRemaining function, e.g.:
let formatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second]
formatter.unitsStyle = .full
return formatter
}()
func timeRemaining(_ timeInterval: TimeInterval) -> String {
formatter.string(from: timeInterval) ?? "Error"
}
Then,
It gets you out of the business of calculating minutes and seconds yourself;
The string will be localized; and
It will ensure grammatically correct wording; e.g. when there are 61 seconds left and the existing routine will report a grammatically incorrect “1 minutes, 1 seconds”.
DateComponentsFormatter gets you out of the weeds of handling these sorts of edge cases, where you want singular instead of plural or languages other than English.
Related
I'm working on an app where I'm using a timer to count down time left in a workout and also count up for total time. My counters are out of sync, looks like it's less than a second off. I'm wondering if it has something to do with #Publish, maybe one fires before the other. Any idea what's happening and how to fix it?
class TimeManager: ObservableObject {
#Published var totalTime: Double = 0.0
#Published var timeRemaining: Double = 180.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2.0) {
self.timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] timer in
guard let self = self else { return }
self.timeRemaining -= 0.1
self.totalTime += 0.1
}
}
}
then my view
#ObservedObject var timeManager = TimeManager()
...
var body: some View {
VStack {
let time = timeManager.timeRemaining
let minutes = Int(time) / 60 % 60
let seconds = Int(time) % 60
ZStack {
Progress()
Text(String(format:"%02i:%02i", minutes, seconds))
.font(.system(size: 60))
}
let total = timeManager.totalTime
let totalMins = Int(total) / 60 % 60
let totalSecs = Int(total) % 60
Text(String(format:"%02i:%02i", totalMins, totalSecs))
.font(.system(size: 40))
}
}
Your time values are in sync. The reason for the behaviour you are seeing is the Double / Int conversions and the rounding applied while display the Texts. Try this line:
Text("\(timeManager.timeRemaining + timeManager.totalTime)")
and you will see this allways adding up to 180.
You could try Int values in your Viewmodel decrementing/incrementing by 1 and a DateComponentsFormatter to format the values in your View.
let componentsFormatter = DateComponentsFormatter()
Text("\(componentsFormatter.string(from: Double(timeManager.timeRemaining)) ?? "NAN")")
.font(.system(size: 60))
You would of course need to tweek the formatter to display the time the way you want it to be. But I agree with Paulw11. This seems like a bad design. It would be better to have a single source of truth as a Date and go from there.
Maybe calculate your 2nd value based on the first when you decrement the time. For example like this:
remaining = (180 - total) >= 0 ? (180 - total) : 0
when i run code, i click iphone home button, then reopen my app,my app is frozen.i feel uncertain,why?
let queue = DispatchQueue(label: "com.aiswei.asw.monitior.search",qos: .default, attributes: .concurrent)
let semaphore = DispatchSemaphore(value: 30)
let minX = 2
let maxX = 254
self.index = minX
self.index = minX
let count = maxX - minX + 1
let lock = NSLock()
for i in minX...maxX {
queue.async {
semaphore.wait()
lock.lock()
// request some server api result
NetCenter.requestAPi { (result) in
semaphore.signal()
}
lock.unlock()
}
}
I would expect this to try to create over 250 threads, which you don't want to do. That said, my suspicion is that the actual problem is with coming in the foreground is elsewhere; possibly in NetCenter. You're probably deadlocking the main queue (especially if NetCenter is written in the same style).
Rather than kicking off a ton of DispatchWorkItems and then blocking them, you can achieve the same thing by using a single DispatchWorkItem on a serial queue, with your semaphore. The serial queue will get rid of the need for the lock. This serializes 30 operations at a time, which is what I think you want.
let queue = DispatchQueue(label: "com.aiswei.asw.monitior.search")
let semaphore = DispatchSemaphore(value: 30)
queue.async {
let minX = 2
let maxX = 254
for _ in minX...maxX {
semaphore.wait()
NetCenter.requestAPi { (result) in
semaphore.signal()
}
}
}
}
I'm assuming here that requestAPi is an async method and will schedule its completion handler on some queue other than queue. (If NetCenter uses the same queue as this function, than that would definitely deadlock.)
I am wanting to have a stopwatch in my app that runs completely off the device's time. I have my code below which takes the time in which the start button is pressed, and then every second updates the secondsElapsed to be the difference between the startTime and current. I am getting stuck on implementing a pause function. If I just invalidate the update timer, then the timer will restart having pretty much carried on from where it left off. Any ideas on how this could be done?
class StopWatchManager: ObservableObject{
#Published var secondsElapsed = 0
var startTime: Date = Date()
var timer = Timer()
func startWatch(){
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
let current = Date()
let diffComponents = Calendar.current.dateComponents([.second], from: self.startTime, to: current)
let seconds = (diffComponents.second ?? 0)
self.secondsElapsed = seconds
}
}
func pauseWatch(){
timer.invalidate()
}
}
I display the stopwatch using this code below:
struct ContentView: View {
#ObservedObject var stopWatchManager = StopWatchManager()
var body: some View{
HStack{
Button("Start"){
stopWatchManager.startWatch()
}
Text("\(stopWatchManager.secondsElapsed)")
Button("Pause"){
stopWatchManager.pauseWatch()
}
}
}
}
Yes. Here is how to do it:
When pause is pressed, note the current time and compute the elapsed time for the timer. Invalidate the update timer.
When the timer is resumed, take the current time and subtract the elapsed time. Make that the startTime and restart the update timer.
Here's the updated code:
class StopWatchManager: ObservableObject{
#Published var secondsElapsed = 0
var startTime: Date = Date()
var elapsedTime = 0.0
var paused = false
var running = false
var timer = Timer()
func startWatch(){
guard !running else { return }
if paused {
startTime = Date().addingTimeInterval(-elapsedTime)
} else {
startTime = Date()
}
paused = false
running = true
timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true){ timer in
let current = Date()
let diffComponents = Calendar.current.dateComponents([.second], from: self.startTime, to: current)
let seconds = (diffComponents.second ?? 0)
self.secondsElapsed = seconds
}
}
func pauseWatch(){
guard !paused else { return }
timer.invalidate()
elapsedTime = Date().timeIntervalSince(startTime)
paused = true
running = false
}
}
Things to note:
I changed the timer interval to 0.1 from 1 to avoid missing updates.
I added paused and running state variables to keep the buttons from being pressed more than once in a row.
Using the code below I'm attempting to limit the times that a BGProcessingTasRequest is called. I don't need it to run multiple times a day, only once and I want to be respectful to the user's battery life. However in testing the code, it never runs (even when substituting one hour for one day). If I comment out the guard statement then the task gets scheduled several times an hour. Is there a logical error here, or something I am not thinking of? (I know that when the processing task fires my Date is saving to UserDefaults properly).
private func schedulePersonalRecordsProcessingTask() {
let request = BGProcessingTaskRequest(identifier: "com.myndarc.personalRecordsProcessing")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
request.earliestBeginDate = Date(timeIntervalSinceNow: 60)
//Only update PRs no more than once a day
let now = Date()
let oneDay = TimeInterval(24 * 60 * 60)
let lastBGTaskUpdateDate = defaults.value(forKey: backgroundTaskForPRLastDateKey) as? Date ?? .distantPast
guard now > (lastBGTaskUpdateDate + oneDay) else {
return
}
do {
try BGTaskScheduler.shared.submit(request)
}
catch {
print("Could not schedule app refresh: \(error)")
}
}
I've done a simple timer in Swift. All is well apart from when the seconds reach 59 seconds. Instead of going back to zero they just carry on going. Would someone would be able to point out where I'm going wrong and why this is happening?
#IBAction func startButtonDidTouch(_ sender: Any) {
if !timerIsRunning{
timer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.updateTimer), userInfo: nil, repeats: true)
timerIsRunning = true
}
}
#objc func updateTimer() {
totalSeconds += 0.01
let totalSecondsTimes100: Int = Int(totalSeconds*100)
let minutes = Int(totalSeconds/60)
let timerChoice = Double(minutes)
let minStr = (minutes == 0) ? "00" : "0\(minutes)"
let secStr = (totalSeconds < 9) ? "0\(Float(totalSecondsTimes100)/100)" : "\(Float(totalSecondsTimes100)/100)"
switch Int(timerChoice) {
case Int(timerCountdownLabel.text!)!:
timerLabel.text = "\(minStr):\(secStr)"
audioPlayer.play()
timer.invalidate()
timerIsRunning = false
default:
timerLabel.text = "\(minStr):\(secStr)"
}
}
You should calculate the seconds as:
let seconds = totalSeconds % 60
and then use seconds in your calculation of secStr instead of using totalSeconds.
There are better ways to write your code:
#objc func updateTimer() {
totalSeconds += 0.01
let minutes = Int(totalSeconds) / 60
let seconds = totalSeconds.remainder(dividingBy: 60)
let timeStr = String(format: "%02d:%06.3f", minutes, seconds)
timerLabel.text = timeStr
if Int(timerCountdonwLabel.text!)! == minutes {
audioPlayer.play()
timer.invalidate()
timerIsRunning = false
}
}
And you really shouldn't keep track of time simply by adding 0.01 to totalSeconds. A Timer is not accurate. Your clock will drift over time. It's best to save a timestamp (Date()) when you start the timer and get the current timestamp (Date()) inside updateTimer and get the difference between the two.
Here is a timer function that outputs format minutes:seconds:milliseconds, compare with your code and you'll find what's wrong with your code.
private weak var timer: Timer?
private var startTime: Double = 0
private var elapsed: Double = 0
private var time: Double = 0
private func startTimer(){
startTime = Date().timeIntervalSinceReferenceDate - elapsed
timer = Timer.scheduledTimer(timeInterval: (0.01), target: self, selector: #selector(updateTimeLabel), userInfo: nil, repeats: true)
}
private func stopTimer(){
elapsed = Date().timeIntervalSinceReferenceDate - startTime
timer?.invalidate()
}
#objc func updateTimeLabel(){
time = Date().timeIntervalSinceReferenceDate - startTime
let minutes = UInt8(time / 60.0)
let timeNoMin = time - (TimeInterval(minutes) * 60)
let seconds = UInt8(timeNoMin)
let timeNoSec = timeNoMin - (TimeInterval(seconds))
let milliseconds = UInt16(timeNoSec * 100)
let strMinutes = String(minutes)
var strSeconds = ""
if strMinutes == "0" {
strSeconds = String(seconds)
}
else {
strSeconds = String(format: "%02d", seconds)
}
let strMilliseconds = String(format: "%02d"), milliseconds)
if strMinutes != "0" {
timerLabel.text = "\(strMinutes):\(strSeconds).\(strMilliseconds)"
}
else {
timerLabel.text = "\(strSeconds).\(strMilliseconds)"
}
}
To get minutes and seconds from a floating point total number of seconds elapsed, elapsed you can:
To get minutes, divide by 60.0 and truncate to the nearest integer:
let minutes = Int(elapsed / 60)
To get seconds, get the remainder, either via:
let seconds = elapsed - Double(minutes) * 60
Or
let seconds = elapsed.truncatingRemainder(dividingBy: 60)
A couple of other observations:
There's no point in running a timer every 0.01 seconds when the screen refresh rate is usually capped at 60 frames per second. If you want to update it with the greatest frequency, use a CADisplayLink which is timed not only for maximum screen refresh rate, but also fires optimally to allow the update to happen before the next frame is to be rendered.
You should not use timer to increment the elapsed time by 0.01 (or any fixed interval) because you have no assurances that it will actually fire with that frequency. If something, for example, momentarily blocks the main thread by 200 milliseconds, you don't want this to affect your calculation of the amount of time that has elapsed.
Instead, save the start time when the timer starts, and every time the timer fires recalculate the elapsed time and format the results accordingly.
To complicate this further, you should not even be comparing Date() instances (or CFAbsoluteTimeGetCurrent() values) because, as the documentation warns us:
Repeated calls to this function do not guarantee monotonically increasing results. The system time may decrease due to synchronization with external time references or due to an explicit user change of the clock.
Instead, you should use a mach_absolute_time based calculation (such as returned by CACurrentMediaTime()), for which repeated calls are assured to return accurately elapsed time calculations.
The only time you should use Date() or CFAbsoluteTimeGetCurrent() if your app is saving the start time in persistent storage, to be retrieved later when the app is restarted (possibly after a device reboot) to render the effect of the elapsed time between starts of an app. But this is a pretty narrow edge case.
Anyway, this yields:
var start: CFTimeInterval?
weak var displayLink: CADisplayLink?
func startTimer() {
self.displayLink?.invalidate() // just in case timer had already been started
start = CACurrentMediaTime()
let displayLink = CADisplayLink(target: self, selector: #selector(handleDisplayLink(_:)))
displayLink.preferredFramesPerSecond = 100 // in case you're using a device that can render more than 60 fps
displayLink.add(to: .main, forMode: .commonModes)
self.displayLink = displayLink
}
#objc func handleDisplayLink(_ displayLink: CADisplayLink) {
let elapsed = CACurrentMediaTime() - start!
let minutes = Int(elapsed / 60)
let seconds = elapsed - Double(minutes) * 60
let string = String(format: "%02d:%05.2f", minutes, seconds)
label.text = string
}
func stopTimer() {
displayLink?.invalidate()
}