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