This is a timer App and I would like for the timer to pause when the app gets killed and when you reopen the app I would like the timer to display the last known time.
I have setup a struct for the data to be saved to.
let defaults = UserDefaults.standard
private enum TimeData {
static let allCurrentTime = "allCurrentTime"
}
override func viewDidLoad() {
super.viewDidLoad()
checkForSavedTimer()
marksSavedData()
theTimer.text = "00:00:00"
theTimer.font = UIFont(name: "Menlo-BoldItalic", size: 40)
marksTableView.isHidden = false
}
//I set the timer info to the struct (see below)
#objc func updateTimerLabel() {
seconds += 1
if seconds == 60 {
minutes += 1
seconds = 0
}else if minutes == 60 {
hours += 1
minutes = 0
}
timerString = "\(hours):\(minutes):\(seconds)"
theTimer.text = timerString
defaults.set(theTimer.text, forKey: TimeData.allCurrentTime)
}
// I then created a function which I called in the viewdidload above
func checkForSavedTimer() {
let allCurrentTime = defaults.value(forKey: TimeData.allCurrentTime) as? String ?? ""
theTimer.text = allCurrentTime
}
I thought this would take the string from theTimer save it to the UserDefaults and then in viewDidLoad bring it back out and save it to "theTimer.text" BUT the result is 00:00:00
When you are calling your checkForSavedTimer() function, it is probably getting the correct value from UserDefaults but then you are setting the timer text back to 00:00:00 two lines later. You will need to remove that line and add some logic within your checkForSavedTimer() function to set the value to 00:00:00 if the value from UserDefaults is nil
override func viewDidLoad() {
super.viewDidLoad()
checkForSavedTimer()
marksSavedData()
// Remove this -> theTimer.text = "00:00:00"
theTimer.font = UIFont(name: "Menlo-BoldItalic", size: 40)
marksTableView.isHidden = false
}
func checkForSavedTimer() {
// Set the timer text to the value from UserDefaults or "00:00:00" if the value is nil
theTimer.text = defaults.string(forKey: TimeData.allCurrentTime) ?? "00:00:00"
}
Also it is better to use .string(forKey:) to get a String from UserDefaults because then the value is already typed for you and therefore you don't need to add as? String
Related
I'm storing an integer using User Defaults, but the number I'm trying to store was transferred via segue (ageFromSettings) and needs to have a default value.
The problem is that the data that was transferred via segue gets stored when presented, but when the app restarts it stores the default value.
Any ideas how to save an item that was passed via segue?
class ViewController: UIViewController {
#IBOutlet weak var youHaveLabel: UILabel!
#IBOutlet weak var daysLeftLabel: UILabel!
#IBOutlet weak var leftToLiveLabel: UILabel!
#IBAction func settingsButtonPressed(_ sender: Any) {
performSegue(withIdentifier: "toSettings", sender: nil)
}
var daysLeft = 0
var ageFromSettings = 36500
override func viewDidAppear(_ animated: Bool) {
UserDefaults.standard.set(ageFromSettings, forKey: "persistedAge")
if let age = UserDefaults.standard.object(forKey: "persistedAge") as? Int{
print("age: \(age)")
daysLeft = age
}
if daysLeft == 36500 {
daysLeftLabel.alpha = 0
youHaveLabel.alpha = 0
leftToLiveLabel.text = "Set your age in Settings"
} else if daysLeft == -1{
leftToLiveLabel.text = "Day On Borrowed Time"
} else if daysLeft == 0{
leftToLiveLabel.text = "Statistically, you died today"
} else if daysLeft == 1{
leftToLiveLabel.text = "Day Left to Live"
} else if daysLeft < 0 {
leftToLiveLabel.text = "Days on Borrowed Time"
}
if daysLeft > -1 {
daysLeftLabel.text = String(daysLeft)
} else {
daysLeftLabel.text = String(daysLeft * -1)
}
}
}
The problem is that you are saying:
UserDefaults.standard.set(ageFromSettings, forKey: "persistedAge")
if let age = UserDefaults.standard.object(forKey: "persistedAge") as? Int{
That's nutty. In the second line, we know that UserDefaults contains a persistedAge key, and we know what its value is — because you just put it there, in the first line. And you're doing that even if you have already set a different persistedAge value on a previous run of the app.
Delete the first line. If your goal is to supply an initial default value, use register(defaults:_). That's what it's for. It will be used only just in case you have not called set(_:forKey:) yet.
In your case, you are setting the value of the persistedAge key every single time in the viewDidAppear(_:) method of your view controller. Right afterwards, you are retrieving the data and trying to read it, which is somewhat redundant. In the latter case, you will also be resetting the value of the persistedAge key every time your view controller appears.
I have tried hard to find a solution but I'm stuck. have a custom table view with timer in each cell. when the timer expires the cell should get deleted even if the cell is Offscreen it should get deleted and should not be displayed.
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! iDealCell
// cell.SponsorLogo.image = UIImage(named: "back.png")!
cell.SponsorName.text = iDeals[indexPath.row].SponsorName;
cell.Distance.text = iDeals[indexPath.row].Distance;
cell.Type.text = iDeals[indexPath.row].Type;
cell.iDealTimer.font = UIFont(name: "DBLCDTempBlack", size: 18.0)
onHourFromNow = NSDate(timeInterval: 10, sinceDate: timeNow)
let TimeDiffInSec = NSCalendar.currentCalendar().components(.Second, fromDate: timeNow, toDate: onHourFromNow, options: []).second
cell.TimeDiffInSec = TimeDiffInSec
cell.kickOffCountdown()
cell.backgroundColor = UIColor.clearColor()
cell.delegate = self
return cell
}
in cell class, three functions to initialise and run the timer
func kickOffCountdown(){
self.setCountDown()
Timer.invalidate()
Timer = NSTimer.scheduledTimerWithTimeInterval(1.0, target: self, selector: #selector(iDealCell.setCountDown), userInfo: nil, repeats: true)
}
func secondsToHoursMinutesSeconds (seconds : Int) -> (String, String, String) {
let hours = seconds / 3600
let minutes = (seconds % 3600) / 60
let seconds = (seconds % 3600) % 60
let hoursString = hours < 10 ? "0\(hours)" : "\(hours)"
let minutesString = minutes < 10 ? "0\(minutes)" : "\(minutes)"
let secondsString = seconds < 10 ? "0\(seconds)" : "\(seconds)"
return (hoursString, minutesString, secondsString)
}
func setCountDown() {
if(TimeDiffInSec > 0) {
let (h,m,s) = secondsToHoursMinutesSeconds(TimeDiffInSec)
self.iDealTimer.text = "\(h):\(m):\(s)"
TimeDiffInSec = TimeDiffInSec - 1
}
else{
self.iDealTimer.text = "EXPIRED"
if let delegate = self.delegate {
delegate.DeleteiDealID(1)
}
Timer.invalidate()
}
}
Any help will be greatly appreaciated
You can do simple thing whenever your time have been expire you can remove those values from your array of dictionary whatever you used for row count.
Simple thing here is your all table cell depends on your row count remover particular cell with by removing particular array object.
example :
if timerExpire == true {
array.removeAtIndex(5)
self.table.reloadData()
}
This is a tricky problem, because you want:
Table rows to be deleted even if they are offscreen at the time the timer pops.
New rows can be added while the old rows are "ticking".
The first point means that you do not want the timer to be kept in the cell. It is the wrong place anyway, because cells get reused and you'd have a nightmare invalidating and restarting timers.
The second point means that the row number you want to delete at the time the timer is started could be different than the row number you delete when the timer pops. You may start your timer for row 5 to be deleted in 5 seconds, but in the meantime row 4 gets deleted, making the former row 5 now row 4. When the former row 5's timer pops, row 4 needs to be deleted from the table.
Here is the approach I suggest:
Give each row in your table a unique ID. This will just be a simple count that is maintained by your UITableViewController class.
var nextID = 0
Maintain a list of active ID's that correspond to the rows that are currently in your table. Add this property to your UITableViewController:
var activeIDs = [Int]()
Add a dictionary to your table that maps a NSTimer to an ID. Add this to your UITableViewController:
var timerIDmap: [NSTimer: Int]()
When you create a new row in your table:
let newID = nextID
activeIDs.append(newID)
nextID += 1
In cellForRowAtIndexPath, be sure to store the ID in a property of the cell.
cell.cellID = activeIDs[indexPath.row]
When you create a timer, you need to store the timer and its corresponding cell ID in the timerIDmap. Since you'll do this in the custom cell, the cell needs to have a weak reference to the tableViewController that holds it:
// add this property to your cell
weak var myTableVC: UITableViewController?
and assign that property in cellForRowAtIndexPath:
cell.myTableVC = self
so that when you create the timer:
let timer = NSTimer.scheduledTimerWithTimerInterval(...
myTableVC?.timerIDmap[timer] = cellID
When your timer ticks, you need to decrement the time left on that timer. That means the time left should also be kept in your model. Add this dictionary to your UITableViewController:
var timeLeft = [Int: Int]() // maps CellID to time left
that means that when you create the timer in the first place, you will store timeLeft in this dictionary
myTableVC?.timeLeft[cellID] = 50 // some appropriate value
OK, so now in your handleCountdown routine which should be implemented in your UITableViewController:
func handleCountdown(timer: NSTimer) {
let cellID = timerIDMap[timer]
// find the current row corresponding to the cellID
let row = activeIDs.indexOf(cellID)
// decrement time left
let timeRemaining = timeLeft[cellID] - 1
timeLeft[cellID] = timeRemaining
if timeRemaining == 0 {
timer.invalidate
timerIDmap[timer] = nil
activeIDs.removeAtIndex(row)
tableView.deleteRowsAtIndexPaths(NSIndexPath(row: row, section: 0), withRowAnimation: ...
} else {
tableView.reloadRowsAtIndexPaths(NSIndexPath(row: row, section: 0), withRowAnimation: ...
}
}
This leaves very little work for your custom cell. It should merely take the time left on the timer and format it for display. In cellForRowAtIndexPath, tell the cell how much time is left on the timer:
cell.timeLeft = timeLeft[activeIDs[indexPath.row]]
The number of items in your table is the same as the number of items in activeIDs, so in tableView:numberOfRowsInSection return
return activeIDs.count
I think, this will be better way:
Inside tableView:cellForRowAtIndexPath calculate (or fetch) time and add it's value to label in cell.
Remove timer from cell.
Add timer in main class (where tableView placed) in viewDidAppear (or inside block where you fetch data), that will every second call method, that check and remove expired objects (or you can apply filter) and fire tableView.reloadData() (or delete needed rows animated).
In viewDidDisappear invalidate timer.
I have been trying these solutions but I dont think they are viable towards the goal. Just wanted to let people know. If you have found something that works or have found the same can you please let us know
This is the solution I have come up with. Its not a perfect solution by any means however, it does solve the problem.
In the viewcontroller:
func handleDate(timer: Timer) {
DispatchQueue.main.async() {
if self.posts.count < 1 {
print("Empty")
timer.invalidate()
} else {
let calendar = Calendar.current
let date = Date()
let componentsCurrent = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: date)
var components = DateComponents()
components.hour = componentsCurrent.hour
components.minute = componentsCurrent.minute
components.second = componentsCurrent.second
components.year = componentsCurrent.year
components.month = componentsCurrent.month
components.day = componentsCurrent.day
let currentTime = calendar.date(from: components)!
for post in self.posts {
let cellID = post.postID
let row = self.postsInFeed.index(of: cellID)
let endDate = TimeInterval(post.time)
if currentTime.timeIntervalSince1970 >= endDate {
self.tableView.beginUpdates()
timer.invalidate()
print(post.postID)
print("Deleting tableview row")
self.postsInFeed.removeFirst()
self.posts.removeFirst()
let store: Dictionary<String, Any> = ["caption": post.caption, "mediaURL": post.imageUrl as Any, "likes": post.likes, "user_ID": FriendSystem.system.CURRENT_USER_ID, "time": post.time]
let firebasePost = FriendSystem().GROUP_REF.child(self.group.groupID).child("storage").child(post.postID)
firebasePost.setValue(store)
FriendSystem().GROUP_REF.child(self.group.groupID).child("posts").child(cellID).removeValue()
self.tableView.deleteRows(at: [IndexPath(row: row!, section: 0)] , with: UITableViewRowAnimation.fade)
self.tableView.endUpdates()
self.tableView.reloadData()
}
}
}
}
}
In the tableviewcell:
func tick(timer: Timer) {
guard let expiresAt = endDate else {
return
}
let calendar = NSCalendar(calendarIdentifier: NSCalendar.Identifier.gregorian)
if let components = calendar?.components([.hour, .minute, .second], from: NSDate() as Date, to: expiresAt, options: []) {
currentTime = formatDateComponents(components: components as NSDateComponents)
self.timerLbl.text = currentTime
if Date() >= endDate! {
timer.invalidate()
}
}
}
func formatDateComponents(components: NSDateComponents) -> String {
let hours = components.hour
let minutes = components.minute
let seconds = components.second
return "\(hours):\(minutes):\(seconds)"
}
I want to start a timer at a specific date and time, then use that start time as a game timer for the rest of the game. Using "timeIntervalSinceDate" will give me seconds but then trying to get the seconds to display on the gameTimerLabel won't work. I might be coming at this the wrong way. Any advice is welcome.
override func viewWillAppear(animated: Bool) {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
let dateAsString1 = "Fri, 1 April 2016 11:30:00 MST"
let date1 = dateFormatter.dateFromString(dateAsString1)!
var currentTime = NSDate()
var counter = 0
gameTimerLabel.text = String(counter)
gameTimerLabel.text = counter //<- Error:Cannot assign value to type 'Int' to type 'String?'
counter = date1.timeIntervalSinceDate(currentTime) //<- Error:Cannot assign value of type 'NSTimeInterval' (aka 'Double') to type 'Int'
}
A couple of things here
First, when you declare counter, it's inferred to be of type Int
var counter = 0
You can declare it as a double by adding a .0 or specifying it's type:
var counter: NSTimeInternval = 0.0
Next, you can use string interoperability to display the count variable in a string, like this:
gameTimerLabel.text = "\(counter)"
Here's an example view controller using an NSTimer as a counter, it counts in seconds:
class ViewController: UIViewController {
// Setup a timer and a counter for use
var timer = NSTimer()
var counter : NSTimeInterval = 0
#IBOutlet weak var label: UILabel!
// Invalidest the timer, resets the counter and udpates the label
#IBAction func resetTimer(sender: AnyObject) {
// Invalidate the timer, reset the label.
self.timer.invalidate()
self.label.text = ""
self.counter = 0
}
// A button that when pressed starts and stops the timer
// There's no pause/resume, so it's invalidated & created again
// But the counter value reamins the same
#IBAction func timerBttnTouched(sender: AnyObject) {
if self.timer.valid {
self.timer.invalidate()
} else {
self.setupTimer()
}
}
// Does the actual counting everytime the timer calls this method
func timerFired() {
self.counter += 1
self.label.text = "\(self.counter)"
}
// Setups a timer, adds it to the run loop and specifies what method should fire when the timer fires
func setupTimer() {
// Setupt the timer, this will call the timerFired method every second
self.timer = NSTimer(
timeInterval: 1,
target: self,
selector: #selector(self.timerFired),
userInfo: nil,
repeats: true)
// Add the timer to the run loop
NSRunLoop.currentRunLoop().addTimer(
self.timer,
forMode: NSDefaultRunLoopMode)
}
}
An important thing to note when using timers is they may not always be called exactly when you need them to, this should be taken into account according to your desired precision when using a timer.
As discussed in comments, here's the solution using a timer to fire a method that compares two dates and uses a NSDateComponentsFormatter to generate a string for display. The initial date is generated in viewDidLoad but can be created anywhere:
class ViewController: UIViewController {
// Setup a timer and a counter for use
var timer = NSTimer()
var counter : NSTimeInterval = 0
var startDate: NSDate?
override func viewDidLoad() {
// Set the initial date
self.startDate = NSDate()
}
#IBOutlet weak var label: UILabel!
// Invalidest the timer, resets the counter and udpates the label
#IBAction func resetTimer(sender: AnyObject) {
// Invalidate the timer, reset the label.
self.timer.invalidate()
self.label.text = ""
self.counter = 0
}
// A button that when pressed starts and stops the timer
// There's no pause/resume, so it's invalidated & created again
// But the counter value reamins the same
#IBAction func timerBttnTouched(sender: AnyObject) {
if self.timer.valid {
self.timer.invalidate()
} else {
self.setupTimer()
}
}
// Does the actual counting everytime the timer calls this method
func timerFired() {
let now = NSDate()
let difference = now.timeIntervalSinceDate(self.startDate!)
// Format the difference for display
// For example, minutes & seconds
let dateComponentsFormatter = NSDateComponentsFormatter()
dateComponentsFormatter.stringFromTimeInterval(difference)
self.label.text = dateComponentsFormatter.stringFromTimeInterval(difference)
}
// Setups a timer, adds it to the run loop and specifies what method should fire when the timer fires
func setupTimer() {
// Setupt the timer, this will call the timerFired method every second
self.timer = NSTimer(
timeInterval: 1,
target: self,
selector: #selector(self.timerFired),
userInfo: nil,
repeats: true)
// Add the timer to the run loop
NSRunLoop.currentRunLoop().addTimer(
self.timer,
forMode: NSDefaultRunLoopMode)
}
}
I'm new to programming and today I've been researching Property Observers in Swift. This got me wondering if it would be possible to use one to trigger an app to change screens when the value of a variable reaches a certain point.
For example, let's say I have a game that uses the variable 'score' to save and load the user's score. Could I use willSet or didSet to trigger a change in views based on the fact that the score will reach a certain value?
What I was thinking was using something like this:
var maxscore : Int = 0 {
didSet{
if maxscore == 5{
switchScreen()
}}
}
... would call the switchScreen function. Should this work? I haven't been able to find any info on this, so don't know if that's because it's not possible or I just haven't found it.
But I have attempted this with no success. It all compiles and runs, but when the score hits that magic number of 5 nothing happens.
For the sake of completeness, my switchScreen function code is below:
func switchScreen() {
let mainStoryboard = UIStoryboard(name: "Storyboard", bundle: NSBundle.mainBundle())
let vc : UIViewController = mainStoryboard.instantiateViewControllerWithIdentifier("HelpScreenViewController") as UIViewController
self.presentViewController(vc, animated: true, completion: nil)
}
And the code I've used for setting the value to 5 is below:
func CheckAnswer( answerNumber : Int)
{
if(answerNumber == currentCorrectAnswerIndex)
{
// we have the correct answer
labelFeedback.text = "Correct!"
labelFeedback.textColor = UIColor.greenColor()
score = score + 1
labelScore.text = "Score: \(score)"
totalquestionsasked = totalquestionsasked + 1
labelTotalQuestionsAsked.text = "out of \(totalquestionsasked)"
if score == 5 { maxscore = 5}
// later we want to play a "correct" sound effect
PlaySoundCorrect()
}
else
{
// we have the wrong answer
labelFeedback.text = "Wrong!"
labelFeedback.textColor = UIColor.blackColor()
totalquestionsasked = totalquestionsasked + 1
labelTotalQuestionsAsked.text = "out of \(totalquestionsasked)"
if score == 5 { maxscore = 5}
// we want to play a "incorrect" sound effect
PlaySoundWrong()
}
SaveScore()
buttonNext.enabled = true
buttonNext.hidden = false
}
This the method is inside the class than you should be able to do!. Check this answer!
//True model data
var _test : Int = 0 {
//First this
willSet {
println("Old value is \(_test), new value is \(newValue)")
}
//value is set
//Finaly this
didSet {
println("Old value is \(oldValue), new value is \(_test)")
}
}
Right now I'm working on a time clock app that allows the user to punch in/out of work time. But I'm having trouble figuring out how to make that function possible.
In my model I have:
struct TimeLog {
var punchInTime: CFAbsoluteTime
var punchOutTime: CFAbsoluteTime
var timeWorked: Double
init (pInTime: CFAbsoluteTime, pOutTime: CFAbsoluteTime) {
self.punchInTime = pInTime
self.punchOutTime = pOutTime
self.timeWorked = pOutTime - pInTime
}
}
And in my view controller:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func punchInButtonPressed(sender: AnyObject) {
// not sure what actions are needed here to start the "aTimeLog" variable
}
#IBAction func punchOutButtonPressed(sender: AnyObject) {
// not sure what actions are needed here to complete the "aTimeLog" variable
}
I'm trying to complete this variable:
var aTimeLog = TimeLog(pInTime: //get the punch in time here, pOutTime: //get the punch out time here)
And once the variable "aTimeLog" is complete (the punchOutButton is pressed) I want to display a log of all of my "timeWorked" variables.
Forgive me in advance. You might be able to tell I'm just learning programming and Swift.
Personally, I'd keep a variable in NSUserDefaults - storing the punch in time. Then, when the punch out button is tapped, it fetches the dateTime variable from NSUserDefaults, does the needed calculation and returns the result. This caters for if a user closes the app (punch in time is remembered).
This creates an NSUserDefaults object
var punchInTime : NSUserDefaults = NSUserDefaults.standardUserDefaults()
#IBAction func punchInButtonPressed(sender: AnyObject) {
Saves the current time
punchInTime.setObject(NSDate(), forKey: "punchInTime")
punchInTime.synchronize()
}
#IBAction func punchOutButtonPressed(sender: AnyObject) {
fetches the saved time
punchInTime.objectForKey("punchInTime") as NSDate
creates a variable holding the current time so you can compare the two
var currentTime : NSDate = NSDate()
currentTime.timeIntervalSinceDate(punchInTime) // returns seconds
Now you can save the start time, end time and total working time inside, say, a dictionary with the key as the date maybe...
}
Then when you want to display all the days worked, simply loop through the dictionary :)
...hope I understood correctly
As one of the possible solutions, you could alter your init method as follows:
initWithStartTime (pInTime: CFAbsoluteTime)
{
self.punchInTime = pInTime
}
Then add a method like:
func workedTimeWithEndTime(pOutTime: CFAbsoluteTime) -> CFAbsoluteTime
{
return pOutTime - self.punchInTime
}
Finally you'll probably want to add "var timeLog: TimeLog!" into your view controller and initialise it in punchInButtonPressed with start time.
You should give your ViewController class a ref to a TimeLog class (why did you choose a struct? works too but just curious).
var timeLog: TimeLog?
Second of all you want to initialise it with a fresh one every time you punch in
#IBAction func punchInButtonPressed(sender: AnyObject) {
let now = CFAbsoluteTimeGetCurrent()
self.timeLog = TimeLog(pInTime: now)
}
So you initialise only with the pInTime, and the rest of the times are set to 0 / null. Next when you punch out you add the pOutTime and you can calculate the timeWorked.
#IBAction func punchOutButtonPressed(sender: AnyObject) {
if (self.timeLog == nil) {
return
}
self.timeLog!.punchOut(CFAbsoluteTimeGetCurrent());
println(self.timeLog!.timeWorked)
}
Since we don't use the constructor like you've created it and it needs a new function we need to change your class (use class unless you know why you want a struct).
class TimeLog {
var punchInTime: CFAbsoluteTime
var punchOutTime: CFAbsoluteTime
var timeWorked: Double
init (pInTime: CFAbsoluteTime) {
self.punchInTime = pInTime
self.punchOutTime = 0
self.timeWorked = 0
}
func punchOut(pOutTime: CFAbsoluteTime) {
self.punchOutTime = pOutTime
self.timeWorked = self.punchOutTime - self.punchInTime
}
}
Try this:
// run this on first action/tap
let start = Date()
// then, run this on the second time/interaction, to get the negative elapsed time
// for example: for 0.8 seconds this will return -0.8694559335708618
print("Elapsed time: \(start.timeIntervalSinceNow) seconds")