UICollectionViewCell losing accessibilityLabel - swift

I have a UICollectionViewCell that includes a label with a date in it. I'm adding the accessibilityLabel property to be a formatted date so that the screen reader can properly read the date.
This is working great, however it seems to be random. I'm assuming it has something to do with the reuse of the cell, but logging out values seems to show that everything is being set correctly.
I'm also clearing everything out on prepareForReuse.
var reviewResult: ReviewA15? {
didSet {
guard let result = reviewResult else { return }
dateLabel.text = result.submissionTime.toString(dateTemplate: DateTemplate.standard)
dateLabel.accessibilityLabel = DateFormatter.localizedString(from: result.submissionTime, dateStyle: .long, timeStyle: .none)
}
}
override func prepareForReuse() {
dateLabel.text = nil
dateLabel.accessibilityLabel = nil
}
Any ideas? Is this just a bug with Apple?

Related

How to get initial value of datePicker Swift

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.

swift uidatePicker pass as arguements not working properly

i am stuck in situation i have to pass a uidatepicker to a cell class. the cell class contains 14 textfields and datepicker will be used as inputview to select to and from time for whole week. the thing is when i directly initialize it in cell class. it gets initialized 14 times and thats too much costly task. but while passing as arguement it replicate the selected time to all the textfield instead of that specific one. i tried to use tags and some other ways as well but no luck. how to handle the situation that it will not impact applicatio performancee .any help regarding this will be appriciated.thankyou
Code:
lazy var fromDatePicker: UIDatePicker = {
let fromDatePicker = UIDatePicker()
fromDatePicker.datePickerMode = .time
fromDatePicker.locale = NSLocale(localeIdentifier: "en_US") as Locale
fromDatePicker.minuteInterval = 30
if #available(iOS 13.4, *) {
fromDatePicker.preferredDatePickerStyle = .wheels
} else {
// Fallback on earlier versions
}
return fromDatePicker
}()
cell.configureCellForEmployee(with:viewModel.profileDataSource, index: indexPath.row, fromDatePicker: fromDatePicker)
Try adjusting the code.
class "you class": UIViewController{
lazy var fromDatePicker: UIDatePicker = {
let fromDatePicker = UIDatePicker()
fromDatePicker.datePickerMode = .time
fromDatePicker.locale = NSLocale(localeIdentifier: "en_US") as Locale
fromDatePicker.minuteInterval = 30
if #available(iOS 13.4, *) {
fromDatePicker.preferredDatePickerStyle = .wheels
} else {
// Fallback on earlier versions
}
return fromDatePicker
}()
}

Sorting most descending date at top TableView

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.

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

UIDatePicker to call specific image in UIImageView

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.