update label from background timer
Instead of trying to run a timer in the background, record the startDate
of the start of your workout and compute the time interval. That way, the app doesn't actually have to run in the background to keep track of the workout time. The timer will only be used to update the user interface.
Pausing now works by recording the current workout interval. When the workout restarts, it subtracts the current workout interval from the Date()
to get a new adjusted startDate
.
Add notifications for the app entering the background and foreground so that you can restart the UI update timer if the workout is active:
import UIKit
enum WorkoutState {
case inactive
case active
case paused
}
class ViewController: UIViewController {
var workoutState = WorkoutState.inactive
var workoutInterval = 0.0
var startDate = Date()
var timer = Timer()
@IBOutlet weak var outputLabel: UILabel!
@IBOutlet weak var start: UIButton!
@IBOutlet weak var paused: UIButton!
@IBAction func startButton(_ sender: UIButton) {
startButtonPressed()
}
@IBAction func pausedButton(_ sender: UIButton) {
pausedButtonPressed()
}
@IBOutlet weak var timerLabel: UILabel!
func updateTimerLabel() {
let interval = -Int(startDate.timeIntervalSinceNow)
let hours = interval / 3600
let minutes = interval / 60 % 60
let seconds = interval % 60
timerLabel.text = String(format:"%02i:%02i:%02i", hours, minutes, seconds)
}
func startButtonPressed() {
if workoutState == .inactive {
startDate = Date()
} else if workoutState == .paused {
startDate = Date().addingTimeInterval(-workoutInterval)
}
workoutState = .active
outputLabel.text = "Workout Started"
start.isHidden = true
paused.isHidden = false
updateTimerLabel()
_foregroundTimer(repeated: true)
print("Calling _foregroundTimer(_:)")
}
func pausedButtonPressed(){
// record workout duration
workoutInterval = floor(-startDate.timeIntervalSinceNow)
outputLabel.text = "Workout Paused"
workoutState = .paused
timer.invalidate()
pauseWorkout()
}
func pauseWorkout(){
paused.isHidden = true
start.isHidden = false
}
func _foregroundTimer(repeated: Bool) -> Void {
NSLog("_foregroundTimer invoked.");
//Define a Timer
self.timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.timerAction(_:)), userInfo: nil, repeats: true);
print("Starting timer")
}
@objc func timerAction(_ timer: Timer) {
print("timerAction(_:)")
self.updateTimerLabel()
}
@objc func observerMethod(notification: NSNotification) {
if notification.name == .UIApplicationDidEnterBackground {
print("app entering background")
// stop UI update
timer.invalidate()
} else if notification.name == .UIApplicationDidBecomeActive {
print("app entering foreground")
if workoutState == .active {
updateTimerLabel()
_foregroundTimer(repeated: true)
}
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(observerMethod), name: .UIApplicationDidEnterBackground, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(observerMethod), name: .UIApplicationDidBecomeActive, object: nil)
print("viewDidLoad()")
print("Hiding buttons")
paused.isHidden = true
start.isHidden = false
print("Clearing Labels")
outputLabel.text = ""
timerLabel.text = ""
print("\(timer)")
timer.invalidate()
}
}
Original Answer
Just call updateTimerLabel()
on the main loop:
DispatchQueue.main.async {
self.updateTimerLabel()
}
Full function:
@objc func _backgroundTimerAction(_ timer: Timer) {
print("_backgroundTimerAction(_:)")
time += 1
DispatchQueue.main.async {
self.updateTimerLabel()
}
NSLog("time count -> \(time)")
}
Notes:
- Running the timer on a background thread isn't buying you anything but trouble in setting it up. I'd recommend just running it on the main thread.
- There is no need to add
-> Void
to a Swift function definition; that is the default. - Swift typically doesn't need the semicolons
;
, so lose those. self.time
is already anInt
, so creating a newInt
from it is unnecessary.replace:
let hours = Int(self.time) / 3600
with:
let hours = self.time / 3600
Update label when returning from background
You need to add your observers inside viewDidLoad()
, NOT in viewDidAppear
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(teasGoBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(runTimer), name: UIApplication.didBecomeActiveNotification, object: nil)
}
SwiftUI update Textlabel from Background Timer
Change your body
to.
var body: some View {
VStack {
Text("\(timeLeft)")
Button(action: {
self.start.toggle()
self.notifyTime = Date().addingTimeInterval(TimeInterval(10)) // add countdown time in sec
self.timeLeft = timeDifference(endTime: self.notifyTime)
self.sendNotification()
//Timer code - Start your timer
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
timeLeft -= 1
if timeLeft == 0 {
timer.invalidate()
//Add your new trigger event
}
}
}) { Text("Start Countdown (10s)")}
}.onAppear(perform: {
UNUserNotificationCenter.current().requestAuthorization(options: [.badge,.sound,.alert]) { (_, _) in
}
})
}
How to run a timer on the background that updates a label in a viewmodel
You are wrongly assigning the event handler as seen in the docs.
Change
timer.Elapsed += new EventHandler(UpdateLabel);
to
timer.Elapsed +=UpdateLabel; //or
timer.Elapsed +=System.EventHandler(UpdateLabel)
not sure from the top of my head
And
private async void UpdateLabel()
to
private static void UpdateLabel(Object source, System.Timers.ElapsedEventArgs e)
This should allow you to to trigger the Elapsed event. You problem was your confusing regular functions and event functions. You should read up on it here
Update label text with timer on UI Thread, without blocking the UI
to understand how i can run a method on my UI thread, and still interact with the UI. Therefore, using a Timer is not a possibility in this case i'm afraid.
You can use a thread (via Task.Run
) and then use Device.InvokeOnMainThreadAsync
or Device.BeginInvokeOnMainThread
to execute UI updates within that "background" thread:
Task.Run(async() =>
{
while (true)
{
await Device.InvokeOnMainThreadAsync(() =>
{
test.Text = i;
});
await Task.Delay(10000);
i++;
}
});
Note: Also do not use Thread.Sleep
as that will hang the entire thread, including its message pump (a really bad thing when it is the UI/main thread).
Updating Winforms Label with Timer and Thread, stock app
Let me point out several things in your implementation.
- You subscribe to timer.Elapsed after timer.Start that might be invalid in case of a short-timer interval
- The event handler is called in background that's why you continuously get "Cross-thread operation not valid". UI components should be dispatched correctly from background threads, for example, by calling flowLayoutPanel2.BeginInvoke(new Action(() => flowLayoutPanel2.Controls.Add(label))); and label.BeginInvoke(new Action(label.Update)). This change already would fix your exception.
- Despite the fact that I would implement this functionality in a different way, here I post slightly changed code that just does exactly what you need with some tweaks.
public partial class Form1 : Form
{
Task _runningTask;
CancellationTokenSource _cancellationToken;
public Form1()
{
System.Globalization.CultureInfo.DefaultThreadCurrentUICulture = System.Globalization.CultureInfo.GetCultureInfo("en-US");
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
using (Prompt prompt = new Prompt("Enter the ticker symbol", "Add ticker"))
{
string result = prompt.Result;
result = result.ToUpper();
if (!string.IsNullOrEmpty(result))
{
do_Things(result);
_cancellationToken = new CancellationTokenSource();
_runningTask = StartTimer(() => do_Things(result), _cancellationToken);
}
}
}
private void onCancelClick()
{
_cancellationToken.Cancel();
}
public async Task<string> getStockPrices(string symbol)
{
try
{
var securities = await Yahoo.Symbols(symbol).Fields(Field.RegularMarketPrice).QueryAsync();
var aapl = securities[symbol];
var price = aapl[Field.RegularMarketPrice];
return symbol + " $" + price;
}
catch
{
return "404";
}
}
private async Task StartTimer(Action action, CancellationTokenSource cancellationTokenSource)
{
try
{
while (!cancellationTokenSource.IsCancellationRequested)
{
await Task.Delay(1000, cancellationTokenSource.Token);
action();
}
}
catch (OperationCanceledException) { }
}
public async void do_Things(string result)
{
var price = await getStockPrices(result);
var label = new Label() { Name = result, Text = result + " $" + price };
flowLayoutPanel2.BeginInvoke(new Action(() => flowLayoutPanel2.Controls.Add(label)));
}
}
Timer that updates label running in background
First of all keep in mind that you cannot perform UI changes in any other thread than the main thread. Having said that, you need the NSTimer
to fire in the main queue, otherwise the program will crash when changing the UILabel
. Have a look at this links http://bynomial.com/blog/?p=67 and this one http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSRunLoop_Class/Reference/Reference.html
To my knowledge, if you schedule the timer in the for the NSRunLoopCommonModes
it will ignore event updates and fire the timer jsut how you want it:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
target:self
selector:@selector(timerDidTick:)
userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-(void) timerDidTick:(NSTimer*) theTimer{
[[self myLabel] setText:@"Timer ticked!"];
}
How to update a UILabel timer after being in background
Store Date() in NSUserDefaults when you go to background in
func applicationDidEnterBackground(_ application: UIApplication) {}
then calculate difference between new Date when you go to foreground in
func applicationWillEnterForeground(_ application: UIApplication) {}
How update a label periodically on iOS (every second)?
The problem is that a scheduledTimer will not get called while the main thread is tracking touches. You need to schedule the timer in the main run loop.
So instead of doing
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(updateLabel:) userInfo:nil repeats:YES];
use
NSTimer* timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(updateLabel:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Related Topics
How to Disable Floating Headers in Uitableview with Uitableviewstyleplain
Check If Uicolor Is Dark or Bright
Uicollectionview Decoration View
Kvo and Arc How to Removeobserver
iOS Enterprise Ota Distribution Unable to Download Application
Implement Dark Mode Switch in Swiftui App
Save Image Data to SQLite Database in Iphone
How to Create a Hex Color String Uicolor Initializer in Swift
How to Add a Button to the Mkpointannotation
Adding Blur Effect to Background in Swift
Analysing Assets.Car File in iOS
Ios4: How to Use Video File as an Opengl Texture
iOS 11 - Keyboard Height Is Returning 0 in Keyboard Notification
Nsdateformatter and Current Language in iOS11
Could Not Load the "" Image Referenced from a Nib in the Bundle with Identifier