Having collectionViewCell.label.text Change Every Second with Timer - swift

I am currently learning to code swift apps and am trying to do some projects on my own. My current personal challenge is to do a countdown timer app. The concept is simple: the user enters a date, from a UIDatePicker, and the app shows the time remaining until his list of various events (uses user default values to keep the events in memory). All the said events are shown in a collection view (see below for screen shots).
I ran into something too complicated for my actual skillset and I thought you guys probably would have the answer, or at least a few suggestions! Here is my issue: I'd like for the time remaining between today and the event to decrease every second, and to be shown through a label inside a collectionViewCell. I found a very simple way to do so, with a timer, but implementing the said solution with a collectionViewCell is giving me quite the headache.
Here are the code excerpts I'd like to share, and hopefully it's enough:
#objc func UpdateTimeLabel(index: Int) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
dateFormatter.timeZone = userCalendar.timeZone
let date = dateFormatter.date(from: UserDefaults.standard.array(forKey: "dates")![index] as! String)!
let timeLeft = userCalendar.dateComponents([.day, .hour, .minute, .second], from: currentDate, to: date)
return "\(timeLeft.day!) days \(timeLeft.hour!) hours \(timeLeft.minute!) minutes \(timeLeft.second!) seconds"
}
That's my function I'd like to fire every second. It is currently made in such a way that its property, called index, is the indexPath.row of the collectionViewCell. It references to an array of event dates stored as UserDefaults. There's as many cells in the collectionView as there is events in the array.
See below the way I implemented it inside the collectionView forItemAt function:
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
// Configure the state of the cell based on the propertires of the card if reprensents
let cardCell = cell as? EventCollectionViewCell
// UI Configuration
cardCell?.eventTitleLabel.text = ((UserDefaults.standard.array(forKey: "events")![indexPath.row]) as! String)
cardCell?.eventDateLabel.text = ("Event on: " + "\((UserDefaults.standard.array(forKey: "dates")![indexPath.row]) as! String)")
cardCell?.countdownLabel.text = UpdateTimeLabel(index: indexPath.row)
cardCell?.eventView.layer.cornerRadius = 10
}
The actual result is that every cell shows the time remaining between today and the event. That's already more than I thought I could do by myself!!!
Results of actual code
Where I think one of you can probably step-in is by helping me answering this question: Where should a timer, looking something like the code below, be placed in order for the cardCell.countdownLabel.text to be updated every second?
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(UpdateTimeLabel), userInfo: nil, repeats: true)
I tried every ways I can think of and am now at a road block. I still have many more things to fix in my project, so I'm not completely stopped yet. If you need more code lines and/or precisions, let me know and I'll provide you with anything you deem helpful.
Thank you so much for taking the time to review my question,
Louis G

Add this method UpdateTimeLabel method into the EventCollectionViewCell and modify method like this
#objc func UpdateTimeLabel() {
let userCalendar = Calendar(identifier: .indian)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
dateFormatter.timeZone = userCalendar.timeZone
let date = dateFormatter.date(from: UserDefaults.standard.array(forKey: "dates")![index] as! String)!
let timeLeft = userCalendar.dateComponents([.day, .hour, .minute, .second], from: Date(), to: date)
countdownLabel.text = "\(timeLeft.day!) days \(timeLeft.hour!) hours \(timeLeft.minute!) minutes \(timeLeft.second!) seconds"
}
After doing this add a property index in EventCollectionViewCell and on didSet of index fire timer in EventCollectionViewCell eg:
var index: Int {
didSet {
let timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(UpdateTimeLabel), userInfo: nil, repeats: true)
timer.fire()
}
}
Replace
cardCell?.countdownLabel.text = UpdateTimeLabel(index: indexPath.row)
with
cardCell?.index = indexPath.row

I found why the solution #Satyen suggested didn't work! Ok I know, it's been almost a month since his last answer, but I have to say I got pretty discouraged when I saw it didn't fix my app and decided to work on other projects, hoping someone else would jump in and offer another solution in the meanwhile. Anyways, I decided this morning to find the issue through a few sessions, and I'm proud to report I did!
Ok so here's what went wrong:
I was using a variable called "current date" as today's. This very variable was declared outside the UpdateTimeLabel function, and transformed in dateComponents right of the bath. Result: the current date wasn't refreshed every second, so every time the event's date was compared to currentDate, the result stayed the same.
Here is the working code:
#objc func UpdateTimeLabel() {
let userCalendar = Calendar.current
let todayDate = Date()
let components = userCalendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: todayDate)
let currentDate = userCalendar.date(from: components)!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
dateFormatter.timeZone = userCalendar.timeZone
let date = dateFormatter.date(from: UserDefaults.standard.array(forKey: "dates")![index] as! String)!
let timeLeft = userCalendar.dateComponents([.day, .hour, .minute, .second], from: currentDate, to: date)
countdownLabel.text = "\(timeLeft.day!) days \(timeLeft.hour!) hours \(timeLeft.minute!) minutes \(timeLeft.second!) seconds"
if currentDate >= date {
countdownLabel.text = "Cheers! This event has occured already."
print("An event expired.")
}
}
Fired by this var index, as suggested by #Satyen:
var index: Int = 0 {
didSet {
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(UpdateTimeLabel), userInfo: nil, repeats: true)
timer.fire()
print("timer fired")
}
}
Everything is now working like expected! I still have a few efficiency tweaks to make my code simpler, but overall it is now displaying the remaining time and decreasing it every second! So excited!!
Thank you to #Satyen once again for his precious input!

Related

How do you send notifications based on user input? SWIFT

How do you set the notification time to match the input of the user?
If a notification reminds the user of new content (i.e the word of the day), is that considered a local notification or a push notification?
The userSelectedTime variable is created when they press done, but I don't know how to pass it on to my sendNotification function which uses DateComponents() to set the time.
#objc func donePressed() {
// formatter
let formatter = DateFormatter()
formatter.dateStyle = .none
formatter.timeStyle = .short
var userSelectedTime = timeTextField.text
userSelectedTime = formatter.string(from: timePicker.date)
print(userSelectedTime)
self.view.endEditing(true)
}
func sendNotification() {
// Default time 10:30, but base it on userSelectedTime if not void
var dateComponents = DateComponents()
dateComponents.hour = 10
dateComponents.minute = 30
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: createNotificationContent(for: myData), trigger: trigger)
center.add(request)
}
First I will answer your question 2. - This mostly depends on where that information is coming from. If the "word of the day" is something you have stored internally inside the app, you can schedule a local notification with that information. However, if you want to change the word of the day externally every day, you will have to create a push notification to achieve this. (At least the only way to make it work properly). Push notifications would require some sort of service to push those to the app so the easiest for you is to go with the local notifications.
To summarize question 2:
Local notification: Something that is created within the app.
Push notification: Information 'pushed' from an external source.
Question 1:
You can do this in a number of ways. What I would recommend from looking at what you already have is to create the date components with the selected time and pass those components to your 'sendNotification()' function.
As an example:
// Capture the time and turn them into components
func donePressed() {
let calendar = Calendar.current
let components = calendar.dateComponents([.hour, .minute, .month, .year, .day], from: timePicker.date)
sendNotification(components: components)
}
// Schedule the notificaiton based on components
func sendNotification(components: DateComponents) {
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: createNotificationContent(for: myData), trigger: trigger)
center.add(request)
}
Now... I can't see all your code, so I assume that what else you got going is working properly. But I hope you get the concept. I would however recommend creating a separate class like "NotificationScheduler" to handle these things, that way you will be able to separate things a bit more.

Automatically Delete rows in a UITableView: Swift3

I have a UITableView which contains rows of posts that are timestamped based on when they enter into my database. I want to have the posts (rows) of each cell automatically delete after a certain amount of time has passed. Currently this is what I have towards this goal:
func deletePost(timer: Timer) {
for post in posts {
let startTime = Date(timeIntervalSince1970: TimeInterval(post.time))
let date = Date()
let endTime = startTime + 25
let call = Calendar.current
let componentsCurrent = call.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 = call.date(from: components)!
print(currentTime)
print(" 2" , endTime)
if currentTime.timeIntervalSince1970 == endTime.timeIntervalSince1970 {
timer.invalidate()
print("ENDDDDD")
posts.removeLast()
tableView.reloadData()
}
}
}
The problem I am seeing is that current date only records the date once but doesn't continue to update. I have looked through other posts regarding timers and I have implemented them however it has not worked within this context. I assume once I can get these variables to equal the other code will be straight forward.
cellForRowAt is not the place for any kind of logic such as this. Its only purpose is to provide the requested cell. That's all.
What you want is to setup a timer, say once a minute (or some other appropriate timespan). When the timer goes off, scan your data source for rows that should be removed. Delete the appropriate values from the data source and update the table view accordingly using deleteRows passing in an array of IndexPath that correspond to the removed entries.
Your code will check if it's time to delete something only when your tableView is
reloadingData(); .
//That's not good, because `cellForRow`
//is called as many times as this method returns
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count;
}
You should remove the data from your array, and then call tableView.reloadData();
To do that by specific time, you can :
timeToRemoveCell = 10:00;
and check this time in viewWillAppear.
or you can make a timer as rmaddy said.

Cannot call value of non-function type "calendar"

I made a simple count down app and I get this wrong message: cannot call value of non-function type "calendar".
What I need to change?
#IBOutlet weak var deteLabelOutlet: UILabel!
let formatter = DateFormatter()
let useCalendar = NSCalendar.current()
let requesedComponent: NSCalendar.Unit = [
NSCalendar.Unit.month,
NSCalendar.Unit.day,
NSCalendar.Unit.hour,
NSCalendar.Unit.minute,
NSCalendar.Unit.second
]
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(printTime), userInfo: nil, repeats: true)
timer.fire()
}
func printTime() {
formatter.dateFormat = "MM/dd/yy hh:mm:ss a"
let startTime = Date()
let endTime = formatter.date(from: String("12/25/16 12:00:00 a"))
let timeDiffrenece = useCalendar.components(requesedComponent,fromDate: startTime, toDate: endTime!, options: [] )
deteLabelOutlet.text = "\(timeDiffrenece.month)Month \(timeDiffrenece.day) days \(timeDiffrenece.minute) minutes \(timeDiffrenece.second) seconds"
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
In Swift 3 the current calendar is not a function. Remove the parentheses.
Anyway it's highly recommended to use the native struct Calendar as suggested in the other answer
let useCalendar = Calendar.current
Edit:
You have to unwrap all date components when assigning the values to the label because in Swift 3 all date components are optional. However as all relevant components are specified you can forced unwrap the components safely.
deteLabelOutlet.text = "\(timeDiffrenece.month!)Month \(timeDiffrenece.day!) days \(timeDiffrenece.minute!) minutes \(timeDiffrenece.second!) seconds"

countdown using NSDate transition to swift 3

Today I switched to Swift 3 language. I am making an app that gives a countdown of days hours minutes and seconds until an event. The following code below displays the following in my app:
Optional(6) days Optional(59) minutes Optional(35) seconds.
My countdown in Swift 2 worked fine but once I converted it to Swift 3 the optional appeared in front of the numbers. Does anyone with experience in Swift 3 know a solution to this problem?
#IBAction func NextWeeklyButtonPressed(_ sender: AnyObject) {
let timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(printTimeWeeks), userInfo: nil, repeats: true)
timer.fire()
}
func printTimeWeeks() {
formatter.dateFormat = "MM/dd/yy hh:mm:ss a"
let startTime = Date()
let endTime = formatter.date(from: WeeklyDate.text!+" 12:00:00 a")
// WeeklyDate.text is a date in short style
let timeDifference = (userCalendar as NSCalendar).components(requestedComponent, from: startTime, to: endTime!, options: [])
WeeklyDateLabel.text = "\(timeDifference.day) Days \(timeDifference.minute) Minutes \(timeDifference.second) Seconds"
}
append ! to fields
WeeklyDateLabel.text = "\(timeDifference.day!) Days \(timeDifference.minute!) Minutes \(timeDifference.second!) Seconds"
You need to unwrap the optional Strings, e.g.
if let day = timeDifference.day, minute = timeDifference.minute, second = timeDifference.second {
WeeklyDateLabel.text = "\(timeDifference.day) Days \(timeDifference.minute) Minutes \(timeDifference.second) Seconds"
}

Swift deleting table view cell when timer expires

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)"
}