How to update UITableViewCells using NSTimer and NSNotificationCentre in Swift - ios

NOTE: Asking for answers in Swift please.
What I'm trying to do:
Have tableview cells update every 1 second and display a real-time
countdown.
How I'm doing it currently:
I have a tableview with cells that contain a label.
When the cells are generated, they call a function that calculates the time between today and a stored target date, and it displays the countdown time remaining in the label.
To get the cells to update the label every second, I
use an NSTimer that reloads the tableview every second.
PROBLEM: The countdown works, but when I try to do a Swipe-to-Delete action, because the NSTimer reloads the tableview, it resets the
swiped cell so that it is no longer swiped and of course this is not acceptable for end users.
What I'm trying / Potential Solutions
I heard that a better way to do this is to use NSNotificationCenter that is triggered by NSTimer and add listeners to each cell. Every 1 second a "broadcast" is sent from the NSNotificationCenter, the cell will receive the "broadcast" and will update the label.
I tried adding a listener to each cell in the part of the code where it generates a new cell:
// Generate the cells
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
var cell = tableView.dequeueReusableCellWithIdentifier("Tablecell", forIndexPath: indexPath) as UITableViewCell
let countdownEntry = fetchedResultController.objectAtIndexPath(indexPath) as CountdownEntry
// MARK: addObserver to "listen" for broadcasts
NSNotificationCenter.defaultCenter().addObserver(self, selector: "updateCountdownLabel", name: mySpecialNotificationKey, object: nil)
func updateCountdownLabel() {
println("broadcast received in cell")
if let actualLabelCountdown = labelCountdown {
// calculate time between stored date to today's date
var countdownValue = CountdownEngine.timeBetweenDatesRealTime(countdownEntry.date)
actualLabelCountdown.text = countdownValue
}
}
but the observer's selector only seems to be able to target functions at the view controller level, not in the cell. (I can't get "updateCountdownLabel" to fire from within the cell. When I create a function of the same name at view controller level, the function can be called)
Questions
So my question is: Is this is the right approach?
If yes, how can I get the listener to fire a function at the cell level?
If no, then what is the best way to achieve this real-time countdown without interrupting Swipe on Cell actions?
Thanks in advance for your help!

It might be a solution not reloading cells at all. Just make the cells listen to an update notification and change their label accordingly.
I assume you subclass UITableViewCell and give the cell a storedDate property. You will set that property when preparing the cell.
The timer will just fire the notification.
Remember to unregister the cell from notification center in dealloc
Here is a quick an dirty example.
The View Controller containing your TableView:
class ViewController: UIViewController, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var timer: NSTimer!
//MARK: UI Updates
func fireCellsUpdate() {
let notification = NSNotification(name: "CustomCellUpdate", object: nil)
NSNotificationCenter.defaultCenter().postNotification(notification)
}
//MARK: UITableView Data Source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "CustomCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) as! CustomTableViewCell
cell.timeInterval = 20
return cell
}
//MARK: View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
self.timer = NSTimer(timeInterval: 1.0, target: self, selector: Selector("fireCellsUpdate"), userInfo: nil, repeats: true)
NSRunLoop.currentRunLoop().addTimer(self.timer, forMode: NSRunLoopCommonModes)
}
deinit {
self.timer?.invalidate()
self.timer = nil
}
}
The custom cell subclass:
class CustomTableViewCell: UITableViewCell {
#IBOutlet weak var label: UILabel!
var timeInterval: NSTimeInterval = 0 {
didSet {
self.label.text = "\(timeInterval)"
}
}
//MARK: UI Updates
func updateUI() {
if self.timeInterval > 0 {
--self.timeInterval
}
}
//MARK: Lifecycle
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
let notificationCenter = NSNotificationCenter.defaultCenter()
notificationCenter.addObserver(self, selector: Selector("updateUI"), name: "CustomCellUpdate", object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
}
I'm pretty sure this example doesn't adhere to your app's logic. Just showing how things are glowed together.

As Ian MacDonald has suggested you should avoid reloading the cell when the timer ticks, if you are swiping. Also drop the NSNotification, as It and timer are essentially doing the samething

Related

Multiple Timers in Tableview Swift

I am trying to have timer for each row which is added manually when user clicks on add button.
Start time is set to 100units(not important for the question) and it should count down.
When new row is added it will have it's own timer started and display the value on this new row.
I tried to have timer in each cell but this is creating issue when dequeuing, so I created array of timer to hold corresponding timer for each cell. Problem I am facing right now is how to update the cell value every second(timer interval)
MultipleTimersTableView_Gist is the link for the code I wrote so far. I thought of using delegate to update the cell but not sure with the best approach.
Any help would be appreciated.
EDIT:
Timers should show the time in increasing order because each row is created(along with timer)from top to bottom meaning first will have less time than next. Looks like while dequeueing something messed up.
Here is the gist I used for above screenshot
Here is how you can handle timers in each UITableViewCell.
Create a custom UITableViewCell
class CustomCell: UITableViewCell {
//MARK: Outlets
#IBOutlet weak var label: UILabel!
//MARK: Internal Properties
var handler: ((Int)->())?
//MARK: Private Properties
private var timer: Timer?
private var counter = 100 {
didSet {
DispatchQueue.main.async {
self.label.text = "\(self.counter)"
self.handler?(self.counter)
}
}
}
//MARK: Internal Methods
func configure(with counter: Int) {
self.counter = counter
self.setTimer()
}
//MARK: Private Methods
private func setTimer() {
self.timer?.invalidate()
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true, block: {[weak self] (timer) in
if let counter = self?.counter, counter > 0 {
self?.counter -= 1
} else {
timer.invalidate()
}
})
}
}
In the above code,
I've created a label that will update the counter value in UI.
handler - it will and store the updated counter value somewhere (in ViewController, explained further) when the cell is moved out of the screen
timer - schedule the timer in the cell with timeinterval = 1
counter - current counter value for each cell
In the ViewController,
class VC: UIViewController, UITableViewDataSource {
let numberOfCells = 20
var timerArr = [Int]()
override func viewDidLoad() {
super.viewDidLoad()
self.timerArr = [Int](repeating: 100, count: numberOfCells)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.numberOfCells
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! CustomCell
cell.configure(with: self.timerArr[indexPath.row])
cell.handler = {[weak self] (counter) in
self?.timerArr[indexPath.row] = counter
}
return cell
}
}
In the above code,
timerArr - keeps track of the counter value for each cell in the tableView.
In tableView(_:cellForRowAt:), the counter for each cell is updated using the handler we created previously in CustomCell.
I would use one timer for all the cells. When creating your object and adding it to the datasource, record the dateAdded = Date(), then when the cells are rebuilt on the timer fire, get the seconds count for each cell from the dateAdded field and update the cell in cellForRowAtIndex.

how to update data in subcontroller in ios, swift

I have viewcontroller and it has split by two views. one is view which cotrolled by tableviewcontroller which is childcontroller of viewcontroller, and one is statusview which is show data directly when tableviewcontrollersdidselectrowatindexpathis called. but when i cant reload my data in statusview when i called didselectrowatindexpath. i can relaod my data in tableview but status view does not reflect data.
the darkgray area is statusview and middle view is tableview.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
self.tableView.reloadData()
}
by this code i can reload my data in tableview . but when i use setneedsDisplay or setneedslayout of parentController(viewcontroller) it crash. how can i solve this problem?
In this case you will need one of these two options:
Create a protocol that your main UIViewController will implement, so that UITableViewController can pass data via the delegate to its parent view controller
Post a UINotification in the UITableViewController and receive this notification in your status bar view and display the data.
Lets explore both options:
Define a protocol, in this example I am only sending a String of the cell that was tapped on:
#objc protocol YourDataProtocol {
func didSelectCell(withString string: String)
}
Next add a delegate property to your UITableViewController
class YourTableViewController: UIViewController {
weak var delegate: YourDataProtocol?
...
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
//call the delegate method with your text - in this case just text from textLabel
if let text = cell?.textLabel?.text {
delegate?.didSelectCell(withString: text)
}
}
}
Make your UIViewContoller be the delegate of the UITableViewController subclass:
class YourViewController: UIViewController, YourDataProtocol {
...
let yourTableVC = YourTableViewController(...
yourTableVC.delegate = self
func didSelectCell(withString string: String) {
statusBar.text = string//update the status bar
}
}
Second option is with using NotificationCenter
In your UITableViewController you post a notification
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let cell = tableView.cellForRow(at: indexPath)
if let text = cell?.textLabel?.text {
let notificatioName = Notification.Name("DataFromTableViewCell")
NotificationCenter.default.post(name: notificatioName, object: nil, userInfo: ["YourData": text])
}
}
In status bar you start listening to this notification
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveData(_:)), name: notificatioName, object: nil)
#objc func didReceiveData(_ notification: Notification) {
if let userData = notification.userInfo, let stringFromCell = userData["YourData"] {
print(stringFromCell)
}
}

How to trigger UITableViewCell editActions programmatically? [duplicate]

This question already has answers here:
Open UITableView edit action buttons programmatically
(5 answers)
Closed 5 years ago.
I have made a few custom edit actions on my tableviewcell. It works fine when I swipe, but I was wondering if there was any way to trigger these actions when I tap the cell. Also, I have seen lots of people answer similar questions with just,
tableView.setEditing(true, animated: true)
though this is not the solution I am looking for. I want the actions to immediately get displayed without the press of another button.
Short answer is - there is no such way.
However, if you really need something like that, you can mimic this behaviour, though it requires lot more implementation and state handling on your own.
Here is a quick and very dirty solution, which overrides touchesEnded method of your custom cell. Remember to set up Cell as a dynamic prototype of the cell in your table view in relevant storyboard and set its reuse identifier to identitifer.
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, CellDelegate {
#IBOutlet weak var tableView: UITableView?
override func viewDidLoad() {
super.viewDidLoad()
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 10
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "identifier") as? Cell else {
return UITableViewCell()
}
cell.textLabel?.text = "\(indexPath.row)"
cell.indexPath = indexPath
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
return nil
}
func doAction(for cell: Cell) {
let indexPath = cell.indexPath
print("doing action for cell at: \(indexPath!.row)")
// your implementation for action
// maybe delete a cell or whatever?
cell.hideFakeActions()
}
}
protocol CellDelegate: class {
func doAction(for cell: Cell)
}
class Cell: UITableViewCell {
weak var delegate: CellDelegate?
var indexPath: IndexPath!
#IBOutlet weak var buttonAction1: UIButton?
#IBOutlet weak var constraintButtonFakeActionWidth: NSLayoutConstraint?
override func awakeFromNib() {
self.constraintButtonFakeActionWidth?.constant = 0
}
override func touchesEnded(_ touches: Set<UITouch>,
with event: UIEvent?) {
guard let point = touches.first?.location(in: self) else {
return
}
if self.point(inside: point, with: event) {
print("event is in cell number \(indexPath.row)")
self.showFakeActions()
}
}
func showFakeActions() {
self.constraintButtonFakeActionWidth?.constant = 80
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
}
func hideFakeActions() {
self.constraintButtonFakeActionWidth?.constant = 0
UIView.animate(withDuration: 0.3) {
self.layoutIfNeeded()
}
}
#IBAction func fakeAction() {
delegate?.doAction(for: self)
}
}
So how does it work? Each cell is a UIView which inherits from abstract UIResponder interface. You can override its methods to do actions on your own on behalf of events that are dispatched to them. This is the first step where you override touchesEnded.
Take a look at the screenshot from interface builder - you have to hook up the constraint.
I've also implemented the delegate which returns nil for all edit actions of the cells, so they don't interfere with your workaround.
Next, you set up a custom delegate to get a callback from the cell. I also attached IndexPath to the cell for the convenience of managing data in the dataSource, which you have to implement.
Remember that this code lacks a lot, like prepareForReuse method implementation. Also, you probably want to do additional checks in touchesEnded override which would guarantee that this delegate callback is not fired more than once per touch and prevent multiple touches. The logic for disabling user interaction on a cell is not implemented here as well. And interface requires fine-tuning (like text appears to be squashed during the animation).

Swift – Table view data not reloading after dismissing view controller

I have a view in my app called JournalViewController that I'm presenting over my PastSessionsViewController. PastSessions has a table view that the user can tap to edit and bring up the journal.
When the user edits an entry and saves it (saving to CoreData), dismissing JournalViewController I'd like for the table view in PastSessions to reflect those changes and show the updated table cell.
I'm calling tableView.reloadData() in PastSessionsViewController viewDidLoad() but that doesn't seem to be working. I've also added a delegate for JournalViewController to interact with PastSessionsViewController ahead of dismissViewController
Here's some code to look at:
In PastSessionsViewController:
class PastSessionsViewController: UIViewController, UITableViewDelegate, JournalVCDelegate {
weak var tableView: UITableView?
weak var backButton: UIButton?
let pastSessionsDataSource: PastSessionsDataSource
init() {
pastSessionsDataSource = PastSessionsDataSource()
super.init(nibName: nil, bundle: nil)
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let tableView = UITableView()
tableView.backgroundColor = nil
tableView.delegate = self
tableView.dataSource = pastSessionsDataSource
tableView.registerClass(EntryCell.self, forCellReuseIdentifier: "cell")
view.addSubview(tableView)
self.tableView = tableView
}
override func viewDidAppear(animated: Bool) {
tableView?.reloadData()
}
func didFinishJournalVC(controller: JournalViewController) {
var newDataSource = PastSessionsDataSource()
tableView?.dataSource = newDataSource
// tried this ^, but it's causing the app to crash
// tableView?.reloadData() <- this isn't doing the trick either
dismissViewControllerAnimated(true, completion: nil)
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let editJournalVC = JournalViewController(label: "Edit your thoughts")
editJournalVC.delegate = self
presentViewController(editJournalVC, animated: true, completion: nil)
}
}
In JournalViewController:
protocol JournalVCDelegate {
func didFinishJournalVC(controller: JournalViewController)
}
class JournalViewController: UIViewController, UITextViewDelegate {
var delegate: JournalVCDelegate! = nil
func doneJournalEntry(sender: UIButton) {
journalEntryTextArea?.resignFirstResponder()
... do some core data saving ...
delegate.didFinishJournalVC(self)
}
}
In PastSessionsDataSource:
import UIKit
import CoreData
class PastSessionsDataSource: NSObject {
var arrayOfEntries = [Entry]()
var coreDataReturn: [Meditation]?
func prepareEntries() {
// gets stuff from coredata and formats it appropriately
}
override init() {
super.init()
prepareEntries()
}
}
extension PastSessionsDataSource: UITableViewDataSource {
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return arrayOfEntries.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) as! EntryCell
... set up the labels in the cell ...
return cell
}
}
Thanks for looking!
viewDidLoad is called when the view controller load its view at the first time, so basically it will only be called once during the view controller's whole life cycle.
One quick solution is to put tableView.reloadData() in PastSessionsViewController viewWillAppear() or viewDidAppear().
However I do not like this quick solution as every time you dismiss JournalViewController, the table view will be reloaded, even the user has not changed anything on JournalViewController (for example, cancel the edit). So I suggest to use delegate approach between PastSessionsViewController and JournalViewController, when the user actually edit the data on JournalViewController then inform PastSessionsViewController to refresh the table.
You are currently prepare entries only on init of PastSessionsDataSource, but not after you did CoreData changes. So each time when you reloadData for tableView you work with the same data set loaded initially. As a quick hack you can try to updated viewDidAppear in a following way:
override func viewDidAppear(animated: Bool) {
if let tableView = tableView {
let dataSource = tableView.dataSource! as PastSessionsDataSource
dataSource.prepareEntries()
tableView.reloadData()
}
}
Your tableView property is probably nil in viewDidAppear, based on your listed code. The reason is that in viewDidLoad you construct a UITableView as tableView, and that is a local variable. You need to assign that variable to the property:
self.tableView = tableView

How do I get the row of a custom UITableViewCell using a button in the custom cell, that will be sent to deleteRowsAtIndexPaths

I have made a table view in iOS that displays a list of buddy (friend) requests. For the buddy request cell, I have made it a prototype cell and have given it a custom class that extends from UITableViewCell. When I click the "Accept" button on the cell, I want to remove that row from the requests array I have and remove it from the table view as well.
The three options I have considered are
1) Giving the custom cell a property for row that corresponds to the row in the table, and hence, the row in the requests array. Then, when accept is called, pass that row to the delegate function and call
requests.removeAtIndex(row)
tableView.reloadData()
which updates all the custom cells' row property. This method works. However, is this a bad practice to reload the table data (it's only reloading from the stored array, not making a network request)
2) Giving the custom cell the row property, but then calling
self.requests.removeAtIndex(row)
self.requestsTableView.beginUpdates()
self.requestsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow:row, inSection: 0)], withRowAnimation: UITableViewRowAnimation.Fade)
self.requestsTableView.endUpdates()
However, this does not update the row value in each of the cells following the deleted cell, and I would somehow either have to update them all, or call reloadData() which isn't what I want to do.
3) Instead of passing the row value, when the "Accept" button is clicked, search for the username in the buddies list, get the index of where it is found, and then delete the row in the table using that index and deleteRowsAtIndexPaths. This seems okay to do, especially since I'll never have a huge amount of buddy requests at once and searching won't require much time at all, but I figure if I had immediate access to the row value, it would make things cleaner.
Here is the code:
View Controller
class RequestsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, RequestTableViewCellDelegate
{
// Outlet to our table view
#IBOutlet weak var requestsTableView: UITableView!
let buddyRequestCellIdentifier: String = "buddyRequestCell"
// List of buddies who have sent us friend requests
var requests = [Buddy]()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(animated: Bool) {
self.getBuddyRequests()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
// MARK: -Table View
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return requests.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell: RequestTableViewCell = tableView.dequeueReusableCellWithIdentifier(buddyRequestCellIdentifier) as! RequestTableViewCell
let buddy = requests[indexPath.row]
let fullName = "\(buddy.firstName) \(buddy.lastName)"
cell.titleLabel?.text = fullName
cell.buddyUsername = buddy.username
cell.row = indexPath.row
cell.delegate = self
return cell
}
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let buddy = self.requests[indexPath.row]
}
func didAccpetBuddyRequest(row: Int) {
// Remove buddy at the 'row' index
// idea 1: update all cells' 'row' value
//self.requests.removeAtIndex(row)
// reloading data will reload all the cells so they will all get a new row number
//self.requestsTableView.reloadData()
// idea 2
// Using row doesn't work here becuase these values don't get changed when other cells are added/deleted
self.requests.removeAtIndex(row)
self.requestsTableView.beginUpdates()
self.requestsTableView.deleteRowsAtIndexPaths([NSIndexPath(forRow:row, inSection: 0)], withRowAnimation: UITableViewRowAnimation.Fade)
self.requestsTableView.endUpdates()
// idea 3: don't use row, but search for the index by looking for the username
}
// MARK: -API
func getBuddyRequests() {
// self.requests = array of buddy requests from API request
self.requestsTableView.reloadData()
}
}
Custom UITableViewCell and protocol for the delegate call
protocol RequestTableViewCellDelegate {
func didAccpetBuddyRequest(row: Int)
}
class RequestTableViewCell: UITableViewCell {
#IBOutlet weak var titleLabel: UILabel!
#IBOutlet weak var acceptButton: UIButton!
var delegate: RequestTableViewCellDelegate?
var buddyUsername: String?
var row: Int?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
#IBAction func touchAccept(sender: AnyObject) {
// <code goes here to make API request to accept the buddy request>
self.delegate?.didAccpetBuddyRequest(self.row!)
}
}
Thanks for taking the time to read this, I appreciate any help/best practices that you know that could help me in this situation.
There shouldn't be a problem with giving the cell the indexPath and delegate properties, and then informing the delegate when the Accept button has been tapped. You do need to call reloadData(), though, to update the references in the cells that are affected.
If you wish to minimise the number of reloaded rows, call reloadRowsAtIndexPaths() instead, but I think that creating the loop that creates the NSIndexPath objects will slow your app down just the same.
As an alternative I can suggest you another way:
First add action method to your acceptButton in viewController. Inside that method you can get indexPath of the cell that contains button. Here is implementation
#IBAction func acceptDidTap(sender: UIButton) {
let point = tableView.convertPoint(CGPoint.zeroPoint, fromView: button)
if let indexPath = tableView.indexPathForRowAtPoint(point) {
// here you got which cell's acceptButton triggered the action
}
}

Resources