Table View Delete from Firebase - ios

I've managed to add data to a table view and import that to the specific Firebase location. I now want to be able to delete this data from inside the application.
Currently, with this code the entry successfully deletes from within the app, but not Firebase and when the app is reopened the entry returns. Basically I would like to know what I've done wrong and what I can change to actually delete the entry from the real time database.
import UIKit
import Firebase
class WorkoutNotesViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate {
var addExer:[String] = []
var handle: DatabaseHandle?
var ref: DatabaseReference?
var keyArray: [String] = []
#IBOutlet weak var tableView: UITableView!
#IBOutlet weak var exerView: UITextField!
#IBAction func inputButton(_ sender: Any) {
if exerView.text != ""
{
ref?.child("users").child(Auth.auth().currentUser!.uid).child("notes").childByAutoId().setValue(exerView.text)
exerView.text = ""
}
else
if exerView.text == "" {
let alertController = UIAlertController(title: "Error", message: "Retry", preferredStyle: .alert)
let defaultAction = UIAlertAction(title: "OK", style: .cancel, handler: nil)
alertController.addAction(defaultAction)
present(alertController, animated: true, completion: nil)
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return addExer.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
cell.textLabel?.text = addExer[indexPath.row]
return cell
}
// Do any additional setup after loading the view.
override func viewDidLoad() {
super.viewDidLoad()
tableView.allowsMultipleSelectionDuringEditing = true
ref = Database.database().reference()
handle = ref?.child("users").child(Auth.auth().currentUser!.uid).child("notes").observe(.childAdded, with: { (snapshot) in
if let item = snapshot.value as? String
{
self.addExer.append(item)
self.tableView.reloadData()
}
})
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
print(indexPath.row)
if editingStyle == .delete {
GetAllKeys()
let when = DispatchTime.now() + 1
DispatchQueue.main.asyncAfter(deadline: when, execute: {
self.ref?.child("users").child(Auth.auth().currentUser!.uid).child("notes").child(self.keyArray[indexPath.row])
self.addExer.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
self.keyArray = []
})
}
}
func GetAllKeys() {
ref?.child("users").child(Auth.auth().currentUser!.uid).child("notes").observeSingleEvent(of: .value, with: { (snapshot) in
for child in snapshot.children {
let snap = child as! DataSnapshot
let key = snap.key
self.keyArray.append(key)
}
})
}
}

You have to embed removeValue()
self.ref?.child("users").child(Auth.auth().currentUser!.uid).child("notes").child(self.keyArray[indexPath.row]).removeValue()
Also it's better to use a completion to be 100% sure that the item is deleted from firebase
self.ref?.child("users").child(Auth.auth().currentUser!.uid).child("notes").child(self.keyArray[indexPath.row]).removeValueWithCompletionBlock({ (error, refer) in
if error != nil {
print(error)
} else {
print("Child Removed successfilly")
}

Related

Swift - can't reload Data from TableView after add button action

I'm new to Swift and need your help.
I created a TableViewController with a custom cell.
Also I created a "add" Button in navigation bar to add a new value to my tableview.
Saving the values in Core Data and fetch them in viewWillAppear.
When pressing the add button a UIAlertController shows up which i had customized like i needed. I added a cancel action and a ok action but when i press the ok button from the alert the new value don't shows up in my tableview. I have to switch to an other viewcontroller that the tableview shows it.
I added groupsTableView.reloadData()on different points in my code but cant get it to work.
Hope someone can help me out!
Code from MasterViewController:
import UIKit
import CoreData
class MasterViewController: UITableViewController {
var groups: [Groups] = []
#IBOutlet weak var groupsTableView: UITableView!
var groupsTextField: UITextField?
override func viewDidLoad() {
super.viewDidLoad()
groupsTableView.delegate = self
groupsTableView.dataSource = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func viewWillAppear(_ animated: Bool) {
// Core date initialization
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Groups> = Groups.fetchRequest()
do {
groups = try managedContext.fetch(fetchRequest)
groupsTableView.reloadData()
} catch {
// TODO: error handling
print("Could not fetch groups")
}
navigationItem.leftBarButtonItem = editButtonItem
let addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(insertNewObject))
navigationItem.rightBarButtonItem = addButton
}
// MARK: - add new Group
#objc func insertNewObject() {
let addButtonAlert = UIAlertController(title: "Neue Gruppe", message: "Füge eine neue Gruppe deiner Liste hinzu", preferredStyle: .alert)
addButtonAlert.addTextField { (UITextField) in
self.groupsTextField = UITextField
self.groupsTextField?.placeholder = "Name der Gruppe"
self.groupsTextField?.clearButtonMode = .whileEditing
}
let okAction = UIAlertAction(title: "Hinzufügen", style: .default, handler: addNewGroup)
let cancelAction = UIAlertAction(title: "Abbrechen", style: .cancel, handler: nil)
addButtonAlert.addAction(okAction)
addButtonAlert.addAction(cancelAction)
self.present(addButtonAlert, animated: true, completion: nil)
}
func addNewGroup(_:UIAlertAction) -> Void {
let group = Groups(groupId: UUID(), groupTitle: groupsTextField!.text ?? "")
do {
try group?.managedObjectContext?.save()
groupsTableView.reloadData()
} catch {
// TODO: error handling
print("Could not save group")
}
}
// MARK: - Segue
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
guard let destination = segue.destination as? DetailViewController,
let selectedRow = self.groupsTableView.indexPathForSelectedRow?.row else {
return
}
destination.group = groups[selectedRow]
destination.title = groups[selectedRow].groupTitle
}
// MARK: - delete Group
func deleteGroup(at indexPath: IndexPath) {
let group = groups[indexPath.row]
guard let managedContext = group.managedObjectContext else {
return
}
managedContext.delete(group)
do {
try managedContext.save()
groups.remove(at: indexPath.row)
groupsTableView.deleteRows(at: [indexPath], with: .automatic)
} catch {
//TODO: error handling
print("Could not delete Group")
groupsTableView.reloadRows(at: [indexPath], with: .automatic)
}
}
// MARK: - Table View
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return groups.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = groupsTableView.dequeueReusableCell(withIdentifier: "GroupsTableViewCell", for: indexPath) as! GroupsTableViewCell
let object = groups[indexPath.row]
cell.groupTitleLabel?.text = object.groupTitle
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
// Return false if you do not want the specified item to be editable.
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
deleteGroup(at: indexPath)
}
}
}
Add group item to your groups array and after that reload your tableview as shown below:-
func addNewGroup(_:UIAlertAction) -> Void {
let group = Groups(groupId: UUID(), groupTitle: groupsTextField!.text ?? "")
do {
try group?.managedObjectContext?.save()
self.groups.append(group)
groupsTableView.reloadData()
} catch {
// TODO: error handling
print("Could not save group")
}
}

How to save UILabel from custom cell so that every time I open the app it displays the latest change?

I am a beginner in Swift. I've exhausted all my trial and errors and need help!!
I am creating a scoreboard project using a UITableView with a Custom Cell that holds a UILabel and a UIButton. After a button press the UILabel increments by one to simulate a point for the player. I am having trouble saving the point in UILabel so that every time I open the app the point for that player remains. I've tried using UserDefaults, structs, and delegates but have't had any luck...I could be doing it wrong. I just need to know what the proper approach is for this.
Note: I am able to save the player name successfully from the UIAlertController so that when I open the app the names are still there unless I delete them, but haven't had any luck saving the points for each name itself, they still remain "0".
It should look like this when I close then open the app, but it only does this when the app is opened:
Scoreboard UITableView - Screenshot
Here's the ViewController code:
import UIKit
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var items = [String]()
#IBOutlet weak var listTableView: UITableView!
#IBAction func addItem(_ sender: AnyObject) {
alert()
}
override func viewDidLoad() {
super.viewDidLoad()
listTableView.dataSource = self
self.items = UserDefaults.standard.stringArray(forKey:"items") ?? [String]()
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! PointsCell
cell.textLabel?.text = items[indexPath.row]
return cell
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func saveData() {
UserDefaults.standard.set(items, forKey: "items")
}
func alert(){
let alert = UIAlertController(title: "Add Player", message: "", preferredStyle: .alert)
alert.addTextField{
(textfield) in
textfield.placeholder = " Enter Player Name "
}
let add = UIAlertAction(title: "Add", style: .default)
{
(action) in guard let textfield = alert.textFields?.first else {return}
if let newText = textfield.text
{
self.items.append(newText)
self.saveData()
let indexPath = IndexPath(row: self.items.count - 1, section: 0)
self.listTableView.insertRows(at: [indexPath], with: .automatic)
}
}
let cancel = UIAlertAction(title: "Cancel", style: .cancel) {
(alert) in
}
alert.addAction(add)
alert.addAction(cancel)
present(alert, animated: true, completion: nil)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
items.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .automatic)
saveData()
}
}
Here is my custom cell code called PointsCell:
import UIKit
class PointsCell: UITableViewCell {
var winScore = 0
#IBOutlet weak var scoreUILabel: UILabel!
#IBAction func pointButtonPressed(_ sender: Any) {
winScore += 1
scoreUILabel.text = "\(winScore)"
}
}
The points still remains 0 because you are note saving them, firstly you need points for each player so you need to combine player name and score into object.
class Player:NSObject, Codable{
let name: String
var score : Int
init(name: String, score: Int) {
self.name = name
self.score = score
}
override var description: String{
return "Name :" + self.name + " Score :" + String(self.score )
}
}
Swift 4 introduced the Codable protocol, by adopting Codable on your own types enables you to serialize them to and from any of the built-in data formats.
Now you can easily access a player with name and score.
var players = [Player]()
To get stored value from UserDefaults
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if let items: Data = UserDefaults.standard.object(forKey: "items") as? Data{
self.players = try! PropertyListDecoder().decode([Player].self, from: items)
self.listTableView.reloadData()
}
}
When you adding new player to your list create new instance of Player and add it to list.
func alert(){
let alert = UIAlertController(title: "Add Player", message: "", preferredStyle: .alert)
alert.addTextField{
(textfield) in
textfield.placeholder = " Enter Player Name "
}
let add = UIAlertAction(title: "Add", style: .default)
{
(action) in guard let textfield = alert.textFields?.first else {return}
if let newText = textfield.text
{
let player = Player(name: newText, score: 0)
self.players.append(player)
let indexPath = IndexPath(row: self.players.count - 1, section: 0)
self.listTableView.insertRows(at: [indexPath], with: .automatic)
}
}
let cancel = UIAlertAction(title: "Cancel", style: .cancel) {
(alert) in
}
alert.addAction(add)
alert.addAction(cancel)
present(alert, animated: true, completion: nil)
}
Now update your UITableViewDataSource method as new list item.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! PointsCell
cell.player = players[indexPath.row]
return cell
}
now need to update saveData method to save new list into UserDefaults, you'll call this method whenever you want to save your list.
func saveData() {
UserDefaults.standard.set(try! PropertyListEncoder().encode(self.players), forKey: "items")
}
PointsCell class also need to be updated as new object type:
class PointsCell: UITableViewCell {
#IBOutlet weak var scoreUILabel: UILabel!
#IBOutlet weak var nameUILabel: UILabel!
var player: Player? {
didSet{
if let name = player?.name {
self.nameUILabel?.text = name
}
if let score = player?.score {
self.scoreUILabel?.text = String(score)
}
}
}
#IBAction func pointbuttonPressed(_ sender: Any) {
if self.player != nil{
let score = self.player?.score ?? 0
self.player?.score = score + 1
scoreUILabel.text = String(self.player?.score ?? 0)
}
}
}

Remove specific array element by cell text string value [swift 4]

I have two arrays for my UITableView. One holds the array items and the other holds the value of the array items in case they have a checkmark on them. I am having a problem now because my two arrays don't have the same IndexPath. I need something to delete the item in my selectedChecklist array by its string value. How can I do that?
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
checklist.remove(at: indexPath.row)
selectedChecklist.removeAll { $0 == String(cell.textLabel) }
myTableView.reloadData()
}
}
printed selectedChecklist
["Test", "Test2", "Test3", "Asdf", "Test2", "Test2", "Test"]
Here is my code for the whole array. I am struggling implementing the answers:
import UIKit
class ChecklistViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource{
var dataHolder = [ListItem]()
var newChecklistItemString: String?
var alertInputTextField: UITextField?
#IBOutlet weak var myTableView: UITableView!
let mainStoryboard:UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
var checkedItems: [ListItem] {
return dataHolder.filter { return $0.isChecked }
}
var uncheckedItems: [ListItem] {
return dataHolder.filter { return !$0.isChecked }
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return (dataHolder.count)
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "cell")
cell.textLabel?.font = UIFont.boldSystemFont(ofSize: 18.0)
cell.textLabel?.text = dataHolder[indexPath.row].title
return cell
}
// checkmarks when tapped
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if (tableView.cellForRow(at: indexPath)?.accessoryType != .checkmark) {
tableView.cellForRow(at: indexPath)?.accessoryType = .checkmark
}else {
tableView.cellForRow(at: indexPath)?.accessoryType = .none
}
tableView.deselectRow(at: indexPath, animated: true)
saveDefaults()
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
checkedItems[indexPath.row].isChecked = false
myTableView.reloadData()
}
}
override func viewDidAppear(_ animated: Bool) {
myTableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
addSlideMenuButton()
loadDefaults()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func addNewObject(_ sender: Any) {
let alert = UIAlertController(title: "New Item", message: nil, preferredStyle: .alert)
alert.addTextField { (alertInputTextField) in
alertInputTextField.autocapitalizationType = .sentences
}
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (action) in
self.dismiss(animated: true, completion: nil)
}))
alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { (action) in
let textf = alert.textFields![0] as UITextField
let indexPath = IndexPath(row: self.dataHolder.count, section: 0)
self.dataHolder.append(ListItem(title: textf.text!, isChecked: false))
self.saveDefaults()
self.myTableView.insertRows(at: [indexPath], with: .automatic)
}))
self.present(alert, animated: true, completion: nil)
}
func loadDefaults()
{
self.dataHolder = UserDefaults.standard.array(forKey: "dataHolder") as? [ListItem] ?? []
}
func saveDefaults()
{
UserDefaults.standard.set(self.dataHolder, forKey: "dataHolder")
}
}
class ListItem {
var title: String
var isChecked: Bool
init(title: String, isChecked: Bool) {
self.title = title
self.isChecked = isChecked
}
}
You code is too complicated. As you are using a class as data source the extra arrays are redundant.
Remove checkedItems and uncheckedItems
var checkedItems: [ListItem] {
return dataHolder.filter { return $0.isChecked }
}
var uncheckedItems: [ListItem] {
return dataHolder.filter { return !$0.isChecked }
}
In cellForRow set the checkmark according to isChecked and reuse cells!
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.font = UIFont.boldSystemFont(ofSize: 18.0) // better set this in Interface Builder
let data = dataHolder[indexPath.row]
cell.textLabel?.text = data.title
cell.accessoryType = data.isChecked ? .checkmark : .none
return cell
}
in didSelectRowAt toggle isChecked in the model and update only the particular row
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
dataHolder[indexPath.row].isChecked.toggle()
tableView.reloadRows(at: [indexPath], with: .none)
tableView.deselectRow(at: indexPath, animated: true)
saveDefaults()
}
In tableView:commit:forRowAt: delete the row at the given indexPath
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
dataHolder.remove(at: indexPath.row)
myTableView.deleteRows(at: [indexPath], with: .fade)
saveDefaults()
}
}
And you cannot save an array of a custom class to UserDefaults. I recommend to use a struct and Codable
struct ListItem : Codable {
var title: String
var isChecked: Bool
}
func loadDefaults()
{
guard let data = UserDefaults.standard.data(forKey: "dataHolder") else {
self.dataHolder = []
return
}
do {
self.dataHolder = try JSONDecoder().decode([ListItem].self, for: data)
} catch {
print(error)
self.dataHolder = []
}
}
func saveDefaults()
{
do {
let data = try JSONEncoder().encode(self.dataHolder)
UserDefaults.standard.set(data, forKey: "dataHolder")
} catch {
print(error)
}
}
Avoid using 2 array to "persist" your models. Instead you can generate a single Array with tuples :
var myArray: [(String, Bool)] = [("Test", false), ("Test1", false), ("Test2", false)]
Starting here the problem is simplified, and you will not have index path issue again
Edit
I've changed my code to support [ListItem] saving to UserDefaults- that comment brought by Leo Dabus I also changed a couple of lines that were inspired by vadian's code who appear to have a great coding style.
class ChecklistViewController: BaseViewController, UITableViewDelegate, UITableViewDataSource{
var dataHolder: [ListItem] = DefaultsHelper.savedItems
#IBOutlet weak var myTableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataHolder.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: UITableViewCell.CellStyle.default, reuseIdentifier: "cell")
cell.textLabel?.font = UIFont.boldSystemFont(ofSize: 18.0)
let currentListItem = dataHolder[indexPath.row]
cell.textLabel?.text = currentListItem.title
cell.accessoryType = currentListItem.isChecked ? .checkmark : .none
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
dataHolder[indexPath.row].isChecked.toggle()
DefaultsHelper.saveItems(items: dataHolder)
tableView.reloadRows(at: [indexPath], with: .none)
tableView.deselectRow(at: indexPath, animated: true)
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
dataHolder.remove(at: indexPath.row)
DefaultsHelper.saveItems(items: dataHolder)
myTableView.reloadData()
myTableView.deleteRows(at: [indexPath], with: .automatic)
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
myTableView.reloadData()
}
override func viewDidLoad() {
super.viewDidLoad()
// be sure you've set your tableView's dataSource and delegate to this class (It's fine if you've handled this on the storyboard side)
addSlideMenuButton()
}
#IBAction func addNewObject(_ sender: Any) {
let alert = UIAlertController(title: "New Item", message: nil, preferredStyle: .alert)
alert.addTextField { (alertInputTextField) in
alertInputTextField.autocapitalizationType = .sentences
}
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: { (action) in
self.dismiss(animated: true, completion: nil)
}))
alert.addAction(UIAlertAction(title: "Add", style: .default, handler: { (action) in
let textf = alert.textFields![0] as UITextField
let indexPath = IndexPath(row: self.dataHolder.count, section: 0)
let itemToInsert = ListItem(title: textf.text!, isChecked: false)
// self.dataHolder.append(itemToInsert)
// thought you would want this, it will add your notes in reverse chronological order
self.dataHolder.insert(itemToInsert, at: 0)
DefaultsHelper.saveItems(items: self.dataHolder)
self.myTableView.insertRows(at: [indexPath], with: .automatic)
}))
self.present(alert, animated: true, completion: nil)
}
}
Model classes:
// implementing NSObject and NSCoding to let us save this item in UserDefaults
class ListItem: NSObject, NSCoding{
var title: String
var isChecked: Bool
init(title: String, isChecked: Bool) {
self.title = title
self.isChecked = isChecked
}
// This code lets us save our custom object in UserDefaults
required convenience init(coder aDecoder: NSCoder) {
let title = aDecoder.decodeObject(forKey: "title") as? String ?? ""
let isChecked = aDecoder.decodeBool(forKey: "isChecked")
self.init(title: title, isChecked: isChecked)
}
func encode(with aCoder: NSCoder) {
aCoder.encode(title, forKey: "title")
aCoder.encode(isChecked, forKey: "isChecked")
}
}
class DefaultsHelper{
private static let userDefaults = UserDefaults.standard
private static let dataKey = "dataHolder"
static var savedItems: [ListItem] {
guard let savedData = userDefaults.data(forKey: dataKey) else { return [] }
do{
let decodedData = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(savedData)
return decodedData as? [ListItem] ?? []
}catch{
print("could not fetch items- you may handle this", error)
}
return []
}
static func saveItems(items: [ListItem]){
do{
let encodedData = try NSKeyedArchiver.archivedData(withRootObject: items, requiringSecureCoding: false)
userDefaults.set(encodedData, forKey: dataKey)
}catch{
print("could not save items- you may handle this", error)
}
}
}

How do You Inherit a Custom TableViewController For An TableView in a ViewController

So I have a custom SwipeCellTableView class that I inherited from when using UITableViewControllers. Now I want to just use that class for an ib outlet table view controller in a regular View Controller. It is proving to be very difficult and seemingly not worth it anymore. Can this be done?
Here is the superclass which inherits from a TableViewController, I have tried to change it to inherit from a view controller but it just doesn't work out
class SwipeTableViewController: UITableViewController, SwipeTableViewCellDelegate {
var cell: UITableViewCell?
override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = 80.0
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! SwipeTableViewCell
cell.delegate = self
return cell
}
func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> [SwipeAction]? {
guard orientation == .right else { return nil }
let deleteAction = SwipeAction(style: .destructive, title: "Delete") { action, indexPath in
// handle action by updating model with deletion
self.updateModel(at: indexPath)
}
deleteAction.image = UIImage(named: "delete-icon")
return [deleteAction]
}
func tableView(_ tableView: UITableView, editActionsOptionsForRowAt indexPath: IndexPath, for orientation: SwipeActionsOrientation) -> SwipeOptions {
var options = SwipeTableOptions()
options.expansionStyle = .destructive
//options.transitionStyle = .reveal
return options
}
func updateModel(at indexPath: IndexPath){
//update data model
print("Item deleted from super class")
}
Here is the View Controller I'm trying to access it from:
class GoalsViewController: UIViewController, SwipeTableViewController {
#IBOutlet weak var categoryTable: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func addCategoryPressed(_ sender: UIButton) {
performSegue(withIdentifier: "showgoalsSeg", sender: self)
}
For reference on how I was using it before when using an actual TableViewController:
class CategoryViewController: SwipeTableViewController {
var categories: Results<Category>? //optional so we can be safe
override func viewDidLoad() {
super.viewDidLoad()
loadCategory()
tableView.rowHeight = 80.0
tableView.separatorStyle = .none
}
//MARK: - Tableview Datasource Methods
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//Only get the count of categories if it's nil, else 1
return categories?.count ?? 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//fetching cell from super view
let cell = super.tableView(tableView, cellForRowAt: indexPath)
cell.textLabel?.text = categories?[indexPath.row].name ?? "No Categories Added Yet"
cell.backgroundColor = UIColor(hexString: categories?[indexPath.row].color ?? "000000")
return cell
}
//MARK: - Tableview Delegate Methods
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "goToItems", sender: self)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let destinationVC = segue.destination as! ToDoListViewController
if let indexPath = tableView.indexPathForSelectedRow {
destinationVC.selectedCategory = categories?[indexPath.row]
}
}
//MARK: - Add New Categories
#IBAction func addButtonPressed(_ sender: Any) {
var textField = UITextField()
let alert = UIAlertController(title: "Add New Category", message: "", preferredStyle: .alert)
let action = UIAlertAction(title: "Add Category", style: .default) { (action) in
let newCategory = Category()
newCategory.name = textField.text!
newCategory.color = UIColor.randomFlat.hexValue()
self.save(category: newCategory)
}
alert.addAction(action)
alert.addTextField { (field) in
textField = field
textField.placeholder = "Add a new category"
}
present(alert, animated: true, completion: nil)
}
func save(category: Category){
let realm = try! Realm()
do {
try realm.write{
realm.add(category)
}
} catch {
print("error saving context")
}
tableView.reloadData()
}
override func updateModel(at indexPath: IndexPath) {
super.updateModel(at: indexPath)
let realm = try! Realm()
if let categoryForDeletion = self.categories?[indexPath.row]{
do{
try realm.write{
realm.delete(categoryForDeletion)
}
} catch {
print("error deleting cell")
}
//tableView.reloadData()
}
}
func loadCategory(){
let realm = try! Realm()
categories = realm.objects(Category.self)
tableView.reloadData()
}
Is this even worth persuing? Or doable?

I select a list of place categories in a tableview but in the segue it only sends one

I have a UiViewController with a tableView, this tableView has a list of places (googlePlaces) that I can select (such as restaurants, cinemas, bar) and then tap a button to go on in the next controller where I expect to see a list of places of the type I have chosen; the problem is that it does not leave places for all the selected categories, for example if I had select cinema, bar and restaurant, one time it shows me only restaurants, the other only the cinemas, in a completely casual manner. Here is my prepare
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == nearbySearchSegueIdentifier {
let selectedCategories: [QCategoryy] = tableView.indexPathsForSelectedRows?.map({ (indexPath) -> QCategoryy in
return list[indexPath.row] }) ?? []
if let selectedRows = tableView.indexPathsForSelectedRows {
if let vc : CourseClass2 = segue.destination as? CourseClass2 {
vc.categories = selectedCategories
}
}
}
}
and this is the next viewController
import UIKit
import CoreLocation
import Social
import AVFoundation
private let resueIdentifier = "MyTableViewCell"
extension UIViewController {
func present(viewController : UIViewController, completion : (() -> ())? = nil ){
if let presented = self.presentedViewController {
presented.dismiss(animated: true, completion: {
self.present(viewController, animated: true, completion: completion)
})
} else {
self.present(viewController, animated: true, completion: completion)
}
}
}
class CourseClass2: UIViewController, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var tableView: UITableView!
var locationManager:CLLocationManager?
let minimumSpacing : CGFloat = 15 //CGFloat(MAXFLOAT)
let cellWidth: CGFloat = 250
let radius = 5000 // 5km
var categories: [QCategoryy?]? = []
var currentLocation : CLLocationCoordinate2D?
var places: [QPlace] = []
var isLoading = false
var response : QNearbyPlacesResponse?
var rows = 0
var numberPlaces = 0
override func viewDidLoad() {
super.viewDidLoad()
for category in categories! {
title = category?.name
}
tableView.dataSource = self
tableView.delegate = self
numberPlaces = HomeClass.globalLimit
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
determineMyCurrentLocation()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
rows = 0
tableView.reloadData()
for category in categories! {
category?.markView()
}
}
#IBAction func refreshTapped(_ sender: Any) {
rows = 0
print("numberOfRows Call", self.numberPlaces)
tableView.reloadData()
}
func canLoadMore() -> Bool {
if isLoading {
return false
}
if let response = self.response {
if (!response.canLoadMore()) {
return false
}
}
return true
}
func loadPlaces(_ force:Bool) {
if !force {
if !canLoadMore() {
return
}
}
print("load more")
isLoading = true
for category in categories! {
NearbyPlaces.getNearbyPlaces(by: category?.name ?? "food", coordinates: currentLocation!, radius: radius, token: self.response?.nextPageToken, completion: didReceiveResponse)
}
}
func didReceiveResponse(response:QNearbyPlacesResponse?, error : Error?) -> Void {
if let error = error {
let alertController = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
let actionDismiss = UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)
let actionRetry = UIAlertAction(title: "Retry", style: .default, handler: { (action) in
DispatchQueue.main.async {
self.loadPlaces(true)
}
})
alertController.addAction(actionRetry)
alertController.addAction(actionDismiss)
DispatchQueue.main.async {
self.present(viewController: alertController)
}
}
if let response = response {
self.response = response
if response.status == "OK" {
if let placesDownloaded = response.places {
places.append(contentsOf: placesDownloaded)
}
self.tableView?.reloadData()
} else {
let alert = UIAlertController.init(title: "Error", message: response.status, preferredStyle: .alert)
alert.addAction(UIAlertAction.init(title: "Cancel", style: .cancel, handler: nil))
alert.addAction(UIAlertAction.init(title: "Retry", style: .default, handler: { (action) in
DispatchQueue.main.async {
self.loadPlaces(true)
}
}))
self.present(viewController: alert)
}
isLoading = false
}
else {
print("response is nil")
}
}
func numberOfSections(in tableView: UITableView) -> Int {
print("numberOfsection Call")
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("numberOfRows Call")
if places.count < self.numberPlaces {
return places.count /* rows */
}
return self.numberPlaces
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: resueIdentifier, for: indexPath) as! MyTableViewCell
let place = places[indexPath.row]
cell.update(place: place)
if indexPath.row == places.count - 1 {
loadPlaces(false)
}
print("CellForRow Call")
return (cell)
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
UIView.animate(withDuration: 0.2, animations: {
let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell", for: indexPath) as! MyTableViewCell
})
performSegue(withIdentifier: "goToLast" , sender: indexPath)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100
}
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == UITableViewCellEditingStyle.delete {
places.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
}
}
What I have to do to make that if had selected more than one category of places, in the tableView of the next viewController shows places for each selected category? (since there is a limit of places that can be shown represented by numberPlaces = HomeClass.globalLimit the best solution it would be to have at least one place for each selected category and others added randomly)
EDIT
here where is the indexPathsForSelectedRows
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let identifier = "CATEGORY_CELL"
let cell = tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath)
let selectedIndexPaths = tableView.indexPathsForSelectedRows
let rowIsSelected = selectedIndexPaths != nil && selectedIndexPaths!.contains(indexPath)
/* cell.accessoryType = rowIsSelected ? .checkmark : .none */
cell.accessoryType = list[indexPath.row].isSelected ? .checkmark : .none
cell.textLabel?.text = list[indexPath.row].name
return cell
}
Apparently your problem is the architecture of your code. On loadPlaces you are iterating through your categories and doing several network calls. Then you append those results to places and use reloadData to reload the table, but on cellForRowAt you call loadPlaces again.
Even that you set isLoading = true inside loadPlaces you have multiple requests going on and all of them set isLoading = false at the end. So at some point you will have some unexpected result. You also have some force load cases that add up to all that.
Last but not least, since you are calling self.tableView?.reloadData() inside a closure, it its possible that its not updating correctly.
TL;DR
Wrap your reloadData around a DispatchQueue.main.async block.
Implement a queue that serialises your network requests to put some order around your calls. You can use a library like this for example.
let queue = TaskQueue()
for category in categories {
queue.tasks +=~ { result, next in
// Add your places request here
}
queue.tasks +=! {
// Reload your table here
}
queue.run {
// check your places array is correct
}
}
Other observations:
Your title is going to be always the last category on categories, since you are not using all the array on title = category?.name.
To better understand whats going on, try to select only 2 categories and to see if there is a patter on which one is loaded (always the first, or always the second). If there is no pattern at all its because the problem is for sure networking.

Resources