Swift provides a powerful way to update table view changes when you're using reloadData(). But it's works on the certain places as such viewDidLoad and viewDidAppear or refreshControl. It would be better when the tableView gets an automatic way to reload data. It's possible to update the tableView when the app is foreground? You can see such as effect in the iOS Reminders app.
Example: iOS Reminder
Let's say you've scheduled reminder. And you're still using the app, at this time your scheduled notification is fired and prompted about you must be do something. At this time your scheduled reminder becomes an overdue item, when you're doing nothing and the detailTextLabel gets a red color. There is no any user interactions.
Here's what I've done:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
// Configure the cell...
let managedObject = self.fetchedResultsController.object(at: indexPath)
cell.textLabel?.text = managedObject.value(forKey: "string") as? String
cell.detailTextLabel?.text = "\(managedObject.value(forKey: "date") as! Date)"
print("Get called")
for viewController in (self.tabBarController?.viewControllers)! {
let overdue = self.fetchedResultsController.fetchedObjects?.filter({ (record) -> Bool in
return (record.date?.compare(Date()) != .orderedDescending)
})
if viewController.tabBarItem.tag == 1 {
if overdue?.count == 0 {
DispatchQueue.main.async(execute: {
viewController.tabBarItem.badgeValue = ""
viewController.tabBarItem.badgeColor = UIColor.clear
})
} else if overdue?.count != 0 {
DispatchQueue.main.async(execute: {
viewController.tabBarItem.badgeValue = "\(overdue!.count)"
viewController.tabBarItem.badgeColor = UIColor.init(red: 0.0, green: 0.5, blue: 0.0, alpha: 1.0)
})
}
}
}
DispatchQueue.main.async {
if managedObject.date?.compare(Date()) != .orderedDescending {
cell.detailTextLabel?.textColor = UIColor.red
} else {
cell.detailTextLabel?.textColor = UIColor.black
}
}
return cell
}
Thanks
Related
I want to ask how can save selected all item in tableViewCell like this picture below. so later I can post it with with alamofire, I don't have any experience with posting data before. here is what have I done.
this is my button so I can later post in with alamofire, so far I want to test it.
#objc func handleSubmit() {
var data: [String] = []
for (index, value) in attendance.enumerated() {
print("index attendance: \(index), value attendance: \(value.status)")
let cell = tableView.cellForRow(at: IndexPath(item: index, section: 0)) as? GStudentAbsenceCell
UserServices.shared.postUserAttendances(status: cell?.status ?? "")
data.append(cell?.status ?? String(index))
}
print(data)
}
// In My UITableViewCell
var status: String?
statusLbl.didSelect { (selectedText , index ,id) in
if selectedText == "Sakit" || selectedText == "Izin" || selectedText == "Alpha" {
self.status = selectedText
self.statusLbl.backgroundColor = #colorLiteral(red: 0.8666666667, green: 0.4078431373, blue: 0.2705882353, alpha: 0.2)
self.statusLbl.textColor = #colorLiteral(red: 0.8666666667, green: 0.4078431373, blue: 0.2705882353, alpha: 1)
}
}
You cannot safely store your data models state in a table view cell, because upon scrolling the cell will likely to be reused for newly visible rows, hence it will store the state for a different row and therefore will lose the "old" state.
You need to store the state in your model; if it's not feasible to use your existing model, you might create a transient dictionary which maps the IndexPath to the state
What you could do is the following:
Extend your Cell with a custom callback handler, which is called when the status is changed.
When you set up your cell, hand in a closure that will deal with those status changes
In that closure, update the model
Some pseudo code might help you:
// in your custom cell
typealias Handler: (String) -> ()
class CustomTableViewCell : UITableViewCell {
var selectionHandler:Handler?
func didSelect(...) {
selectionHandler?(lblStatus.text)
}
}
// in your view controller
// in rowForCellatIndexPath:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
let cell = /* dequeue */ as! CustomTableViewCell
cell.selectionHandler = { statusText in
let row = indexPath.row
attendance[row].status = statusText /* get the status from the cell */
}
return cell
}
I wanted to make when tabbaritem click call api and fill tableView. But in my case i had to use multiple cell.
up category (tab bar item) -> category (1- cell) -> products (2- cell)
up category (tab bar item)-> products (2- cell)
i have two prototype cell in tableView. First cell for show category. Second cell for show products.
When category click, i reloaded tableview then i can show products.
But when i showed products just only 1 time. My tableview unavaliable. I am sure i call reload data.
Message from debugger: Terminated due to signal 9
Here's my code,
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
if let orderType = selectedOrderType
{
switch orderType
{
case OrderViewHelper.OrderType.Category:
return self.categories.count;
case OrderViewHelper.OrderType.Menu:
return self.menus.count;
case OrderViewHelper.OrderType.Product:
return self.products.count;
}
}
return 0;
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
var returnerCell = UITableViewCell();
switch selectedOrderType!
{
case OrderViewHelper.OrderType.Category:
let cell = tableView.dequeueReusableCell(withIdentifier: ViewCellStatics.TABLE_ORDER_VIEW_CELL, for: indexPath) as! CategoryTableViewCell;
cell.lblCategoryId.text = self.categories[indexPath.row].Id;
cell.lblCategoryName.text = self.categories[indexPath.row].Name;
cell.lblCategoryId.isHidden = true;
returnerCell = cell;
break;
case OrderViewHelper.OrderType.Menu:
let cell = tableView.dequeueReusableCell(withIdentifier: ViewCellStatics.TABLE_PRODUCT_VIEW_CELL, for: indexPath) as! ProductsTableViewCell;
cell.lblPrice.text = self.menus[indexPath.row].Price;
cell.lblProductName.text = self.menus[indexPath.row].Name;
cell.lblId.text = self.menus[indexPath.row].Id;
cell.lblId.isHidden = true;
cell.lblProductName.numberOfLines = 2;
returnerCell = cell;
break;
case OrderViewHelper.OrderType.Product:
let cell = tableView.dequeueReusableCell(withIdentifier: ViewCellStatics.TABLE_PRODUCT_VIEW_CELL, for: indexPath) as! ProductsTableViewCell;
cell.lblPrice.text = self.products[indexPath.row].Price;
cell.lblProductName.text = self.products[indexPath.row].Name;
cell.lblId.text = self.products[indexPath.row].Id;
cell.lblId.isHidden = true;
cell.lblProductName.numberOfLines = 2;
cell.addButton.Model = self.products[indexPath.row];
cell.addButton.addTarget(self, action: #selector(addBasket(_:)), for: .touchUpInside);
break;
}
return returnerCell;
}
private func getCategories(upperCategoryId : String)
{
var paramaters = [WebServiceParamater]();
paramaters.append(WebServiceParamater(key: WebServiceVariableKeys.UPPER_CATEGORY, value: upperCategoryId));
WebService().GetData(action: ActionNames.CATEGORIES, paramater: paramaters)
{
(objects) in
if (objects == nil || objects?.count == 0) { return; }
self.ClearModels(type: OrderViewHelper.OrderType.Category);
DispatchQueue.main.sync
{
self.categories = JsonParser.ParseCategories(jsonArray: objects);
self.tableOrders.reloadData();
}
}
}
private func getProducts(category : CategoryModel)
{
var paramaters = [WebServiceParamater]();
paramaters.append(WebServiceParamater(key: WebServiceVariableKeys.CATEGORY, value: category.Id));
WebService().GetData(action: ActionNames.PRODUCT, paramater: paramaters)
{
(objects) in
if (objects == nil || objects?.count == 0) { return; }
self.ClearModels(type: OrderViewHelper.OrderType.Product);
DispatchQueue.main.sync
{
self.products = JsonParser.ParseProduct(jsonArray: objects);
self.tableOrders.reloadData();
}
}
}
Screenshots
Category
I clicked one category so i can see products
Reload data doesnt work.
Never call DispatchQueue.main.sync, try to DispatchQueue.main.async instead.
BTW: Make sure your methods in WebService calling asynchronously in background threads so you won't block UI while loading something.
Hope this answer will help you in understandings https://stackoverflow.com/a/44324968/4304998
So the issue is when a cell is tapped, desired data is shown and when again tapped on same cell ( again desired data is shown.)
But when one cell is selected and we again select other cell (then the data is been shown of second tapped cell but the first one is not deselected).
How can I take care of this issue?
var selectedIndex = -1
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
UIView.animate(withDuration: 1) {
self.labelViewHeightConstraint.constant = 60
self.labelLeadingConstraint.constant = 136
self.view.layoutIfNeeded()
}
let cell = tableView.cellForRow(at: indexPath) as! CustomCell
if(selectedIndex == indexPath.row) {
selectedIndex = -1
print("deselect")
UIView.animate(withDuration: 0.4) {
cell.secondView.isHidden = true
cell.firstView.backgroundColor = UIColor(red: 0.8588, green: 0.84705, blue: 0.8745, alpha: 1.0)
}
} else {
cell.secondView.isHidden = false
}
self.expandTableView.beginUpdates()
//self.expandTableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic )
self.expandTableView.endUpdates()
}
You can archive single selection by setting tableView property like belwo
tableView.allowsMultipleSelection = false
This can also be done from Attributes Inspector
Hope this helps
you must disable multiple selection by,
self.tbl.allowsMultipleSelection = false
and enable single selection by,
self.tbl.allowsSelection = true
EDIT:-
if you want to access your old (selected cells), you should make a call like this,
//first assign tag or indexPath in Cell,
cell.tag = indexPath.row
// or
cell.indexPath = indexPath
//then fetch like bellow,
let visibleCell = tableView.visibleCells.filter({$0.tag == self.selectedIndex})
//or
let visibleCell = tableView.visibleCells.filter({$0.indexPath.row == self.selectedIndex})
//if you use ,
let cell = tableView.cellForRow(at: indexPath) as! CustomCell
//then it will get you new cell object.
I have a ViewController that has a TableView. The tableView cell has a StackView with two images in it and outside it a label.
In the tableView:cellForRowAtIndexPath I get the cell and my data is inside a arrayList. And this is what I do:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cellIdentifier = "NamesTableViewCell"
let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier, forIndexPath: indexPath) as! NamesTableViewCell
let _name = _Names[indexPath.row]
if indexPath.row % 2 != 0 {
cell.backgroundColor = UIColor(hexString: "ededed")
}
cell.labelName.text = _name.name
if _name.gender! == "M" {
cell.pinkCircleImageView.hidden = true
} else if _name.gender! == "F" {
cell.blueCircleImageView.hidden = true
}
return cell
}
So as you can see I hide the images depending on the gender of the name and also change the background of every other cell.
Now, the behavior I am seeing is:
https://gyazo.com/1b2d39696892b7fb2f15b71696d9a925
The gender is available for every object, I've checked.
What do you guys think? Thanks!
cell.pinkCircleImageView.hidden = false
cell.blueCircleImageView.hidden = false
if _name.gender! == "M" {
cell.pinkCircleImageView.hidden = true
cell.bringSubviewToFront(blueCircleImageView)
} else if _name.gender! == "F" {
cell.blueCircleImageView.hidden = true
cell.bringSubviewToFront(pinkCircleImageView)
}
if indexPath.row % 2 == 0
{
cell.backgroundColor=UIColor.whiteColor()
}
else
{
cell.backgroundColor=UIColor(red: 248/255, green: 248/255, blue: 248/255, alpha: 1.0)
}
I'm using a tableview to display some categories in a left menu. When you select it, the data in the cells changes to courses ('Cursussen') within that category.
Each cell contains an UIImageView and an UILabel.
I noticed a while ago that when you select a course in the left menu, the label will change to that of a category. That wasn't a big issue back then, but now that I'm working to disable certain courses if they are not available it suddenly became a big issue. To indicate a course that's not available, I'm setting label.enabled = false, which works fine, however I also need to prevent the user from tapping on it and navigating to a course that's not available. To do that, I'm using tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) where I check whether the UILabel is enabled. If it's disabled the App won't navigate to the course.
Back to the issue, tapping on a course that is unavailable (which is displaying the correct image and label) will trigger the didSelectRowAtIndexPath delegate, but when I dequeue the Cell and check whether the UILabel in it is disabled it so happens to be enabled instead and furthermore the label.text does not equal the value I see in the App.
State before selecting a row:
State after selecting a row:
cellForRowAtIndexPath method:
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("LeftCell", forIndexPath: indexPath)
let label = cell.contentView.subviews[0] as! UILabel
let image = cell.contentView.subviews[1] as! UIImageView
if(selectedCategory.ID > 0 || searchCursusses.count > 0) {
//Category is selected, load course into cell
var cursus : Cursus
if(selectedCategory.ID > 0) {
cursus = cursusses[indexPath.row] as Cursus
} else {
cursus = searchCursusses[indexPath.row] as Cursus
}
image.image = self.chooseImage(false, name: cursus.category!.Name)
label.numberOfLines = 0
label.text = cursus.name
label.lineBreakMode = .ByTruncatingTail
if(defaults.boolForKey("offline-mode")) {
let realm = try! Realm()
let videos = realm.objects(DownloadedVideo).filter("video.cursus.ID = %#", cursus.ID)
if(videos.count > 0) {
label.enabled = true
} else {
label.enabled = false
}
} else {
label.textColor = UIColor.blackColor()
}
cell.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
cell.setSelected(false, animated: false)
} else {
let category = categories[indexPath.row] as Category
image.image = self.chooseImage(false, name: category.Name)
label.numberOfLines = 0
label.text = category.Name
label.lineBreakMode = .ByTruncatingTail
label.textColor = UIColor.blackColor()
cell.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
}
return cell
}
didSelectRowAtIndexPath method:
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let cell = tableView.dequeueReusableCellWithIdentifier( "LeftCell", forIndexPath: indexPath)
cell.selectionStyle = .None
if(selectedCategory.ID > 0 || searchCursusses.count > 0) {
let label = cell.contentView.subviews[0] as! UILabel
if(!label.enabled) {
return
}
let storyBoard : UIStoryboard = UIStoryboard(name: "Main", bundle:nil)
let resultViewController = storyBoard.instantiateViewControllerWithIdentifier("CursusView") as! CursusViewController
if(selectedCategory.ID > 0) {
resultViewController.loadCursus(self.cursusses[indexPath.row], completionHandler: {
self.presentViewController(resultViewController, animated:true, completion:nil)
})
} else {
resultViewController.loadCursus(self.searchCursusses[indexPath.row], completionHandler: {
self.presentViewController(resultViewController, animated:true, completion:nil)
})
}
} else {
let category = categories[indexPath.row] as Category
cell.setSelected(true, animated: false)
let label = cell.contentView.subviews[0] as! UILabel
let image = cell.contentView.subviews[1] as! UIImageView
if(label.textColor == UIColor.lightGrayColor()) {
return
} else {
image.image = self.chooseImage(true, name: category.Name)
label.numberOfLines = 0
label.text = category.Name
label.textColor = UIColor.whiteColor()
label.lineBreakMode = .ByTruncatingTail
cell.backgroundColor = UIColor(red: 45.0/255.0, green: 145.0/255.0, blue: 220.0/255.0, alpha: 1.0)
self.selectedCategory = category
self.topBarTitle.text = category.Name
let realm = try! Realm()
let cursusses = realm.objects(Cursus).filter("category.ID = %#", selectedCategory.ID)
for cursus in cursusses {
self.cursusses.append(cursus)
}
let title = self.leftMenuNav.subviews[0] as! UILabel
let titleImg = self.leftMenuNav.subviews[1] as! UIImageView
titleImg.image = UIImage(named: "back-icon")
title.text = "Cursussen"
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.tableView.slideInFromRight(0.5)
self.tableView.reloadData()
self.collectionView.crossFade(0.3)
self.collectionView.reloadData()
})
}
}
}
It seems the old cells are not properly cleaned up after calling reloaddata, causing multiple cells to be at the same IndexPath.
I'm at a loss here, please help!
The problem is the line
let cell = tableView.dequeueReusableCellWithIdentifier( "LeftCell", forIndexPath: indexPath)
in the didSelectRowAtIndexPath function because it is creating a new cell (or trying to reuse one) and corrupting the table view cell cache.
You should be getting the existing cell instead using the cellForRowAtIndexPath function.
You have a problem with your approach: didSelectRowAtIndexPath is supposed to look at the data in the model, not in the view. Your code is trying, incorrectly, to access the cell and examine its labels etc. Instead, your method should be accessing the same underlying data source that has been used to make the labels in the first place.
In other words, instead of writing
let label = cell.contentView.subviews[0] as! UILabel
and then examining the enabled/disabled status of the label
you should write
cursus = cursusses[indexPath.row] as Cursus
and examine the availability of the cursus.
One general rule of thumb is that you should get very suspicious when you see code accessing components of UITableViewCell outside cellForRowAtIndexPath. Testing the state of a label is nearly universally an indication that the code is incorrect.
Man you are doing that wrong. Only tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell can dequeue cells and setup its contents.
To prevent selection you should react on func tableView(_ tableView: UITableView,
willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath and return nil to indicate that you don't want to select anything.
Main problem is that you are writing to complex methods. It is hard to figure out what are you doing and what is you intention. (will/did)SelectRowAtIndexPath should invoke only one/tow some simple methods, for example: perform a segue or load some data.