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.
Related
I want to get initial value of timePicker, value changes when I am just scrolling time. Please watch photos it will be more clear to understand what I want.
https://imgur.com/a/3Hg69uR
#IBAction func datePickerChanged(_ sender: Any) {
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .short
dateFormatter.dateFormat = "HH:mm"
let strDate = dateFormatter.string(from: datePicker.date)
datePickerLb.text = strDate
}
All you need is to update the label inside your viewDidLoad method. I would move the date formatter declaration out of that method to avoid creating a new one every time the value changes. Note that you should use timeStyle or dateFormat but not both. When displaying dates to the end user you should always respect the devices locale and settings so you should choose timeStyle in this case:
let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.timeStyle = .short
return dateFormatter
}()
override func viewDidLoad() {
super.viewDidLoad()
// your code
// you can update the label here
// datePickerLb.text = dateFormatter.string(from: datePicker.date)
// or manually send the valueChanged action to force the update at view did load
datePicker.sendActions(for: .valueChanged)
}
#IBAction func datePickerChanged(_ datePicker: UIDatePicker) {
datePickerLb.text = dateFormatter.string(from: datePicker.date)
}
The answer by #Leo is practical. One thing to add is that if you wanted the date updated while picker it is rolling then, unfortunately, it is impossible with UIDatePicker itself. This is because UIDatePicker is not a subclass of UIPickerView and it manages a UIPickerView internally. The the solution here might be use a custom UIPickerView. Then for that UIPickerView you can implement it's delegate method like this:
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
switch component {
case 0:
currentTime.hours = pickerView.selectedRow(inComponent: 0)
return "\(row) Hour"
case 1:
currentTime.minutes = pickerView.selectedRow(inComponent: 1)
return "\(row) Minute"
case 2:
currentTime.seconds = pickerView.selectedRow(inComponent: 2)
return "\(row) Second"
default:
return ""
}
}
currentTime var should, for example, have a didSet to update some view elements when it changes.
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!
I need a button where the user clicks to let everyone know they are attending a location, but I only want it to be able to be clicked up to 3 times per 24hrs depending on the contents of the button, as there is a tableView with one button per cell
I haven't tried anything as I have never used time in swift still new to it
#IBAction func myButtonClicked(_ sender: Any) {
DataService.ds.REF_BARS.child(imageTitleLabel.text!).child("GoingCount").observeSingleEvent(of: .value, with: { (snapshot) in
print(snapshot.value as! String)
self.countString = snapshot.value as! String
self.countInt = Int(self.countString)! + 1
DataService.ds.REF_BARS.child(self.imageTitleLabel.text!).updateChildValues(["GoingCount": String(self.countInt)])
})
}
if you can add the pre or post condition of user clicks in or around that button
You can do this for 1 button
if let storedDate = UserDefaults.standard.object(forKey:"StoredDate") as? Date {
let toDate = Calendar.current.date(byAdding: .day, value:1, to:storedDate)
let cliks = UserDefaults.standard.integer(forKey:"NumOfClicks")
if Date() <= toDate {
if cliks < 3 {
// do what you need and increase NumOfClicks by 1
}
else {
// no more clicks this day
}
}
else {
// date exceed may be reset the date and cliks to 1
}
}
else {
// first attempt
UserDefaults.standard.set(Date(),forKey:"StoredDate")
UserDefaults.standard.set(1,forKey:"NumOfClicks")
// do what you need once from here
}
For an array handling you can think of every key above as an array instead , in storing/retrieve like [Date]/[Int]
Situation
I have a tableview with scheduled local notifications stored in cells, containing a title, date, UUID and mood. They repeat weekly from the day the users chooses and works perfectly fine. I have successfully sorted the cells so that the most descending (soonest) notification is at the top of the table view through this line of code in the refreshList():
dqItems.sort(by: {$0.date < $1.date})
Problem
However, when a notification has been fired and is overdue, 7 days is being added to its date through the if (dqItem.isOverdue) function. This works - 7 days is added to the date. But the tableview doesn't re-sort the cells - the notification cell which has been rescheduled with 7 days is still at the top! Despite the fact that there are other notifications with more descending dates scheduled. (See image for example)
This is extremely frustrating and I can't managed to find a solution. Here comes a snippet from the Table View Controller file.
//Outlets
var dqItems: [DQItem] = []
var MoodString = String()
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
refreshList()
}
func refreshList() {
dqItems = DQList.sharedInstance.allItems()
dqItems.sort(by: {$0.date < $1.date})
tableView.reloadData()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dqItems.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "dqCell", for: indexPath) // retrieve the prototype cell (subtitle style)
var dqItem = dqItems[(indexPath as NSIndexPath).row] as DQItem
cell.textLabel?.text = dqItem.title as String!
if (dqItem.isOverdue)
{ // the current time is later than the to-do item's date
let newDate = Calendar.current.date(byAdding: .day, value: 7, to: dqItem.date)!
dqItem.date = newDate
dateFormatter.dateFormat = "EEEE's' 'at' HH:mm"
print("newdate", newDate) //seven days is added!
print("dqItem.d", dqItem.date) //same here
viewDidAppear(true) //reload the view to re-sort the cells??? = not working
}
cell.detailTextLabel?.text = dateFormatter.string(from: dqItem.date as Date)
return cell
}//End of cellforrowatindexPath
The DQItem, where the struct is.
import UIKit
struct DQItem {
var title: String
var date: Date
var UUID: String
var mood: String
init(date: Date, title: String, UUID: String, mood: String) {
self.date = date
self.title = title
self.UUID = UUID
self.mood = mood
}
//Overdue function
var isOverdue: Bool {
return (Date().compare(self.date) == .orderedDescending) // date is earlier than current date
}
}
Appreciate all help trying to solve this silly problem!
Don't call viewDidAppear. That method is supposed to only be called by the UIKit. Just call your own refresh code instead.
(Even if calling view state methods was a good idea, you would be calling the wrong one since you implemented viewWillAppear to do your initial refresh.)
EDIT:
For the problem of how to change your table ordering while the table is being updated, you need to wait until the current update is finished. One way to do that would be to submit the next reload request to the main queue. Since the main queue is serial, that would delay it until current work finishes.
Try moving the table reordering into its own method...
func orderTable() {
dqItems.sort(by: {$0.date < $1.date})
tableView.reloadData()
}
...then call it via GCD instead of calling the view lifecycle method.
DispatchQueue.main.async {
orderTable()
}
That's the simple version. If you want to be more efficient, you can look into ways to make sure the orderTable call only happens once per reload.
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)"
}