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

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

Related

How to fix deleted task being restored in Userdefaultrs

I'm making a TodoList app.
If you swipe a task from the list to delete it and then add a new task, the deleted task will be restored.
Use storyboard and UIKit.
I want to keep it deleted, what should I do? ..
mac 10.15.7
xcode 12.1
import UIKit
class ViewController: UIViewController {
#IBOutlet var tableView: UITableView!
var tasks = [String]()
override func viewDidLoad() {
super.viewDidLoad()
self.title = "リスト"
tableView.delegate = self
tableView.dataSource = self
if !UserDefaults().bool(forKey: "setup") {
UserDefaults().set(true, forKey: "setup")
UserDefaults().set(0, forKey: "count")
}
updateTasks()
}
func updateTasks() {
tasks.removeAll()
guard let count = UserDefaults().value(forKey: "count") as? Int else {
return
}
for x in 0..<count {
if let task = UserDefaults().value(forKey: "task_\(x+1)") as? String {
tasks.append(task)
}
}
tableView.reloadData()
}
#IBAction func didTapAdd() {
let vc = storyboard?.instantiateViewController(identifier: "entry") as! EntryViewController
vc.title = "リストに追加"
vc.update = {
//非同期処理 更新を優先
DispatchQueue.main.async {
self.updateTasks()
}
}
navigationController?.pushViewController(vc, animated: true)
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let vc = storyboard?.instantiateViewController(identifier: "task") as! TaskViewController
vc.title = "リストに追加"
vc.task = tasks[indexPath.row]
navigationController?.pushViewController(vc, animated: true)
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return tasks.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.textLabel?.text = tasks[indexPath.row]
return cell
}
//タスクを削除
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
let index = indexPath.row
tasks.remove(at: index)
let userDefaults = UserDefaults.standard
userDefaults.set(tasks, forKey: "tasks")
userDefaults.removeObject(forKey: "tasks")
tableView.reloadData()
}
}
Is remove (at :) different?
import UIKit
class EntryViewController: UIViewController, UITextFieldDelegate {
#IBOutlet var field: UITextField!
var update: (() -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
field.delegate = self
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "保存", style: .done, target: self, action: #selector(saveTask))
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
saveTask()
return true
}
#objc func saveTask() {
guard let text = field.text, !text.isEmpty else {
return
}
guard let count = UserDefaults().value(forKey: "count") as? Int else {
return
}
let newCount = count + 1
UserDefaults().set(newCount, forKey: "count")
UserDefaults().set(text, forKey: "task_\(newCount)")
update?()
navigationController?.popViewController(animated: true)
}
}
How to save UserDefaults?
The problem I see is; in updateTasks function, you get the tasks from UserDefaults by keys as task_1, task_2 etc. But when deleting a specific task, lets say task_2, you are just removing it from tasks array, not deleting it from UserDefaults. So whenever you call updateTasks function, the deleted task re-appears.
In your EntryViewController, change the saveTask function with following:
#objc func saveTask() {
guard let text = field.text, !text.isEmpty else {
return
}
let tasks = UserDefaults.standard.array(forKey: "tasks") as? [String] ?? []
tasks.append(text)
UserDefaults.standard.set(tasks, forKey: "tasks")
update?()
navigationController?.popViewController(animated: true)
}
In your ViewController, change the following functions:
func updateTasks() {
tasks.removeAll()
tasks = UserDefaults.standard.array(forKey: "tasks") as? [String] ?? []
tableView.reloadData()
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
tasks.remove(at: indexPath.row)
let userDefaults = UserDefaults.standard
userDefaults.set(tasks, forKey: "tasks")
updateTasks()
}

Im getting an error at cellForRowAt and im not sure why

I am getting an error in the function cellForRowAt
Cannot assign value of type 'Product' to type 'String?'
Is cell.textLabel?.text = product the issue that's causing this error?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let product = items[indexPath.row]
cell.textLabel?.text = product
cell.contentView.backgroundColor = UIColor(red:0.92, green:0.92, blue:0.92, alpha:1.0)
cell.textLabel?.textColor = UIColor(red:0.13, green:0.13, blue:0.13, alpha:1.0)
tableView.separatorColor = UIColor(red:0.13, green:0.13, blue:0.13, alpha:1.0)
return cell
}
cell.textLabel?.text can only show a String object, not other objects.
It will be product.item or product.price or product.salesPrice or all in one line. (based on your requirement).
Make sure the value of product is not nil.
cell.textLabel?.text = "\(product.item) \(product.price) \(product.salesPrice)"
The full code you can try this:
class ViewController: UIViewController, UIAdaptivePresentationControllerDelegate {
#IBOutlet weak var tableView: UITableView!
var items:[Product]? = []
// VIEW LOAD
override func viewDidLoad() {
super.viewDidLoad()
if #available(iOS 13.0, *) {
self.isModalInPresentation = true
}
getData()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
getData()
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(true)
storeData()
}
override var prefersStatusBarHidden: Bool {
return true
}
// ADD ITEMS
#IBAction func addButtonTapped(_ sender: Any) {
let alert = UIAlertController(title: "Product Information", message: nil, preferredStyle: .alert)
alert.addTextField { (itemTextField) in
itemTextField.placeholder = "Item"
}
alert.addTextField { (priceTextField) in
priceTextField.placeholder = "Price"
}
alert.addTextField { (salePriceTextField) in
salePriceTextField.placeholder = "Sale Price"
}
let action = UIAlertAction(title: "Add", style: .default) { (_) in
let item = alert.textFields?[0].text ?? ""
let price = alert.textFields?[1].text ?? ""
let salesPrice = alert.textFields?[2].text ?? ""
let product = Product(item: item, price: price, salesPrice: salesPrice)
self.addProduct(product)
}
alert.addAction(action)
present(alert, animated: true)
storeData()
}
func addProduct(_ product: Product) {
let index = 0
items?.insert(product, at: index)
let indexPath = IndexPath(row: index, section: 0)
tableView.insertRows(at: [indexPath], with: .left)
storeData()
}
//STORE DATA
func storeData() {
UserDefaultUtil.saveData(products: items)
}
func getData() {
items = UserDefaultUtil.loadProducts()
}
}
//EXTENSION
extension ViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items!.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell()
let product = items![indexPath.row]
cell.textLabel?.text = "\(product.item!) \(product.price) \(product.salesPrice)"
cell.contentView.backgroundColor = UIColor(red:0.92, green:0.92, blue:0.92, alpha:1.0)
cell.textLabel?.textColor = UIColor(red:0.13, green:0.13, blue:0.13, alpha:1.0)
tableView.separatorColor = UIColor(red:0.13, green:0.13, blue:0.13, alpha:1.0)
return cell
}
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
guard editingStyle == .delete else { return }
items?.remove(at: indexPath.row)
tableView.deleteRows(at: [indexPath], with: .fade)
storeData()
}
}
class UserDefaultUtil {
private static let Key = "savedData"
private static func archivePeople(people : [Product]) -> NSData {
return NSKeyedArchiver.archivedData(withRootObject: people as NSArray) as NSData
}
static func loadProducts() -> [Product]? {
if let unarchivedObject = UserDefaults.standard.object(forKey: Key) as? Data {
return NSKeyedUnarchiver.unarchiveObject(with: unarchivedObject as Data) as? [Product]
}
return nil
}
static func saveData(products : [Product]?) {
let archivedObject = archivePeople(people: products!)
UserDefaults.standard.set(archivedObject, forKey: Key)
UserDefaults.standard.synchronize()
}
}
class Product: NSObject, NSCoding {
var item: String?
var price: String?
var salesPrice: String?
required init(item:String, price:String, salesPrice: String) {
self.item = item
self.price = price
self.salesPrice = salesPrice
}
required init(coder aDecoder: NSCoder) {
self.item = aDecoder.decodeObject(forKey: "item") as? String
self.price = aDecoder.decodeObject(forKey: "price") as? String
self.salesPrice = aDecoder.decodeObject(forKey: "salesPrice") as? String
}
public func encode(with aCoder: NSCoder) {
aCoder.encode(item, forKey: "item")
aCoder.encode(price, forKey: "price")
aCoder.encode(salesPrice, forKey: "salesPrice")
}
}
cell.textLabel?.text = product
try doing product.item or product.price or product.salesPrice.
cell.textLabel?.text is expecting a string what you are setting is another type.
First of all reuse cells.
Never create cells with the default initializer.
Assign an identifier to the cell in Interface Builder (for example MainCell), then replace
let cell = UITableViewCell()
with
let cell = tableView.dequeueReusableCell(withIdentifier: "MainCell", for: indexPath)
The error is very clear. As already mentioned in other answers you have to assign the value of a property of Product to the label
cell.textLabel?.text = product.title
Don't create cells with default initializer
Assign an identifier to the tableview's cell in interface builder
let cell = tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
you have to assign the property of Product to label's text
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableview.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
let product = items[indexPath.row]
cell.textLabel?.text = product.price // product.item, product.salesPrice
return cell
}

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?

Table View Delete from Firebase

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

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