2

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
}
}
3
  • "i have to arrays for my UITableView". Don't. Use one single array what will have custom struct/object and holds all of the necessary values, like "is checked", "value to display in the cell label", etc. Commented Nov 30, 2018 at 14:20
  • 2
    use only one list with struct { title:String, selected:Bool} Commented Nov 30, 2018 at 14:20
  • delete operation in array is very costly, be careful Commented Nov 30, 2018 at 14:59

3 Answers 3

2

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)
        }
    }
    
Sign up to request clarification or add additional context in comments.

Comments

1

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

7 Comments

So if I delete the string by indexPath, the other property will be deleted too?
I think you don't want to remove it, but changing the 2 params ( the boolean ) to true or false depending of the selection state isn't it ?
Oh yeah you are completely right. I didn't thought of that :D
i edited my question would you mind looking over it? :)
Apple strongly discourages from using tuples beyond a temporary scope.
|
1

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

6 Comments

I like your answer... how do I set the cell text to the dataHolder indexPath row? My tries are throwing errors
That should do the job- cell.label.text = dataHolder[indexPath.row].title, though I don't know exactly how your table looks like so it's hard for me to tell which array you should populate from on which step... more info would help
i edited my question would you mind looking over it? :)
You can't save custom objects to UserDefaults, you need to make your custom class inherit from NSObject and conform to NSCoding to be able to save it in a property list or as Data to userDefaults
You are right, I missed it and I’m going to update my answer
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.