How to remove row in UITableViewController presented as popover - ios

I have a custom UITableViewController that I present as a popover in my app. In some of the cells there is a delete button (trash can) to remove that item. Everything works as it should except that I the UI is not update when pressing the delete button. That is, the data is cleared and I call self.tableView.reloadData(), but the cell remains visible in the UI. (Pressing the delete button again makes the app crash in my C++ code because of an assert). I have no storyboard or xib as I do not need it. I only want this to be in code.
What am I missing? It might be something simple, but I cannot fathom why. I have tried:
Separate data source implementation.
Calling reloadData() both sync and async.
Setting delegate to self.
Various other hacks.
Here is the UITableViewController implementation:
import Foundation
class IngredientInfoPopoverViewController : UITableViewController
{
var slViewController: ShoppingListViewController?;
var ingredientName: String = "Ingrediens";
#IBOutlet var uniqueIngredients: [Ingredient] = []; // Unique per *recipe* so that we can list all the recipes for the ingredients
var clickedCellIndexPath: IndexPath? = nil;
enum SECTIONS : Int
{
case HEADER = 0;
case RECIPE = 1;
}
static let ROW_HEIGHT = 44;
override func viewDidLoad()
{
super.viewDidLoad();
tableView.register(UINib(nibName: "OpenIngredientInfoCell", bundle: nil), forCellReuseIdentifier: "OpenIngredientInfoCell");
tableView.register(UINib(nibName: "OpenRecipeCell", bundle: nil), forCellReuseIdentifier: "OpenRecipeCell");
tableView.separatorStyle = .singleLine;
tableView.bounces = false; // "Static" table view
updateSize();
}
func updateSize()
{
let totalCount = min(uniqueIngredients.count + 1, 6); // + 1: header row. min: Allow max 5 recipes in list (enables scrolling)
self.preferredContentSize = CGSize(width: 300, height: totalCount * IngredientInfoPopoverViewController.ROW_HEIGHT);
}
func setup(slvc: ShoppingListViewController?, ingredients: [Ingredient], clickedCellIndexPath: IndexPath)
{
self.slViewController = slvc;
self.clickedCellIndexPath = clickedCellIndexPath;
if (ingredients.count > 0)
{
let first = ingredients[0];
for i in ingredients
{
assert(i.getId() == first.getId());
}
ingredientName = first.getName();
var uniqueRecipeNames: Set<String> = [];
for i in ingredients
{
uniqueRecipeNames.insert(i.getRecipeName());
}
let sorted = uniqueRecipeNames.sorted();
uniqueIngredients.removeAll();
for s in sorted
{
for i in ingredients
{
if (i.getRecipeName() == s)
{
uniqueIngredients.append(i);
break;
}
}
}
}
}
override func numberOfSections(in tableView: UITableView) -> Int
{
return 2;
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int
{
switch section
{
case SECTIONS.HEADER.rawValue:
return 1;
case SECTIONS.RECIPE.rawValue:
return uniqueIngredients.count;
default:
assert(false);
return 0;
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
switch (indexPath.section)
{
case SECTIONS.HEADER.rawValue:
assert(indexPath.row == 0);
if (uniqueIngredients.count > 0)
{
let ingredient = uniqueIngredients[0]; // All are the same ingredient
self.dismiss(animated: true, completion: nil);
slViewController?.onIngredientInfoButtonClicked(ingredient);
}
break;
case SECTIONS.RECIPE.rawValue:
if (indexPath.row < uniqueIngredients.count)
{
let ingredient = uniqueIngredients[indexPath.row];
self.dismiss(animated: true, completion: nil);
slViewController?.onRecipeInfoButtonClicked(ingredient);
}
break;
default:
break;
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
{
let cell = UITableViewCell();
switch (indexPath.section)
{
case SECTIONS.HEADER.rawValue:
if (uniqueIngredients.count > 0)
{
let ingredient = uniqueIngredients[0];
let cell = tableView.dequeueReusableCell(withIdentifier: "OpenIngredientInfoCell", for: indexPath) as! OpenIngredientInfoCell;
cell.setup(ingredient);
}
break;
case SECTIONS.RECIPE.rawValue:
if (indexPath.row < uniqueIngredients.count)
{
cell.selectionStyle = .none; // Without this the cell contents become gray and disappear when long pressing! FML
let ingredient = uniqueIngredients[indexPath.row];
let cell = tableView.dequeueReusableCell(withIdentifier: "OpenRecipeCell", for: indexPath) as! OpenRecipeCell;
cell.setup(self, ingredient, clickedCellIndexPath);
}
break;
default:
break;
}
return cell;
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
{
return CGFloat(IngredientInfoPopoverViewController.ROW_HEIGHT);
}
func ingredientRemoved(_ ingredient: Ingredient)
{
for i in 0..<uniqueIngredients.count
{
if (uniqueIngredients[i].getRecipeId() == ingredient.getRecipeId())
{
uniqueIngredients.remove(at: i);
// let indexPath = IndexPath(row: i, section: SECTIONS.RECIPE.rawValue);
// self.tableView.deleteRows(at: [indexPath], with: .fade);
DispatchQueue.main.async {
self.tableView.reloadData();
}
break;
}
}
if (uniqueIngredients.count == 0)
{
self.dismiss(animated: true, completion: nil);
}
else
{
DispatchQueue.main.async {
self.tableView.reloadData();
}
}
}
}
Here is how I present the IngredientInfoPopoverViewController:
#objc func ingredientInfoClicked(_ sender: UITapGestureRecognizer)
{
let tapLocation = sender.location(in: self.tableView)
let indexPath = self.tableView.indexPathForRow(at: tapLocation)!
let ingredients = CppInterface.shoppingList.getIngredients(UInt(indexPath.section), position: UInt(indexPath.row));
let controller = IngredientInfoPopoverViewController();
controller.setup(slvc: self, ingredients: ingredients!, clickedCellIndexPath: indexPath);
controller.modalPresentationStyle = .popover;
controller.popoverPresentationController!.delegate = self;
self.present(controller, animated: true, completion: {
self.tableView.reloadData();
});
}
Here is how the view controller looks when presented. If I click the trash can on one of the items, the data is cleared, but the cell is not removed from the UI, which is what I am trying to achieve.

I'm actually surprised your tableView shows any data at all. Because you declare cell as a let in cellForRowAt when you do let cell = UITableViewCell();, that makes it immutable, and the first cell (outside of the switch) is the one that should technically get returned. Hence why no data should be displaying. And probably also the reason why your tableView is not updating correctly.
Anyway, you should only declare cell when you're dequeueing it, and you should as much as possible, avoid force-unwrapping of a variable.
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == SECTIONS.HEADER.rawValue, let cell = tableView.dequeueReusableCell(withIdentifier: "OpenIngredientInfoCell", for: indexPath) as? OpenIngredientInfoCell {
// not sure this check is necessary, but I'm adding it because it was in your original code
guard uniqueIngredients.count > 0 else { return UITableViewCell() }
let ingredient = uniqueIngredients[0]
cell.setup(ingredient)
return cell
} else if indexPath.section == SECTIONS.RECIPE.rawValue, let cell = tableView.dequeueReusableCell(withIdentifier: "OpenRecipeCell", for: indexPath) as? OpenRecipeCell {
// it shouldn't be possible for the indexPath to ever be greater than the dataSource items count, but I'll keep the check
guard indexPath.row < uniqueIngredients.count else { return UITableViewCell() }
cell.selectionStyle = .none
let ingredient = uniqueIngredients[indexPath.row]
cell.setup(self, ingredient, clickedCellIndexPath)
return cell
}
return UITableViewCell()
}
I've removed the semi-colons as they're not necessary in Swift.
For specifying the table cells' reuse identifiers, using the class names would probably be better. So you would use "\(OpenRecipeCell.self)" instead of "OpenRecipeCell"

If you are using the defaulted way of editing a UITableView (either swiping or entering edit mode), then here's my delegate code that works fine:
func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let movedStep = appState.recipe.steps[sourceIndexPath.row]
appState.recipe.steps.remove(at: sourceIndexPath.row)
appState.recipe.steps.insert(movedStep, at: destinationIndexPath.row)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
appState.recipe.steps.remove(at: indexPath.row)
tblSteps.deleteRows(at: [indexPath], with: .automatic)
}
}
Notes:
I manually place this table view in edit mode through a UIBarButtonItem, and a cell can both be moved or deleted.
My data source is in my model, at appState.recipe.steps. The structure doesn't matter, just handling the array.
I set a Notification anytime this array is changed that triggers a reloadData() in this table view.
I don't see either of these delegate methods listed, so I'm posting this answer. If by chance it doesn't help you, I'll gladly delete this.

Related

Swift TableView insert row below button clicked

I am new to Swift and I am using Swift 4.2 . I have a TableView with a label and button . When I press a button I would like to add a new row directly below the row in which the button was clicked . Right now when I click a button the new row gets added to the bottom of the TableView every time. I have been looking at posts on here but haven't been able to get it working this is my code base . I have a method called RowClick I get the indexpath of the row that was clicked but do not know how to use that to get the new row to appear directly below the clicked row .
class ExpandController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var TableSource: UITableView!
var videos: [String] = ["FaceBook","Twitter","Instagram"]
override func viewDidLoad() {
super.viewDidLoad()
TableSource.delegate = self
TableSource.dataSource = self
TableSource.tableFooterView = UIView(frame: CGRect.zero)
// Do any additional setup after loading the view.
}
#IBAction func RowClick(_ sender: UIButton) {
guard let cell = sender.superview?.superview as? ExpandTVC else {
return
}
let indexPath = TableSource.indexPath(for: cell)
InsertVideoTitle(indexPath: indexPath)
}
func InsertVideoTitle(indexPath: IndexPath?)
{
videos.append("Snapchat")
let indexPath = IndexPath(row: videos.count - 1, section: 0)
TableSource.beginUpdates()
TableSource.insertRows(at: [indexPath], with: .automatic)
TableSource.endUpdates()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return videos.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let videoTitle = videos[indexPath.row]
let cell = TableSource.dequeueReusableCell(withIdentifier: "ExpandTVC") as! ExpandTVC
cell.Title.text = videoTitle
cell.ButtonRow.tag = indexPath.row
cell.ButtonRow.setTitle("Rows",for: .normal)
return cell
}
}
This is how my table looks I clicked the Facebook Rows button and it appended the string SnapChat . The Snapchat label should appear in a row below Facebook instead . Any suggestions would be great !
I think the easiest solution without re-writing this whole thing would be adding 1 to the current row of the IndexPath you captured from the action.
let indexPath = TableSource.indexPath(for: cell)
var newIndexPath = indexPath;
newIndexPath.row += 1;
InsertVideoTitle(indexPath: newIndexPath);
I did this from memory because I am not near an IDE, so take a look at the change and apply that change if needed in any other location.
class ExpandController: UIViewController,UITableViewDelegate,UITableViewDataSource {
#IBOutlet weak var TableSource: UITableView!
var videos: [String] = ["FaceBook","Twitter","Instagram"]
override func viewDidLoad() {
super.viewDidLoad()
TableSource.delegate = self
TableSource.dataSource = self
TableSource.tableFooterView = UIView(frame: CGRect.zero)
// Do any additional setup after loading the view.
}
#IBAction func RowClick(_ sender: UIButton) {
guard let cell = sender.superview?.superview as? ExpandTVC else {
return
}
let indexPath = TableSource.indexPath(for: cell)
var newIndexPath = indexPath;
newIndexPath.row += 1;
InsertVideoTitle(indexPath: newIndexPath);
}
func InsertVideoTitle(indexPath: IndexPath?)
{
videos.append("Snapchat")
let indexPath = IndexPath(row: videos.count - 1, section: 0)
TableSource.beginUpdates()
TableSource.insertRows(at: [indexPath], with: .automatic)
TableSource.endUpdates()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return videos.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let videoTitle = videos[indexPath.row]
let cell = TableSource.dequeueReusableCell(withIdentifier: "ExpandTVC") as! ExpandTVC
cell.Title.text = videoTitle
cell.ButtonRow.tag = indexPath.row
cell.ButtonRow.setTitle("Rows",for: .normal)
return cell
}
}
Your current code calls append to add the new item at the end of the array. What you want to do is insert a new row at indexPath.row+1. Array has an insert(element,at:) function.
You have to handle the case where the user has tapped the last row and not add 1 to avoid an array bounds error:
func InsertVideoTitle(indexPath: IndexPath)
{
let targetRow = indexPath.row < videos.endIndex ? indexPath.row+1 : indexPath.row
videos.insert("Snapchat" at:targetRow)
let newIndexPath = IndexPath(row: targetRow, section: 0)
TableSource.beginUpdates()
TableSource.insertRows(at: [newIndexPath], with: .automatic)
TableSource.endUpdates()
}

How can i create a hamburger menu with Sub Menu inside

I am working on an app in which requirement is to create a hamburger menu with submenu in it like
I tried different method using table inside table view cell etc but unable to create this menu.
if someone has a solution then recommend me
You can create such an item structure as your data source
struct Item {
let text: String
var subItems: [String]?
var isExpanded = false
init(_ text: String, items: [String]? = nil) {
self.text = text
self.subItems = items
}
}
Usage
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet var tableView: UITableView!
private let imgOpen = UIImage(named: "open")
private let imgClose = UIImage(named: "close")
private var dataSource = [Item]()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "groupCell")
self.tableView.dataSource = self
self.tableView.delegate = self
self.dataSource.append(Item("HOME"))
self.dataSource.append(Item("ABOUT US"))
self.dataSource.append(Item("OUR PROJECTS", items: ["Project-1", "Project-2", "..."]))
self.dataSource.append(Item("BAHRIA TOWN PHASE 1 - 7"))
self.dataSource.append(Item("BAHRIA TOWN PHASE 8"))
self.tableView.reloadData()
}
func numberOfSections(in tableView: UITableView) -> Int {
return self.dataSource.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let item = self.dataSource[section]
if item.isExpanded, let count = item.subItems?.count {
return count + 1
}
return 1
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let item = self.dataSource[indexPath.section]
let cell = tableView.dequeueReusableCell(withIdentifier: "groupCell", for: indexPath)
var imageView: UIImageView?
if indexPath.row > 0, let text = item.subItems?[indexPath.row - 1] {
cell.textLabel?.text = text
} else {
cell.textLabel?.text = item.text
if item.subItems != nil {
imageView = UIImageView(image: item.isExpanded ? self.imgClose : self.imgOpen)
}
}
cell.accessoryView = imageView
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = self.dataSource[indexPath.section]
if indexPath.row == 0 && item.subItems != nil {
self.dataSource[indexPath.section].isExpanded = !item.isExpanded
let indexSet = IndexSet(integer: indexPath.section)
tableView.reloadSections(indexSet, with: .automatic)
} else {
// non-expandable menu item tapped
}
}
}
You should separate the process.
First, create the hamburger menu: for this, I recommend using this 3rd party library: https://github.com/John-Lluch/SWRevealViewController
With the help of this, it is very easy to create a side out menu from the left side of the screen.
The best thing that you will get a ViewController which will responsible for the menu, so you can easily customize it.
Second, as mentioned below, you should use a tableView with expandable cells. The best way to do this is to basically just show the headers of the cells. If the user taps on a header, then show the actual cell. (rowheight > 0). There is a tutorial about this: https://www.youtube.com/watch?v=bSKUYRsMCrM
-> you can create a sliding drawer menu(hamburger menu) using any of the following libraries:
1) REFrostedViewController
2) SWRevealViewController or any other
-> Sub Menu: In the drawer view controller, you have to add a table view and implement expandable/collapsible sections to display a submenu. You can follow any tutorial explaining about expand-collapse table view sections. Some of the tutorial links are below:
https://github.com/jeantimex/ios-swift-collapsible-table-section
https://medium.com/#legonaftik/uitableview-with-collapsible-sections-927d726b985c
1st Follow https://github.com/jonkykong/SideMenu.
And then to make EXPANDABLE Cells:-
You just need to create 2 Cells in UITableView(In Storyboard). First cell for those who are not expandable and Second cell for the expandable.
class SideMenuTableViewController: UITableViewController {
// MARK:- Constants And Vars
var isOurProjectCellExpanded = false
}
class SideMenuTableViewController: UITableViewDataSource, UITableViewDelegate {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "simpleCell", for: indexPath) as! SideMenuBasicTableViewCell
switch indexPath.row {
case 0:
cell.itemName.text = "HOME"
break
case 1:
cell.itemName.text = "About Us"
break
case 2:
if(isOurProjectCellExpanded){
//expandedCell
let cell = tableView.dequeueReusableCell(withIdentifier: "expandedCell", for: indexPath) as! SideMenuBasicTableViewCell
cell.itemName.text = "Our Projects"
return cell
}else{
cell.arrowDownImageView.isHidden = false
cell.itemName.text = "Our Projects"
}
break
case 3:
cell.itemName.text = "Bahria Town phase 1-7"
break
case 4:
cell.itemName.text = "Bahria Town phase 8"
break
default:
break
}
return cell
}
//And in `DidSelectRow` Method
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if(indexPath.row == 2){
if(isOurProjectCellExpanded){
isOurProjectCellExpanded = false
tableView.reloadRows(at: [indexPath], with: .none)
}else{
isOurProjectCellExpanded = true
tableView.reloadRows(at: [indexPath], with: .none)
}
}else if(indexPath.row == 0){
// Handle it yourself
}else if(indexPath.row == 1){
// Handle it yourself
}else if(indexPath.row == 3){
// Handle it yourself
}else if(indexPath.row == 4){
// Handle it yourself
}
}
}

Single table view through two different NSFetchedResultsControllers with sections

Good morning to everyone. I am using Swift 3.1.1, Xcode 8.3.2. I need to connect a single Table View to two different tables (entities) in Core Data through two different NSFetchedResultsControllers. I have created two NSFetchedResultsControllers, and even fetched data from table but I faced problem how to tell Table View that first controller should response for section one and second controller should be responsible for section two.
I can show you the code:
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var tv: UITableView!
#IBAction func pressed(_ sender: UIButton) {
ModelA.read { table1 in
ModelB.read { table2 in
if table1.isEmpty {
ModelA.save(recordToSave: [(a: 1, b: "a"), (a: 2, b: "b"), (a: 3, b: "c")]) {
ModelB.save(recordToSave: [(a: 4, b: 5.0, c: true), (a: 6, b: 7.0, c: false)]) {
self.tvReload()
}
}
} else {
self.tvReload()
}
}
}
}
var fetchedResultsControllerForModelA = CoreDataFunctions.fetchedResultsController
var fetchedResultsControllerForModelB = CoreDataFunctions.fetchedResultsController
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
tvReload()
}
func modelOfTableA(indexPath: IndexPath) -> (forLabel1: String, forLabel2: String, forLabel3: String)? {
if let fetchedResultsControllerForModelA = CoreDataFunctions.getNSManagedObjectForIndexPathOfTable(fetchedResultsController: fetchedResultsControllerForModelA, indexPath: indexPath) {
if let model = ModelA.read(nsmanagedobject: fetchedResultsControllerForModelA) {
return (forLabel1: "\(model.a)", forLabel2: model.b, forLabel3: "")
}
}
return nil
}
func modelOfTableB(indexPath: IndexPath) -> (forLabel1: String, forLabel2: String, forLabel3: String)? {
if let fetchedResultsControllerForModelB = CoreDataFunctions.getNSManagedObjectForIndexPathOfTable(fetchedResultsController: fetchedResultsControllerForModelB, indexPath: indexPath) {
if let model = ModelB.read(nsmanagedobject: fetchedResultsControllerForModelB) {
return (forLabel1: "\(model.a)", forLabel2: "\(model.b)", forLabel3: "\(model.c)")
}
}
return nil
}
func tvReload() {
fetchedResultsControllerForModelA = CoreDataFunctions(tableName: .a).fetchedResultsController(keyForSort: ModelA.a.rawValue, searchParameters: nil)
fetchedResultsControllerForModelB = CoreDataFunctions(tableName: .b).fetchedResultsController(keyForSort: ModelB.a.rawValue, searchParameters: nil)
do {
try fetchedResultsControllerForModelA?.performFetch()
try fetchedResultsControllerForModelB?.performFetch()
DispatchQueue.main.async {
self.tv.reloadData()
}
} catch {
print("Error")
}
}
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let sections1 = fetchedResultsControllerForModelA?.sections {
if let sections2 = fetchedResultsControllerForModelB?.sections {
return sections1[section].numberOfObjects + sections2[section].numberOfObjects
}
return sections1[section].numberOfObjects
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
if indexPath.section == 0 {
if let modelOfTable = modelOfTableA(indexPath: indexPath) {
cell.l1.text = modelOfTable.forLabel1
cell.l2.text = modelOfTable.forLabel2
cell.l3.text = modelOfTable.forLabel3
}
} else {
if let modelOfTable = modelOfTableB(indexPath: indexPath) {
cell.l1.text = modelOfTable.forLabel1
cell.l2.text = modelOfTable.forLabel2
cell.l3.text = modelOfTable.forLabel3
}
}
return cell
}
}
I could not find any tutorial on this theme, so I am asking question there. I do not want to use inheritance from single entity in Core Data, because, in real life it would be impossible.
Thank you for any help or advice!
OK - I downloaded your code, and there are a couple issues...
1) If you want your data to fill two sections in the table, the table must have two sections - currently, you are just returning 1, so use this (although you may want/need different handling based on data retrieved):
func numberOfSectionsInTableView(_ tableView: UITableView) -> Int {
if fetchedResultsControllerForModelA == nil || fetchedResultsControllerForModelB == nil {
return 0
}
return 2
}
2) For number of rows in each section, your code was close but not quite right...
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if let sections = fetchedResultsControllerForModelA?.sections {
return sections[0].numberOfObjects
}
} else {
if let sections = fetchedResultsControllerForModelB?.sections {
return sections[0].numberOfObjects
}
}
return 0
}
3) For the actual cellForRowAtIndexPath data, again your code was close but you need to keep track of which data to get for each section...
func tableView(_ tableView: UITableView, cellForRowAtIndexPath indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
if indexPath.section == 0 {
if let modelOfTable = modelOfTableA(indexPath: indexPath) {
cell.l1.text = modelOfTable.forLabel1
cell.l2.text = modelOfTable.forLabel2
cell.l3.text = modelOfTable.forLabel3
}
} else {
// Each "Table data model" has only one section... since this is the 2nd table section, we need
// to change the section of the index path for our call to the data model
let dataIndexPath = IndexPath(row: indexPath.row, section: 0)
if let modelOfTable = modelOfTableB(indexPath: dataIndexPath) {
cell.l1.text = modelOfTable.forLabel1
cell.l2.text = modelOfTable.forLabel2
cell.l3.text = modelOfTable.forLabel3
}
}
return cell
}
That should get you on your way.

number of rows contained in existing section after update (15) must equal

So i'm building an app as a hobby and have researched, it appears a few people have a similar problem, except mine happens when inserting the data to begin with. So I think it's slightly different.
When I go to insert data into my array and table it returns an error (title), it retrieves the right amount of current count, but struggled to add a new one.
class AccountsViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var totalLabel: UILabel!
#IBOutlet weak var tableview: UITableView!
#IBOutlet weak var tableview2: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
//Set the table background as the image
tableview.backgroundView = UIImageView(image: UIImage(named: "splasnowords-1.png"))
//Use the edit button item provided by the table view controller
navigationItem.leftBarButtonItem = editButtonItem
//self.navigationItem.leftBarButtonItem = self.editButtonItem;
//Calculate the latest totalstandings
BudgetDataModel.calculateTotalStandings()
totalLabel.text = ("Total Current Standings = £\(BudgetDataModel.returnTrueValue(number: BudgetDataModel.total))")
self.tableview.delegate = self
self.tableview2.delegate = self
self.tableview.dataSource = self
self.tableview2.dataSource = self
}
// MARK: - Table view data source
func numberOfSections(in tableView: UITableView) -> Int {
if (tableView == tableview){
return 1
//BudgetDataModel.budgets.count
}
else{
return 2
//SavingsDataModel.savings.count
}
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?{
//reload data?
if (tableView == tableview){
return "Budgets"
}
else{
return "Savings"
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
var rowCount = 0
if (tableView == self.tableview) {
rowCount = BudgetDataModel.budgets.count
}
if (tableView == self.tableview2) {
rowCount = SavingsDataModel.savings.count
}
return rowCount
// #warning Incomplete implementation, return the number of rows
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//Table view cells are reused and should be dequeued using a cell identifier.
if (tableView == self.tableview){
let cellIdentifier = "AccountsTableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! AccountsTableViewCell
let budget = BudgetDataModel.budgets[(indexPath as NSIndexPath).row]
cell.nameLabel.text = budget.name
cell.amountLabel.text = ("£\(BudgetDataModel.returnTrueValue(number: budget.amount))")
cell.backgroundColor = UIColor(white: 1, alpha: 0.5)
return cell
}
else if (tableView == self.tableview2){
let cellIdentifier = "SavingsTableViewCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! SavingsTableViewCell
let saving = SavingsDataModel.savings[(indexPath as NSIndexPath).row]
cell.savingsnameLabel.text = saving.savingname
cell.savingsamountLabel.text = ("£\(BudgetDataModel.returnTrueValue(number: saving.savingamount))")
cell.backgroundColor = UIColor(white: 1, alpha: 0.5)
return cell
}
else { preconditionFailure ("unexpected cell type") }
}
// Override to support conditional editing of the table view.
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
// Override to support editing the table view.
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
if (tableView == tableview){
// Delete the row from the data source
BudgetDataModel.budgets.remove(at: indexPath.row)
BudgetDataModel.saveBudgets()
BudgetDataModel.calculateTotalStandings()
totalLabel.text = ("Total Current Standings = £\(BudgetDataModel.returnTrueValue(number:BudgetDataModel.total))")
// self.tableview.reloadData()
tableView.deleteRows(at: [indexPath], with: .fade)
}
else if (tableView == tableview2){
// Delete the row from the data source
SavingsDataModel.savings.remove(at: indexPath.row)
SavingsDataModel.saveSavings()
//implement BudgetDataModel.calculateTotalStandings()
//implement totalLabel.text = ("Total Current Standings = £\(BudgetDataModel.returnTrueValue(number:BudgetDataModel.total))")
//self.tableview2.reloadData()
tableView.deleteRows(at: [indexPath], with: .fade)
}
} else if editingStyle == .insert {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
}
}
// Override to support rearranging the table view.
func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
}
// MARK: - Navigation
// In a storyboard-based application, you will often want to do a little preparation before navigation
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "ShowDetail"{
let budgetDetailViewController = segue.destination as! BudgetViewController
//Get the cell that generated this segue.
if let selectedBudgetCell = sender as? AccountsTableViewCell {
let indexPath = tableview.indexPath(for: selectedBudgetCell)!
let selectedBudget = BudgetDataModel.budgets[indexPath.row]
budgetDetailViewController.budget = selectedBudget
}
}
else if segue.identifier == "AddItem"{
//self.tableview.reloadData()
print("Adding new budget.")
}
else if segue.identifier == "ShowSavings"{
let savingDetailViewController = segue.destination as! SavingsViewController
//Get the cell that generated this segue.
if let selectedSavingsCell = sender as? SavingsTableViewCell {
let indexPath = tableview2.indexPath(for: selectedSavingsCell)!
let selectedSavings = SavingsDataModel.savings[indexPath.row]
savingDetailViewController.saving = selectedSavings
}
}
else if segue.identifier == "AddSaving"{
//self.tableview2.reloadData()
print ("Adding new saving.")
}
}
//MARK: Actions
#IBAction func unwindToBudgetList(_ sender: UIStoryboardSegue){
if let sourceViewController = sender.source as? BudgetViewController, let budget = sourceViewController.budget {
if let selectedIndexPath = tableview.indexPathForSelectedRow{
//Update an existing budget.
BudgetDataModel.budgets[selectedIndexPath.row] = budget
tableview.reloadRows(at: [selectedIndexPath], with: .none)
}
else{
//Add a new budget
let newIndexPath = IndexPath(row:BudgetDataModel.budgets.count, section: 0)
BudgetDataModel.budgets.append(budget)
tableview.insertRows(at: [newIndexPath as IndexPath], with: .bottom)
}
//Save the budgets.
BudgetDataModel.saveBudgets()
BudgetDataModel.calculateTotalStandings()
totalLabel.text = ("Total Current Standings = £\(BudgetDataModel.returnTrueValue(number: BudgetDataModel.total))")
}
}
#IBAction func unwindtoSavingsList(_ sender: UIStoryboardSegue){
if let sourceViewController = sender.source as? SavingsViewController, let savings = sourceViewController.saving {
if let selectedIndexPath = tableview2.indexPathForSelectedRow{
//Update an existing budget.
SavingsDataModel.savings[selectedIndexPath.row] = savings
tableview2.reloadRows(at: [selectedIndexPath], with: .none)
}
else{
//Add a new saving
let newIndexPath = IndexPath(row:SavingsDataModel.savings.count, section: 1)
SavingsDataModel.savings.append(savings)
//tableview2.reloadData()
tableview2.insertRows(at: [newIndexPath as IndexPath], with: .bottom)
}
//Save the budgets.
SavingsDataModel.saveSavings()
//implement SavingsDataModel.calculateTotalStandings()
// totalLabel.text = ("Total Current Standings = £\(BudgetDataModel.returnTrueValue(number: BudgetDataModel.total))")
}
}
}
Thanks #jcaron
Two corrections required:
Change my numberofsectionscode to return 1, so that my second table didn't have a randomly replicated second section
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
Replace the section with 0, when adding a new saving
//Add a new saving
let newIndexPath = IndexPath(row:SavingsDataModel.savings.count, section: 1)

Reload tableView data from UIAlertController not working

I'm having trouble refreshing tableView data from a UIAlertController.
The code is for a quiz-style app and this page lets the user choose
subjects as well as some other options (see screenshots). There is a
reset button next to "Show only unseen questions" which triggers a
UIAlertController. However, clicking the Reset action in this alert
updates the database but doesn't update the tableView. The database is
definitely updated as if I go back a page and then revisit the
tableView, the unseen question values in the subject cells are updated. I realise there's quite a few of
these type of questions here but I'm afraid none of the usual fixes are
working.
Extra info:
The tableView is customised with a series of custom
UITableViewCells
Data is loaded from a SQLite database through FMDB
The UIAlertController is triggered from a NSNotification when the reset
button is clicked
So far I have:
Checked datasource and delegates set correctly, programmatically and
in IB. Confirmed with print(self.tableView.datasource) etc
Confirmed reloadData() is firing
Using main thread for reloadData()
Extract of TableViewController code and screenshots below.
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.dataSource = self
self.tableView.delegate = self
//For unique question picker changed
NotificationCenter.default.addObserver(self, selector: #selector(SubjectsTableViewController.reloadView(_:)), name:NSNotification.Name(rawValue: "reload"), object: nil)
//For slider value changed
NotificationCenter.default.addObserver(self, selector: #selector(SubjectsTableViewController.updateQuantity(_:)), name:NSNotification.Name(rawValue: "updateQuantity"), object: nil)
//Trigger UIAlertController
NotificationCenter.default.addObserver(self, selector: #selector(SubjectsTableViewController.showAlert(_:)), name:NSNotification.Name(rawValue: "showAlert"), object: nil)
}
// MARK: - Table view data source
///////// Sections and Headers /////////
override func numberOfSections(in tableView: UITableView) -> Int {
return 3
}
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
{
let subjectHeaderCell = tableView.dequeueReusableCell(withIdentifier: "sectionHeader")
switch section {
case 0:
subjectHeaderCell?.textLabel?.text = "Select Subjects"
return subjectHeaderCell
case 1:
subjectHeaderCell?.textLabel?.text = "Options"
return subjectHeaderCell
case 2:
subjectHeaderCell?.textLabel?.text = ""
return subjectHeaderCell
default:
subjectHeaderCell?.textLabel?.text = ""
return subjectHeaderCell
}
}
//Header heights
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
{
return 34.0
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
return SubjectManager.subjectWorker.countSubjects()
case 1:
return 2
case 2:
return 1
default:
return 0
}
}
///////// Rows within sections /////////
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
switch (indexPath.section) {
case 0:
//Configure subjectCell //
let cellWithSubject = tableView.dequeueReusableCell(withIdentifier: "subjectCell", for: indexPath) as! SubjectTableViewCell
//Curve corners
cellWithSubject.subjectCellContainer.layer.cornerRadius = 2
cellWithSubject.subjectCellContainer.layer.masksToBounds = true
//Set subject title label
cellWithSubject.subjectTitleLabel.text = SubjectManager.subjectWorker.collateSubjectTitles()[indexPath.row]
//Available questions for subject label
questionCountForSubjectArray = QuestionManager.questionWorker.countQuestions()
cellWithSubject.subjectAvailableQuestionsLabel.text = "Total questions available: \(questionCountForSubjectArray[indexPath.row])"
//Get questions in subject variables
seenQuestionsForSubjectArray = QuestionManager.questionWorker.countOfQuestionsAlreadySeen()
//New questions available label
unseenQuestionsForSubjectArray.append(questionCountForSubjectArray[indexPath.row] - seenQuestionsForSubjectArray[indexPath.row])
cellWithSubject.newQuestionsRemainingLabel.text = "New questions remaining: \(unseenQuestionsForSubjectArray[indexPath.row])"
return cellWithSubject
case 1:
switch (indexPath.row) {
case 0:
//Configure uniqueQuestionCell //
let cellWithSwitch = tableView.dequeueReusableCell(withIdentifier: "uniqueQuestionCell", for: indexPath) as! UniqueQuestionTableViewCell
//Curve corners
cellWithSwitch.uniqueQuestionContainer.layer.cornerRadius = 2
cellWithSwitch.uniqueQuestionContainer.layer.masksToBounds = true
return cellWithSwitch
case 1:
//Configure sliderCell //
let cellWithSlider = tableView.dequeueReusableCell(withIdentifier: "questionPickerCell", for: indexPath) as! QuestionPickerTableViewCell
//Curve corners
cellWithSlider.pickerCellContainer.layer.cornerRadius = 2
cellWithSlider.pickerCellContainer.layer.masksToBounds = true
//Set questions available label
cellWithSlider.questionsAvailableLabel.text = "Available: \(sumQuestionsSelected)"
//Configure slider
cellWithSlider.questionPicker.maximumValue = Float(sumQuestionsSelected)
cellWithSlider.questionPicker.isContinuous = true
//Logic for if available questions changes - updates slider stuff
if questionQuantityFromSlider > sumQuestionsSelected {
questionQuantityFromSlider = sumQuestionsSelected
cellWithSlider.questionsToStudy = questionQuantityFromSlider
cellWithSlider.questionsChosenLabel.text = "Questions to study: \(questionQuantityFromSlider)"
} else { questionQuantityFromSlider = cellWithSlider.questionsToStudy
}
//Configure questions chosen label:
if questionsToStudyDict.isEmpty {
cellWithSlider.chooseSubjectsLabel.text = "Choose a subject"
cellWithSlider.questionsChosenLabel.text = "Questions to study: 0"
} else {
cellWithSlider.chooseSubjectsLabel.text = ""
}
return cellWithSlider
default:
return UITableViewCell()
}
case 2:
print("cellForRowAt case 2")
//Configure beginCell //
let cellWithStart = tableView.dequeueReusableCell(withIdentifier: "beginCell", for: indexPath) as! BeginStudyTableViewCell
//Curve corners
cellWithStart.startContainer.layer.cornerRadius = 2
cellWithStart.startContainer.layer.masksToBounds = true
return cellWithStart
default:
return UITableViewCell()
}
}
//Row heights
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
switch (indexPath.section) {
case 0:
return 120.0
case 1:
switch (indexPath.row) {
case 0:
return 60.0
case 1:
return 100.0
default:
return 44.0
}
case 2:
return 100.0
default:
return 44.0
}
}
override func tableView(_ tableView: UITableView, shouldHighlightRowAt indexPath: IndexPath) -> Bool {
if indexPath.section == 2 || indexPath.section == 0 {
return true
} else {
return false
}
}
override func tableView(_ tableView: UITableView, didHighlightRowAt indexPath: IndexPath) {
if indexPath.section == 2 && selectedRowsDict.isEmpty != true && questionQuantityFromSlider > 0 {
let cellToBegin = tableView.cellForRow(at: indexPath) as! BeginStudyTableViewCell
cellToBegin.startContainer.backgroundColor = UIColor.lightGray
}
}
override func tableView(_ tableView: UITableView, didUnhighlightRowAt indexPath: IndexPath) {
if indexPath.section == 2 {
let cellToBegin = tableView.cellForRow(at: indexPath) as! BeginStudyTableViewCell
cellToBegin.startContainer.backgroundColor = UIColor.white
}
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch (indexPath.section) {
case 0:
//Set checkbox to ticked image
let cellWithSubject = tableView.cellForRow(at: indexPath) as! SubjectTableViewCell
cellWithSubject.subjectSelectedImageView.image = UIImage(named: "CheckboxTicked")
//Determine questions available for subject depending on unseen value
if showUnseenQuestions == true {
questionsToStudyDict[indexPath.row] = unseenQuestionsForSubjectArray[indexPath.row]
} else {
questionsToStudyDict[indexPath.row] = questionCountForSubjectArray[indexPath.row]
}
//Sum questions available
sumQuestionsSelected = Array(questionsToStudyDict.values).reduce(0, +)
//Reload table to pass this to questions available label in UISlider cell and reselect selected rows
let key: Int = indexPath.row
selectedRowsDict[key] = indexPath.row
self.tableView.reloadData()
if selectedRowsDict.isEmpty == false {
for (keys,_) in selectedRowsDict {
let index: IndexPath = NSIndexPath(row: selectedRowsDict[keys]!, section: 0) as IndexPath
tableView.selectRow(at: index, animated: false, scrollPosition: .none)
}
}
case 1:
break
case 2:
if selectedRowsDict.isEmpty != true && questionQuantityFromSlider > 0 {
self.performSegue(withIdentifier: "showStudyQuestion", sender: self)
} else {
print("Segue not fired")
}
default:
break
}
}
override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
if indexPath.section == 0 {
//Set checkbox to unticked image
let cellWithSubject = tableView.cellForRow(at: indexPath) as! SubjectTableViewCell
cellWithSubject.subjectSelectedImageView.image = UIImage(named: "Checkbox")
//Remove questions available for unselected subject from questions dictionary
questionsToStudyDict[indexPath.row] = nil
//Update sum of questions selected
sumQuestionsSelected = Array(questionsToStudyDict.values).reduce(0, +)
//Reload table to pass this to questions available label in UISlider cell and reselect selected rows
let key: Int = indexPath.row
selectedRowsDict[key] = nil
self.tableView.reloadData()
if selectedRowsDict.isEmpty == false {
for (keys,_) in selectedRowsDict {
let index: IndexPath = NSIndexPath(row: selectedRowsDict[keys]!, section: 0) as IndexPath
tableView.selectRow(at: index, animated: false, scrollPosition: .none)
}
}
}
}
func reloadView(_ notification: Notification) {
//Change bool value
showUnseenQuestions = !showUnseenQuestions
//For keys in dict, update values according to showUnseenQuestion value
if showUnseenQuestions == true {
for (key,_) in questionsToStudyDict {
questionsToStudyDict[key] = unseenQuestionsForSubjectArray[key]
}
} else {
for (key,_) in questionsToStudyDict {
questionsToStudyDict[key] = questionCountForSubjectArray[key]
}
}
//Re-run sum dict function
sumQuestionsSelected = Array(questionsToStudyDict.values).reduce(0, +)
//Finally reload the view and reselect selected rows
let selectedRowsIndexes = tableView.indexPathsForSelectedRows
self.tableView.reloadData()
if selectedRowsIndexes != nil {
for i in (selectedRowsIndexes)! {
tableView.selectRow(at: i, animated: false, scrollPosition: .none)
}
}
}
func updateQuantity(_ notification: Notification) {
//Reload the view and reselect selected rows
let selectedRowsIndexes = tableView.indexPathsForSelectedRows
self.tableView.reloadData()
if selectedRowsIndexes != nil {
for i in (selectedRowsIndexes)! {
tableView.selectRow(at: i, animated: false, scrollPosition: .none)
}
}
}
func showAlert(_ notification: Notification) {
let alertController = UIAlertController(title: "Reset Seen Questions", message: "Are you sure you want to reset all questions to unseen?", preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { action in
// ...
}
alertController.addAction(cancelAction)
let OKAction = UIAlertAction(title: "Reset", style: .default, handler:{(action:UIAlertAction) -> Void in
QuestionManager.questionWorker.resetHasSeenValues()
self.reloadData()
print("reloadData fired")
})
alertController.addAction(OKAction)
self.present(alertController, animated: true, completion: nil)
}
func reloadData() {
DispatchQueue.main.async(execute: {
self.tableView.reloadData()
})
}
func countOfQuestionsAlreadySeen() -> [Int] {
var questionSeenYesArray: [Int] = []
if openWriteDatabase() {
let queryYes = "SELECT SUM(hasSeen) FROM UserData GROUP BY subjectID"
let querySeenYes: FMResultSet? = writeDatabase?.executeQuery(queryYes, withArgumentsIn: nil)
while (querySeenYes?.next())! {
if let questionSeenYes = (querySeenYes?.int(forColumnIndex: 0)) {
questionSeenYesArray.append(Int(questionSeenYes))
}
}
}
return questionSeenYesArray
}
func resetHasSeenValues() {
if openWriteDatabase() {
let resetHasSeenValues = "UPDATE UserData Set hasSeen = 0"
_ = writeDatabase?.executeUpdate(resetHasSeenValues, withArgumentsIn: nil)
}
}
Some more debugging revealed that the unseenQuestionsForSubjectArray wasn't being populated correctly in the cellForRowAt method. I fixed this, and this fixed the reloadData() issue. Thanks all for the help.
add self.reloadData() inside main queue. Outlet changes always should be held inside main thread.
let OKAction = UIAlertAction(title: "Reset", style: .default, handler:{(action:UIAlertAction) -> Void in
QuestionManager.questionWorker.resetHasSeenValues()
DispatchQueue.main.async {
self.tableView.reloadData()
}
print("reloadData fired")
})

Resources