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.
Related
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!
Is there anyway to lock buttons until the following day? For example, my app has coupons in the form of buttons and when you click the button "Coupon 1" it toggles to say "USED + the current date". I want the user to be able to only use 2 coupons per ViewController/page per dag, and am wondering if its possible to lock the other coupons once two are pressed by the user, and the coupons unlock the following day. I don't want the buttons to disappear, just want them to be locked. Below is the code for one of my buttons/coupons. It would be ideal if a lock actually appeared in the corner of the remaining buttons after 2 are pressed.
var didClick : Bool = false
#IBAction func special1BTNpressed(_ sender: UIButton) {
if !didClick {
didClick = true
let date = Date()
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy"
let result = formatter.string(from: date)
sender.setTitle("USED " + result, for: .normal)
You can store the value of the date of which you did the action and then later compare it with today
func canPerformActionForSpecialBTN() -> Bool {
let timeInterval = UserDefaults.standard.double(forKey: "ActionDateForSpecialBTN")
guard timeInterval.isNormal else { return true }
let date = Date(timeIntervalSince1970: timeInterval)
guard Calendar.current.isDateInToday(date) else { return true }
return false
}
func performSpecialBTNAction() {
guard canPerformActionForSpecialBTN() else { return }
defer { UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: "ActionDateForSpecialBTN") }
//implement action here
}
and maybe in the viewDidAppear you can enable or disable the button
button.isEnabled = !canPerformActionForSpecialBTN()
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.
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)"
}
Why is "1925-1-23" and "1924-2-5" being excluded when I try to call an image with them in date picker? I am successfully able to call images with every date in between the two dates above. Why are the dates "1925-1-23" and "1924-2-5" the only dates not calling images.
import UIKit
public func ==(lhs: NSDate, rhs: NSDate) -> Bool {
return lhs === rhs || lhs.compare(rhs) == .OrderedAscending
}
public func <(lhs: NSDate, rhs: NSDate) -> Bool {
return lhs.compare(rhs) == .OrderedDescending
}
extension NSDate: Comparable { }
class ViewController: UIViewController {
#IBOutlet weak var displayAnimal: UIImageView!
#IBOutlet weak var dateWheel: UIDatePicker!
#IBAction func goButton(sender: UIDatePicker)
{
let dateStringFormatter = NSDateFormatter()
dateStringFormatter.dateFormat = "yyyy-MM-dd"
dateStringFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
{
let dateStringFormatter = NSDateFormatter()
dateStringFormatter.dateFormat = "yyyy-MM-dd"
dateStringFormatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
let rabbit1A = dateStringFormatter.dateFromString("1924-2-5")
let rabbit2A = dateStringFormatter.dateFromString("1925-1-23")
if(dateWheel.date.compare(rabbit1A!) == NSComparisonResult.OrderedDescending &&
dateWheel.date.compare(rabbit2A!) == NSComparisonResult.OrderedAscending)
{
// set the image of UIImageView
displayAnimal.image = UIImage(named: "rabbit")
}
In your IBOutlet:
#IBAction func selectDateButton(sender : UIDatePicker) {
// set the date criteria here
if(sender.date.compare(dateA) == NSComparisonResult.OrderedAscending && sender.date.compare(dateB) == NSComparisonResult.OrderedDescending) {
// set the image of UIImageView
displayAnimal.image = UIImage(named: "rabbit")
}
}
where dateA and dateB are the date (in form of NSDate) that falls into the criteria of displaying rabbit image.
A date picker won't talk to an image view. A date picker is a UIControl.
The logic to do what you want belongs in your view controller.
You would set up an IBOutlet to your date picker.
You'd add an OK button to your view controller in IB that linked to an IBAction method.
In that IBAction method you would look at the date property of the picker (using the picker's outlet.) If the date matches one of your magic dates, you'd have code that would load one of your images into an image view that was linked to another IBOutlet in your view controller.
Break it down into baby steps. Hook up the outlets and actions. At first just put a println("In button action") in your IBAction method. Next, fetch the date from the picker and log that in your IBAction.
Next, figure out how to write a switch statement that matches your different dates and prints statements when you match your different dates. That will require a detour into the docs to read about Swift's very powerful switch statement.
Next figure out how to load your different images from your array of names and install them into an image view on your view controller when you click a button. (First set up an Int instance variable imageIndex that starts at 0, and have a button press load an image name from you array of image names, increment the index for next time, and then installs the loaded image into your image view.
Finally, adjust your switch statement so your various magic dates load the desired image into your image view.