Is it possible to refresh a timer in a Today Widget?
Yes you can. I have just tested and it works. You just have to add your timer to the main run loop NSRunLoopCommonModes:
RunLoop.main.add(yourTimerName, forMode: .commonModes)
import NotificationCenter
class TodayViewController: UIViewController, NCWidgetProviding {
@IBOutlet weak var strTimer: UILabel!
var timer = Timer()
func updateInfo() {
strTimer.text = Date().description
}
override func viewDidLoad() {
super.viewDidLoad()
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(updateInfo), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .commonModes)
}
func widgetPerformUpdate(completionHandler: @escaping (NCUpdateResult) -> Void) {
completionHandler(.newData)
}
}
How to refresh multiple timers in widget iOS14?
Why it's not working
I'll start by explaining why your current approach is not working as you expected.
Let's assume you're in the getTimeline
function and you want to pass the duration
to the entry (for this example let's assume duration = 15
).
Currently the duration
describes seconds and is relative. So duration = 15
means that after 15 seconds the timer fires and should display "00:00"
.
If you have one timer only, the approach described in SwiftUI iOS 14 Widget CountDown will work (see also Stopping a SwiftUI Widget's relative textfield counter when hits zero?). After 15 seconds you just re-create the timeline and that's fine. Whenever you're in the getTimeline
function you know that the timer has just finished (or is about to start) and you're in the starting point.
The problem starts when you have more than one timer. If duration
is relative how do you know in which state you are when you're entering getTimeline
? Every time you read duration
from Core Data it will be the same value (15 seconds
). Even if one of the timers finishes, you'll read 15 seconds
from Core Data without knowing of the timer's state. The status
property won't help here as you can't set it to finished
from inside the view nor pass it to getTimeline
.
Also in your code you have:
let duration = timerEntities?[0].duration ?? 0
I assume that you if you have many timers, they can have different durations and more than one timer can be running at the same time. If you choose the duration
of the first timer only, you may fail to refresh the view when faster timers are finished.
You also said:
The timer runs every second.
But you can't do this with Widgets. They are not suited for every-second operations and simply won't refresh so often. You need to refresh the timeline when any of timers ends but no sooner.
Also, you set the timeline to run only once:
let timeline = Timeline(entries: entries, policy: .never)
With the above policy
your getTimeline
won't be called again and your view won't be refreshed either.
Lastly, let's imagine you have several timers that fire in the span of an hour (or even a minute). Your widget has a limited number of refreshes, generally it's best to assume no more than 5 refreshes per hour. With your current approach it's possible to use the daily limit in minutes or even seconds.
How to make it work
Firstly, you need a way to know in which state your timers are when you are in the getTimeline
function. I see two ways:
(Unrecommended) Store the information of timers that are about to finish in
UserDefaults
and exclude them in the next iteration (and setstatus
tofinished
). This, however, is still unreliable as the timeline can theoretically be refreshed before the next refresh date (set in theTimelineReloadPolicy
).Change the
duration
to be absolute, not relative. Instead ofDouble
/Int
you can make it to beDate
. This way you'll always now whether the timer is finished or not.
Demo
struct TimerEntity: Identifiable {
let id = UUID()
var task: String
var endDate: Date
}
struct TimerEntry: TimelineEntry {
let date: Date
var timerEntities: [TimerEntity] = []
}
struct Provider: TimelineProvider {
// simulate entities fetched from Core Data
static let timerEntities: [TimerEntity] = [
.init(task: "timer1", endDate: Calendar.current.date(byAdding: .second, value: 320, to: Date())!),
.init(task: "timer2", endDate: Calendar.current.date(byAdding: .second, value: 60, to: Date())!),
.init(task: "timer3", endDate: Calendar.current.date(byAdding: .second, value: 260, to: Date())!),
]
// ...
func getTimeline(in context: Context, completion: @escaping (Timeline<TimerEntry>) -> Void) {
let currentDate = Date()
let timerEntities = Self.timerEntities
let soonestEndDate = timerEntities
.map(\.endDate)
.filter { $0 > currentDate }
.min()
let nextRefreshDate = soonestEndDate ?? Calendar.current.date(byAdding: .hour, value: 1, to: Date())!
let entries = [
TimerEntry(date: currentDate, timerEntities: timerEntities),
TimerEntry(date: nextRefreshDate, timerEntities: timerEntities),
]
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
struct TimerEntryView: View {
var entry: TimerEntry
var body: some View {
VStack {
ForEach(entry.timerEntities) { timer in
HStack {
Text(timer.task)
Spacer()
if timer.endDate > Date() {
Text(timer.endDate, style: .timer)
.multilineTextAlignment(.trailing)
} else {
Text("00:00")
.foregroundColor(.secondary)
}
}
}
}
.padding()
}
}
Note
Remember that widgets are not supposed to be refreshed more often than every couple of minutes). Otherwise your widget will simply not work. That's the limitation imposed by Apple.
Currently, the only possibility to see the date refreshing every second is to use style: .timer
in Text
(other styles may work as well). This way you can refresh the widget only after the timer finishes.
How to update today widget in swift every x seconds
The one way is NSTimer. It is useful for calling method every x seconds.
var timer : NSTimer?
override func viewDidLoad() {
super.viewDidLoad()
timer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "animateFrame:", userInfo: nil, repeats: true)
}
func animateFrame(timer: NSTimer) {
// Do something
}
In the case, you can call animateFrame: every 3 seconds.
Updating time text label each minute in WidgetKit
A possible solution is to use the time
date style:
/// A style displaying only the time component for a date.
///
/// Text(event.startDate, style: .time)
///
/// Example output:
/// 11:23PM
public static let time: Text.DateStyle
- You need a simple
Entry
with aDate
property:
struct SimpleEntry: TimelineEntry {
let date: Date
}
- Create an
Entry
every minute until the next midnight:
struct SimpleProvider: TimelineProvider {
...
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
var entries = [SimpleEntry]()
let currentDate = Date()
let midnight = Calendar.current.startOfDay(for: currentDate)
let nextMidnight = Calendar.current.date(byAdding: .day, value: 1, to: midnight)!
for offset in 0 ..< 60 * 24 {
let entryDate = Calendar.current.date(byAdding: .minute, value: offset, to: midnight)!
entries.append(SimpleEntry(date: entryDate))
}
let timeline = Timeline(entries: entries, policy: .after(nextMidnight))
completion(timeline)
}
}
- Display the date using the
time
style:
struct SimpleWidgetEntryView: View {
var entry: SimpleProvider.Entry
var body: some View {
Text(entry.date, style: .time)
}
}
If you want to customise the date format you can use your own DateFormatter
:
struct SimpleWidgetEntryView: View {
var entry: SimpleProvider.Entry
static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm"
return formatter
}()
var body: some View {
Text("\(entry.date, formatter: Self.dateFormatter)")
}
}
Here is a GitHub repository with different Widget examples including the Clock Widget.
how to refresh periodically a panel wxPython including several widgets
You have to create a Timer object and also overwrite the OnPaint event if you want to draw something. Bind the Timer object to an update method, this will be called periodically, 60 times in a second (1/60). I will paste below an old code of mine which draws a circle in the upper left corner. If you press the Move button, the circle will move diagonally to the lower right.
So I guess if it works on one Panel, it should also work with multiple Panels.
import wx
class MyPanel(wx.Panel):
def __init__(self, parent):
super().__init__(parent)
self.Bind(wx.EVT_PAINT, self.OnPaint)
# create a timer
self.timer = wx.Timer(self)
self.Bind(wx.EVT_TIMER, self.update, self.timer)
# start x, y position of the circle
self.x, self.y = 0, 0
# refresh rate
self.delta = 1/60
self.SetDoubleBuffered(True)
btn1 = wx.Button(self, wx.ID_ANY, u"Move", size=(100, 50), pos=(800, 100))
self.Bind(wx.EVT_BUTTON, self.start_simulation, btn1)
def start_simulation(self, evt):
self.timer.Start(self.delta)
def OnPaint(self, event):
dc = wx.PaintDC(self)
dc.SetBrush(wx.Brush('#c56c00'))
dc.DrawEllipse(self.x, self.y, 60, 60)
# dc.DrawEllipse(0, 20, 60, 60)
dc.DrawLine(0, 600, 800, 600)
def update(self, evt):
self.x += 2
self.y += 2
self.Refresh()
class MyFrame(wx.Frame):
def __init__(self):
self.size = (1280, 720)
super().__init__(None, title="Moving circle", size=self.size, style=wx.DEFAULT_FRAME_STYLE)
self.panel = MyPanel(self)
class MyApp(wx.App):
def OnInit(self):
frame = MyFrame()
frame.Show()
return True
if __name__ == "__main__":
app = MyApp()
app.MainLoop()
Related Topics
How to Get the Correct Current Time in iOS
Value of Type 'Error' Has No Member 'Code'
Disable Vertical Scroll in Uiscrollview Swift
How to Insert Items at 0 Index to the Realm Container
Issue with Observing Wkwebview Url Changes via JavaScript Events
iOS - How to Make the Uiscrolling Indicator Height Same for Any Number of Data
Swift: Memory Not Clearing When I Segue to Another View Controller, Recieving Memory Warning
How to Make a Bullet List with Swift
Objective-C Call Swift Function
Xcode 6 Project Crashing After Segue on iOS 7.1
Periodically Call an API with Rxswift
Exc_Bad_Access in Parent Class Init() with Xcode 10.2
Custom Uitoolbar Too Close to the Home Indicator on iPhone X
iOS - Uinavigationcontroller, Hide Navigationbar
Uitapgesturerecognizer Sender Is the Gesture, Not the UI Object