Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Adding new task duplicates existing tasks in UITableView

I’m working on an iOS app where users can add tasks using Core Data. I have a TaskViewModel that handles the data flow between Core Data and my UITableView. However, I’m encountering an issue where adding a new task duplicates existing tasks in the table view.

Here’s how the app flow works:

When the user taps the "Add Task" button, it presents an AddNewTaskViewController.
In the AddNewTaskViewController, the user enters the task name and taps "Save".
The entered task is then added to Core Data via the TaskViewModel.
After adding the task, the TaskViewModel updates its tasks array and notifies the UITableView to reload its data.

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

Despite these steps, whenever I add a new task, it duplicates existing tasks in the table view. For example, if I add "Test" as the first task and then "Test2" as the second task, the table view shows "Test, Test, Test2".

here is gif of the problem after adding new task: gif of the problem

ViewController.Swift

class ViewController: UIViewController {
    let viewModel = TaskViewModel.shared
    lazy var tasksTableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.dataSource = self
        tableView.delegate = self
        tableView.register(TaskTableViewCell.self, forCellReuseIdentifier: "TaskTableCell")
       
        tableView.estimatedRowHeight = 200
        tableView.rowHeight = UITableView.automaticDimension
        return tableView
    }()

    lazy var addNewButton: UIButton = {
        let v = UIButton()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.setTitle("Add Task", for: .normal)
        v.backgroundColor = .systemBlue
        v.layer.cornerRadius = 10
        v.frame = CGRect(x: 0, y: 0, width: 100, height: 35)

        v.addTarget(self, action: #selector(addPressed(sender: )), for: .touchUpInside)
        return v
    }()

    override func viewDidAppear(_ animated: Bool) {

        tasksTableView.reloadData()
    }


    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        view.backgroundColor = .systemBackground
        title = "Alisveris Listesi"

       navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "plus.circle.fill"), style: .done, target: self, action: #selector(showAddVC))

        navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .close,  target: self, action: #selector(deleteAllPressed(sender: )))

        setupViews()
    }
    private func setupViews() {
        view.addSubview(tasksTableView)
        configureConstraints()
    }

    private func configureConstraints() {
        NSLayoutConstraint.activate([
            tasksTableView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16),
            tasksTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
            tasksTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
            tasksTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 16)
        ])
    }

    @objc func showAddVC() {
        let vc = AddNewTaskViewController()
        vc.delegate = self
        let navController = UINavigationController(rootViewController:  vc)

        navigationController?.present(navController, animated: true)
    }

    @objc func addPressed(sender: UIButton) {
        viewModel.addNewTask(name: "New Task")
    }

    @objc func getTaskPressed(sender: UIButton) {
        let tasks = CoreDataManager.shared.fetchAll()
        for task in tasks {
            print(task.name ?? "" )
        }
    }

    @objc func deleteAllPressed(sender: UIButton) {
        let tasks = CoreDataManager.shared.fetchAll()
        for task in tasks {
            CoreDataManager.shared.deleteItem(id: task.id ?? UUID() )
        }

        let fetchedTasks = CoreDataManager.shared.fetchAll()
        
        tasksTableView.reloadData()
        print(fetchedTasks.count)

    }

}


extension ViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.numberOfRows(by: section)
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//        if indexPath.section == 0 {
//            guard let cell = tableView.dequeueReusableCell(withIdentifier: "TaskTableCell", for: indexPath) as? TaskTableViewCell else {
//                return UITableViewCell()
//            }
////            let taskSummary =
//            cell.configure(with: viewModel.tasks[indexPath.row])
//            return cell
//        }

        guard let cell = tableView.dequeueReusableCell(withIdentifier: "TaskTableCell", for: indexPath) as? TaskTableViewCell else {
            return UITableViewCell()
        }
        cell.configure(with: viewModel.tasks[indexPath.row])
        return cell
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return viewModel.numberOfTasks
    }


}

extension ViewController: ItemControllerDelegate {
    // Implement delegate methods
    func didItemAdded() {

        // Handle item added event
        tasksTableView.reloadData()
    }

    func didItemUpdated() {
        // Handle item updated event
    }
}


class TaskViewModel {
    static let shared = TaskViewModel()
    var tasks = [Task]()
    
    private init() {
        // Clear the existing tasks array
        tasks.removeAll()
        // Get all records from CoreData
        fetchAllTasks()
    }

    var numberOfTasks: Int {
        tasks.count
    }

    func fetchAllTasks() {
        // Fetch all data from core data
        tasks = CoreDataManager.shared.fetchAll().map(Task.init)
        print("Number of tasks after fetching: \(tasks.count)")
    }

    func numberOfRows(by section:Int) -> Int {
        if section == 0 {
            return 1
        }
        return numberOfTasks
    }

    func getTasksByType() -> (complete: Int, InComplete: Int) {
        let completedCount = tasks.lazy.filter({ $0.completed }).count
        let InCompletedCount = tasks.lazy.filter({ !$0.completed }).count
        
        return (completedCount,InCompletedCount)
    }

    func task(by index: Int) -> Task {
        return tasks[index]
    }

    func addNewTask(name: String) {
        let newItem = Item(context: CoreDataManager.shared.context)
        newItem.id = UUID()
        newItem.completed = false

        newItem.name = name
        newItem.createdAt = Date.now

        let newTask = Task(task: newItem)
        tasks.append(newTask)

        CoreDataManager.shared.addNewItem(item: newItem)

    }

    func toggleCompleted(task: Task) {
        CoreDataManager.shared.toggleCompleted(id: task.id)
        // call core data to toggle
        fetchAllTasks()
    }

    func deleteItem(task: Task) {
        CoreDataManager.shared.deleteItem(id: task.id)
        // call core data to delete the task
        fetchAllTasks()
    }

}

TaskViewModel.swift

class TaskViewModel {
    static let shared = TaskViewModel()
    var tasks = [Task]()
    
    private init() {
        // Clear the existing tasks array
        tasks.removeAll()
        // Get all records from CoreData
        fetchAllTasks()
    }

    var numberOfTasks: Int {
        tasks.count
    }

    func fetchAllTasks() {
        // Fetch all data from core data
        tasks = CoreDataManager.shared.fetchAll().map(Task.init)
        print("Number of tasks after fetching: \(tasks.count)")
    }

    func numberOfRows(by section:Int) -> Int {
        if section == 0 {
            return 1
        }
        return numberOfTasks
    }

    func getTasksByType() -> (complete: Int, InComplete: Int) {
        let completedCount = tasks.lazy.filter({ $0.completed }).count
        let InCompletedCount = tasks.lazy.filter({ !$0.completed }).count
        
        return (completedCount,InCompletedCount)
    }

    func task(by index: Int) -> Task {
        return tasks[index]
    }

    func addNewTask(name: String) {
        let newItem = Item(context: CoreDataManager.shared.context)
        newItem.id = UUID()
        newItem.completed = false

        newItem.name = name
        newItem.createdAt = Date.now

        let newTask = Task(task: newItem)
        tasks.append(newTask)

        CoreDataManager.shared.addNewItem(item: newItem)

    }

    func toggleCompleted(task: Task) {
        CoreDataManager.shared.toggleCompleted(id: task.id)
        // call core data to toggle
        fetchAllTasks()
    }

    func deleteItem(task: Task) {
        CoreDataManager.shared.deleteItem(id: task.id)
        // call core data to delete the task
        fetchAllTasks()
    }

}

CoreDataManager.swift

class CoreDataManager {
    static let shared = CoreDataManager()

    private init() {}

    // persistence CoreDataModel
    lazy var persistentContainer: NSPersistentContainer = {
        // Name of the CoreDataModel -- Items
        let container = NSPersistentContainer(name: "Items")
        container.loadPersistentStores { _ , error in
            if let error = error as NSError? {
                fatalError("Unresolved error \(error), \(error.userInfo)")
            }
        }
        return container
    }()
    
    // for saving -- save, delete etc.
    var context: NSManagedObjectContext {
        persistentContainer.viewContext
    }

    func saveContext() {
        if context.hasChanges {
            do {
                try context.save()
            } catch {
                let nserror = error as NSError
                print("Error - saveContext:", nserror.description, nserror.userInfo)
            }
        }
    }

    // Fetch all Items from CoreData
    func fetchAll() -> [Item] {
        var tasks = [Item]()
        
        // EntityName.fetchRequest()
        let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
        let sortByCreatedDate = NSSortDescriptor(key: "createdAt", ascending: true)

        fetchRequest.sortDescriptors = [sortByCreatedDate]

        do {
            tasks = try context.fetch(fetchRequest)
        } catch {
            let nserror = error as NSError
            print("Error - fetchAll:", nserror.description, nserror.userInfo)
        }

        return tasks
    }

    // Add a item to CoreData
    func addNewItem(item: Item) {
        // create a new item if this item doesn't exist

        // save changes
        saveContext()
    }
    
    // Toggle Completed from CoreData
    func toggleCompleted(id: UUID) {
        let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
        let predicate = NSPredicate(format: "id=%@", id.uuidString)
        fetchRequest.predicate = predicate

        do {
            if let fetchedTasks = try context.fetch(fetchRequest).first(where: { foundItem in
                foundItem.id == id
            }) {
                fetchedTasks.completed = !fetchedTasks.completed
            }

            // Save Core Data
            saveContext()

        } catch let error as NSError {
            print("toggleCompleted Error: \(error) \(error.userInfo)")
        }
    }

    // Delete item from CoreData
    func deleteItem(id: UUID) {
        let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()

        let predicate = NSPredicate(format: "id=%@", id.uuidString)
        fetchRequest.predicate = predicate

        do {
            let fetchedTasks = try context.fetch(fetchRequest)

            for task in fetchedTasks {
                context.delete(task)
            }

            // Save Core Data
            saveContext()

        } catch let error as NSError {
            print("deleteItem Error: \(error) \(error.userInfo)")
        }
    }

}

AddNewTaskViewController.swift

class AddNewTaskViewController: UIViewController {
    weak var delegate: ItemControllerDelegate?

    lazy var taskNameLabel: UILabel = {
        let label = UILabel()
        label.translatesAutoresizingMaskIntoConstraints = false
        label.text = "Task Name"
        return label
    }()

    lazy var taskNameTextField: UITextField = {
        let textField = UITextField()
        textField.translatesAutoresizingMaskIntoConstraints = false
        textField.placeholder = "Enter Task Name"
        textField.borderStyle = .roundedRect
        return textField
    }()

    let viewModel = TaskViewModel.shared

    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupView()
    }

    private func setupView() {
        view.backgroundColor = .systemBackground
        title = "Add New Task"
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .done, target: self, action: #selector(savePressed))

        view.addSubview(taskNameLabel)
        view.addSubview(taskNameTextField)
        setupConstraints()
    }

    private func setupConstraints() {
        NSLayoutConstraint.activate([
            taskNameLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 15),
            taskNameLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 15),
            taskNameLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -15),

            taskNameTextField.topAnchor.constraint(equalTo: taskNameLabel.bottomAnchor, constant: 15),
            taskNameTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 15),
            taskNameTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -15),
        ])
    }

    // Action Function
    @objc func savePressed() {
        guard let name = taskNameTextField.text else { 
            let alert = UIAlertController(title: "Error", message: "Name can't be empty", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "OK", style: .default))
            present(alert, animated: true)
            return }

        viewModel.addNewTask(name: name)

        delegate?.didItemAdded()

        navigationController?.dismiss(animated: true)
    }
}

I’ve tried clearing the tasks array before fetching tasks from Core Data, but the issue persists. What am I doing wrong? How can I prevent the duplication of tasks in my table view?

Any help or insights would be greatly appreciated! Thank you in advance.

>Solution :

In the delegate method numberOfSections(in:) you return the total number of rows which must be wrong

func numberOfSections(in tableView: UITableView) -> Int {
    return viewModel.numberOfTasks
}

Looking at the function numberOfRows(by:) in your view model I assume the number of sections are two so change the delegate method to

func numberOfSections(in tableView: UITableView) -> Int {
    return 2
}
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading