I'm trying to replicate the default Clock app found on the iPhone in order to learn swift better and get used to different parts of iOS Development.
I'm currently working on the timer section of the app.
I have a DatePicker to get the date and I'm assigning the countdownDuration property to a TimeInterval when the timer starts.
This is giving me a random value each time however (always seems to be around 80.0-120.0)
I fully understand I might have the concept of TimeIntervals wrong but from what I've read online and in the Developer Documentation, I have the concept right?
Here is the code I'm using to run the timer:
#IBOutlet weak var datePicker:UIDatePicker!
var IsTimerRunning:Bool = false;
var TimerDuration:TimeInterval = 0;
var timer:Timer?;
//Called every second when timer is running
#objc func onTimerFires() {
TimerDuration = TimerDuration - 1.0;
print (TimerDuration);
if (TimerDuration <= 0.0) {
EndTimer();
}
}
func EndTimer() {
timer?.invalidate();
IsTimerRunning = false;
}
//Start button pressed
#IBAction func StartButtonPressed() {
if (IsTimerRunning == false) {
TimerDuration = datePicker.countDownDuration;
timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(onTimerFires), userInfo: nil, repeats: true)
IsTimerRunning = true;
}
}
A couple of considerations:
Make sure your date picker has a Mode of countDownTimer. As the documentation says:
If the mode of the date picker is not UIDatePicker.Mode.countDownTimer, this value [countDownDuration] is undefined...
If you added this on IB, you may want to set "date" property to "Custom". As the documentation warns us:
Note
When you use Interface Builder to set the Mode attribute to Count Down Timer and specify a value for the timer attribute, your date picker may not respect the Timer attribute value when you build and run your project. If this happens, return to the Attributes inspector for the date picker, select Custom in the Date pop-up menu (you can ignore the associated value), and rebuild your project.
I notice very strange countDownDuration values until I selected "custom" for the "Date" pop-up menu in IB.
Related
I am having this strange issue in UIDatePicker mode .countdownTimer
When I add constraints of up/down/left/right to the datePicker view, the callback is not registered for the first time. However, if I add only Alignment constraints, the callback works everytime. Maybe its a bug. Any idea why isn't it working?
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var datePicker: UIDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
datePicker.datePickerMode = .countDownTimer
datePicker.addTarget(self, action: #selector(datePickerValueChanged(_:)), for: .valueChanged)
}
#objc func datePickerValueChanged(_ sender: UIDatePicker){
print("Selected value \(sender.countDownDuration)")
}
}
OK, it's a long-known bug in iOS, since iOS 7 or so.
You can work-around in your case if you put the following somewhere into viewDidLoad:
DispatchQueue.main.async {
self.datePicker.countDownDuration = self.datePicker.countDownDuration
}
Seems nasty, and is nasty.
The workaround of setting the countDownDuration didn't work for me. Instead the only way I managed to solve this is to replace the UIDatePicker with a UIPickerView and populate it myself with 4 components: hours, "hour(s)" label, mins, "min(s)" label.
The hours are 0-23
The hours label is a single row component, which
gets reloaded when the hours are changed so that it switches to
"hour" when it's 1 hour
The mins are 0-59
The mins label is the same
as the hours label
I was able to fix this issue in SwiftUI (using a "UIDatePicker" created with "UIViewRepresentable") by appending an "onAppear" to the picker and inside the closure updating the state variable used for tracking the duration to any value that is not the default value. This forces a programmatic update on the first render which then allows the picker to work correctly on the first spin.
I have a editviewcontroller(secondVC)(which you access by tapping on the uitableviewcell task) where you get the option of setting a reminder. When you set the reminder; an icon appears in front of the task in the UITableViewCell in the firstVC.Now I want that once the reminder is triggered and a notification is sent the icon from the task is removed in realtime. Currently, the way I have set it; if you visit the editVC after the task has been reminded, i compare the current time to time set by the user and then updated a label which says "Time's up".
I want a similar thing to occur with the appropriate cell in the firstVC.
FIRSTVC:
When the time is up, it tells you that time is up and when you return to the firstVC the bell icon is removed. But I want it to happen in realtime even if you are in the firstVC and you don't have to go to the secondVC and then return to firstVC to get the changes.
In short, i want the bell icon to be removed when a task has been reminded to the user which is set in the secondVC. Thanks!
EditVC:
Code:
The following code is executed in the editVC in viewDidLoad. If the current time is more than the time selected, it changes the label to "Time's Up" and changes the bellicon tintcolor to white for that specific reminder.
guard let selectedDate = editnotes?.sSelectedDate,
var needsToRemind = editnotes?.sReminderState else {
print("nil")
return
}
if selectedDate <= Date() && needsToRemind {
editnotes?.sReminderDate = "Time's up"
editnotes?.belliconcolor = .white
reminderMsg.text = editnotes?.sReminderDate
}
You can use to fire post notification.post notification is used to perform an action without going to particular VC.
Do you know about post notification or Notification center? If yes, then it is easy to implement in your code otherwise you need some R&D on its. First of all, register post notification on first vc then after on secondvc fire this notification which is register on first vc. It's simple. If you can't get it then i will send some code for easily got it.
You can fire when your timer stop. And one more important things that when you fire notification , you must need to pass current stop time. Because this time is used to first vc method which is register. In this method, you can compare your reminder time and your current time which is passed by notification if both are same then you can hide the bell otherwise not. One more thing, please Manage array to accurate the code after then reload table.
One problem with your code that I can see right away is that you will only say "Time's up" in your edit VC's viewDidLoad. What happens if time is up a few seconds (or even a second) after viewDidLoad?
If this were my code, I would have a Timer property on BOTH the editVC and on the parent (or first) view controller. It would look like this:
var timesUpTimer : Timer?
Which you can set up in viewWillAppear:
if let selectedDate = editnotes?.sSelectedDate
{
self.timesUpTimer = Timer(fireAt: date, interval: 0, target: self, selector: #selector(doSomething), userInfo: nil, repeats: false)
RunLoop.main.add(timesUpTimer, forMode: RunLoopMode.commonModes)
}
// in the editVC you can have this:
func doSomething()
{
editnotes?.sReminderDate = "Time's up"
editnotes?.belliconcolor = .white
reminderMsg.text = editnotes?.sReminderDate
}
and you would do the appropriate thing in the doSomething you have in the firstVC.
Also, in viewWillDisappear for each view controller, you need to invalidate and set your Timer property to nil.
I'm developing an iOS application using a bluetooth device with a button that is communicating with the iPad. Basically I want a help request to be issued when the button is held for 3 seconds or longer.
From all the documentation I found, I couldn't find a way to stop a Timer without invalidating it, with the invalidate() method. From Apple's documentation:
The run loop then removes the timer (and the strong reference it had to the timer), either just before the
invalidate()
method returns or at some later point. Once invalidated, timer objects cannot be reused.
So the idea in my code is that when the button is pressed, the boolean buttonWasHeld is set to true and a timer is fired. If the button is released, buttonWasHeld is set to false and, when the timer calls the handler it knows the button wasn't held long enough. Then if the button is pressed again within the 3 seconds, the timer is set over again.
Problem is: every button press makes a new timer, which means that repeatedly pressing the button will also issue the help request. Furthermore, all those timers are addressed by the same variable so I can't tell them apart.
Is there a way to uniquely tell what was the last timer created? Or an obscure way to pause/stop it?
Here's the piece of code controlling this feature:
var buttonTimer: Timer?
var buttonWasHeld: Bool = false
func didUpdateModule() {
// gpioListener takes a handler to be called whenever a button is
// pressed or released. isPushed is a self-explanatory boolean.
self.controller.gpioListener() { isPushed in
if isPushed {
self.buttonWasHeld = true
self.buttonTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { _ in
if self.buttonWasHeld {
// Issue a help request
self.delegate?.notifyDevice(message: .HELP)
print("Asking for help")
}
}
print("Button was pressed")
}
else {
self.buttonWasHeld = false
// Also tried "self.buttonTimer = nil" here. Didn't work
print("Button was released")
}
}
}
As usual, the answer was quite simple.
If the Timer is declared as a weak var, and not just var, only the weak instantiation will be invalidated. So the code should be:
weak var buttonTimer: Timer?
var buttonWasHeld: Bool = false
func didUpdateModule () {
(...)
else {
// This will only invalidate the current running timer,
// not the whole variable :)
self.buttonTimer.invalidate
// I removed buttonWasHeld, it's not necessary anymore ;)
print("Button was released")
}
}
}
Swift newbie here. I have two questions:
First, how do I create a timer which AUTOMATICALLY counts down when a ViewController scene is opened? My problem is that the NSTimer ENDS automatically when the scene is opened.
For instance, whenever I go to the said scene, the TimerLabel says: “Time’s Up!”
Before my second question, below is my tweaked code from: makeapppie.com/…/swift-swift-using-nstimer-to-make-a-timer…/
var timer = NSTimer()
let timeInterval:NSTimeInterval = 0.05
let timerEnd:NSTimeInterval = 120.0 //Timer should end in 120 seconds
var timeCount:NSTimeInterval = 0.0
func timerDidEnd(timer:NSTimer){
timeCount = timeCount - timeInterval
if timeCount <= 0 {
TimerLabel.text = "Time's Up!"
timer.invalidate()
}
}
func timeString(time:NSTimeInterval) -> String {
let minutes = Int(time) / 60
let seconds = time - Double(minutes) * 60
return String(format:"%02i:%02i",minutes,Int(seconds))
}
func StartTimer() { // Function called in viewDidLoad
if !timer.valid{
TimerLabel.text = timeString(timeCount)
timer = NSTimer.scheduledTimerWithTimeInterval(timeInterval,
target: self,
selector: "timerDidEnd:",
userInfo: nil,
repeats: true)
}
}
My second question is this: I do not want to use any UISwitch to choose whether to have the timer counting up, or counting down. I need a COUNTING DOWN timer only which AUTOMATICALLY counts down when view is opened. How do I do that?
Please take note that my time format is: “minutes:seconds” as described in the timeString function.
Please help.
Welcome to SO. I think you're in over your head here.
A timer doesn't count up or count down. It fires on a regular interval. You write a method that gets called each time the timer fires.
Note that 0.05 is a pretty short interval for a timer. Timers are not super-accurate, so you might be trying to get too much out of them.
Your code is a confused mess. You start timeCount out at 0. Your timer method, that gets called repeatedly, is called timerDidEnd even though you want your timer to keep running. Your timerDidEnd method subtracts "timeInterval" (.05 seconds) from timeCount when the timer fires, which makes timeCount negative. You then check to see if timeCount is less than 0 (it will be the very first time through this code) and if it is, you invalidate the timer. You also never do anything to display the value of timeCount.
If you want code that counts down, you need to start out with a value that's larger than 0, and subtract a small amount from it each time the timer fires. Then you need to do something with that timeCount value. If you're not displaying it somewhere, it really isn't doing anything useful.
P.S. On an unrelated subject, the avatar pictures on SO are square. You should post a square avatar picture (or create a square crop to post.) When you post a rectangular picture it gets really stretched-out and looks bad.
My problem is that the NSTimer ENDS automatically when the scene is opened.
It sounds like your putting your NSTimer and the code that manages it in a view controller. In that case, it's not surprising that the timer goes away when that view controller does. If the timer should persist from one view controller to the next, then you should manage it from an entirely separate object from the view controller. Since you don't want the timer object to have to keep track of all the view controllers that could possibly be interested in it, have it post notifications that interested view controllers can listen to.
There is similar question on Stack Overflow, but the problem there was different then mine, so I feel free to open this one.
I have iOS view controller with a UIDatePicker defined in storyboard.
I have defined(connected) IBOutlet in view controller code like this:
#IBOutlet weak var endTimeDatePicker: UIDatePicker!
At one point I set initial date like this:
endTimeDatePicker.date = state!.endTime;
or like this:
endTimeDatePicker.setDate(state!.endTime, animated: true);
and it shows correct date indicating that date picker is connected correctly.
But then, if I pick another date and try to get selected date with endTimeDatePicker.date it always returns the same - today's date, no matter what I pick.
Storyboard properties for date picker are:
Mode - Date and Time
Interval - 1 minute
Date - Current Date (but it's the same with custom, only returning defined custom date every time)
Is there something that I've missed to do?
I don't have for sure two different datePickers (like in potential duplicate Stack Overflow questions)
EDIT: looks like it only happens when Date Picker has set initial value from code.
EDIT: code that sets Date Picker:
private func reloadData (state : State?)
{
if (state != nil)
{
endTimeDatePicker.setDate(state!.endTime, animated: true);
backgroundImage.image = UIImage(named: state!.type.imageName)!;
}
else
{
let endDate = NSDate(timeIntervalSinceNow: 1800);
endTimeDatePicker.setDate(endDate, animated: true);
backgroundImage.image = UIImage(named: StateType.Active.imageName);
}
}
Code that tries to read selected date:
#IBAction func doneTapped(sender: AnyObject) {
let row: Int = self.stateTypePicker.selectedRowInComponent(0);
state = State (id: -1, type: stateTypes[row], startTime: NSDate(), endTime: self.endTimeDatePicker.date);
service.addState(state!) {
(responseDict) in
}
}
func reloadData is called in callback for http request. Could be thread lock problem maybe?
Your issue is related to the callback method reloadData from the NSURLSession. In here, you are updating the UIDatePicker in a background thread, but all UI updates have to be done on the main thread.
This is the reason why you are seeing todays date, when you are reading the date property of the UIDatePicker.