Building a Basic To-Do App with Local Storage: A Beginner’s Guide

Abhishek Biswas
19 min readApr 28, 2024

In today’s tutorial, I’ll guide you through the process of creating a robust ToDo app for iOS devices using Swift and UIKit. This app utilizes local storage, specifically Core Data, for efficient offline data management on iOS devices.

We’ll adopt a predominantly programmatic approach, leveraging Swift’s powerful capabilities, with minimal reliance on Storyboard. While Storyboard facilitates rapid UI development, its limitations in terms of customization and reusability prompt us to prioritize programmatic implementation. By utilizing Swift’s native features, we ensure a streamlined and flexible application architecture.

Throughout this tutorial, we’ll adhere to professional coding standards and best practices, ensuring clarity, maintainability, and scalability of our codebase. Additionally, we’ll refrain from incorporating third-party libraries, demonstrating the full potential of Swift and UIKit in building robust iOS applications.

Let’s embark on this journey to create a feature-rich ToDo app that seamlessly integrates with Core Data for efficient local data management on iOS devices.

Requirements

  1. Basic Knowledge of Swift and UIKit Framework
  2. Xcode
  3. Knowledge of Core Data

Why CoreData ? An Brief Introduction.

Core Data is Apple’s Builtin framework for working and storing offline data in iOS device.

  • Ease of Use: provides a high-level abstraction layer that simplifies the process of managing object graphs and their persistence. It allows developers to work with objects rather than tables and columns, making it easier to manipulate and store data.
  • Object Graph Management: Core Data manages the relationships between objects in app’s data model, allowing you to represent complex data structures with ease. This is particularly useful for tasks like managing tasks and their associated properties.
  • Performance: Having features like lazy loading and faulting that help reduce memory usage and improve app responsiveness. This makes it well-suited for handling large datasets efficiently, which is often the case in to-do apps with many tasks.
  • Concurrency Support: provides built-in support for concurrency, allowing app to perform data operations concurrently without risking data corruption or inconsistency. This is essential for apps that need to handle multiple tasks simultaneously, such as syncing data in the background.
  • Integration with UIKit: integrates seamlessly with UIKit, Apple’s framework for building user interfaces. This means it can easily bind Core Data entities to UI elements like table views and forms, making it straightforward to display and edit the app’s data.
  • Built-in Persistence: offers multiple options for data persistence, including SQLite, XML, and in-memory stores. This flexibility allows to choose the storage mechanism that best fits your app’s requirements, whether it’s a lightweight SQLite database or an in-memory store for temporary data.

Rough Flow of App

The Rough Flow of app is described briefly in the following below points.

  1. Home Screen:
  • When the user opens the app, the Home Screen pops up.
  • Check if any previous todo data is present in Core Data.
  • If data is present, display it on the Home Screen.
  • Provide options for the user to either edit the previous tasks or create new ones.

2. Creating New Task:

  • If the user chooses to create a new todo task, they can press the floating add button.
  • Navigate to the next screen, which contains two text fields for the task name and description.
  • Perform validation on the input fields.
  • After filling and validating, add the task to Core Data.
  • Dismiss the current screen and return to the Home Screen, showing the newly added task.

3. Editing Task:

  • If the user wants to edit a task, they can tap the “Edit Task” button on the Home Screen.
  • Navigate to the Edit Screen where the user can modify the task details.
  • Provide an option to cancel the edit.
  • Once the user finishes editing, update the task in the database.
  • Navigate back to the Home Screen and display the updated task.

4. Deleting Task:

  • If the user wants to delete a task, they can tap the “Delete” button on the Home Screen.
  • Mark the task as completed and remove it from the Home Screen.
  • Update the current task model and update the data in the database.
  • Optionally, move the completed task to a separate section or screen for completed tasks.

5. Viewing Completed Tasks:

  • Provide a button in the navigation bar for viewing completed tasks.
  • When the user taps on the “Completed Tasks” button, navigate to a Completed Tasks Screen.
  • Display tasks that are marked as completed, allowing users to review their completed tasks.

By this flow ensures a smooth user experience by allowing users to seamlessly create, edit, and delete tasks, as well as view their completed tasks. Each action is accompanied by appropriate navigation and database updates to maintain data integrity and provide feedback to the user. Here task means ToDo Task

Flow for Todo-app (ios)

Screens Screenshots

Below are screenshots of the ToDo app’s user interface after it has been built. Additionally, I’ve included source code snippets to illustrate how the app was constructed.

Screens For Todo-app (screenshots) (not all included)

Helper Extensions

These are the Helper extension added to make some part of UI building easy.

  1. UITextField Extension:
  • This extension adds a bottom border to a UITextField.
  • The addBottomBorder function takes two parameters: height (default is 1.0) and color (default is black).
  • It creates a UIView with the specified color and height, and adds it as a subview of the text field.
  • Constraints are applied to make the border view span the entire width of the text field and sit at the bottom.

2. UIButton Extension:

  • This extension provides a method to style a UIButton with elevation and rounded corners.
  • The makeElevation function sets the background color and applies a shadow effect to the button's layer.
  • It creates a shadow with a specified color, offset, opacity, and radius to give the button a raised appearance.
  • Rounded corners are applied to the button’s layer to give it a rounded shape.

3. UIViewController Extension:

  • This extension adds a convenience method for presenting UIAlertController as an alert.
  • The makeAlert function creates an alert with the specified title and message, and an optional cancel button.
  • It takes a closure parameter onTap which is executed when the user taps the "ok" button.
  • The alert is presented using the current view controller’s present method.

4. UUID Extension:

  • This extension adds two functionalities related to UUIDs.
  • The timeBasedUUID function generates a time-based UUID using the uuid_generate_time function from the CoreFoundation framework.
  • The timestamp property extracts the timestamp (in seconds) embedded within a time-based UUID.
  • It splits the UUID string into components, calculates the timestamp from the first three components, and converts it into seconds.

These extensions provide convenient methods for common tasks such as styling UI elements, presenting alerts, and working with UUIDs. They encapsulate reusable functionality and promote cleaner and more readable code.


extension UITextField {
internal func addBottomBorder(height: CGFloat = 1.0, color: UIColor = .black) {
let borderView = UIView()
borderView.backgroundColor = color
borderView.translatesAutoresizingMaskIntoConstraints = false
addSubview(borderView)
NSLayoutConstraint.activate(
[
borderView.leadingAnchor.constraint(equalTo: leadingAnchor),
borderView.trailingAnchor.constraint(equalTo: trailingAnchor),
borderView.bottomAnchor.constraint(equalTo: bottomAnchor),
borderView.heightAnchor.constraint(equalToConstant: height)
]
)
}
}

extension UIButton {
internal func makeElevation() {
self.backgroundColor = UIColor(red: 146/255, green: 150/255, blue: 206/255, alpha: 1.0)
self.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
self.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
self.layer.shadowOpacity = 1.0
self.layer.shadowRadius = 0.0
self.layer.masksToBounds = false
self.layer.cornerRadius = 10.0
}
}

extension UIViewController {
func makeAlert(title: String, message: String, isCancelNeed: Bool ,onTap: (() -> Void)?){
let alert : UIAlertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "ok", style: .default, handler: { action in
onTap?()
}))
if isCancelNeed {
alert.addAction(UIAlertAction(title: "cancel", style: .cancel))
}
self.present(alert, animated: true)
}
}

extension UUID {
static func timeBasedUUID() -> UUID? {
let uuidSize = MemoryLayout<uuid_t>.size
let uuidPointer = UnsafeMutablePointer<UInt8>.allocate(capacity: uuidSize)
uuid_generate_time(uuidPointer)
let uuid = uuidPointer.withMemoryRebound(to: uuid_t.self, capacity: 1) {
return $0.pointee
}

uuidPointer.deallocate()
return UUID(uuid: uuid)
}

var timestamp: TimeInterval {
let components = uuidString.split(separator: "-")
assert(components.count == 5)
let hex = String(components[2].dropFirst()) + components[1] + components[0]
// 100-nanoseconds intervals
let interval = Int(hex, radix: 16)!
let seconds = TimeInterval(interval / 10000000)
return seconds - 12219292800
}
}

Custom UI Helpers

These are the Helper Class added to make some part of UI building easy. Basically idea for making reusable UI.

class UIFactoryClass {
static func makeCustomTxtField(hintString: String) -> UITextField {
let txtField = UITextField()
txtField.placeholder = hintString
txtField.translatesAutoresizingMaskIntoConstraints = false
txtField.layer.masksToBounds = true
return txtField
}

static func makeCustomButton(buttonTitle: String) -> UIButton{
let button = UIButton()
button.setTitle(buttonTitle, for: .normal)
button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 18)
button.setTitleColor(UIColor.white, for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
button.makeElevation()
return button
}
}

Home Screen

The NotesListViewController class is responsible for managing the list of notes in the ToDo app. Upon launching the app, it fetches existing notes from Core Data and displays them in a table view. Each note is represented by a custom cell, which contains buttons for editing, deleting, and marking the note as completed. Users can tap on a note to select it for further actions.

The floating “Add” button allows users to create new notes, leading to a screen where they can enter the note’s title and description. Upon submission, the note is added to the list and stored in Core Data.

Additionally, the controller provides functionality for editing, deleting, and marking notes as completed. When a note is edited, its details are updated in both the UI and Core Data. If a note is marked as completed, it is removed from the list and stored in a separate Completed Tasks screen.

//
// NotesListViewController.swift
// TodoApp
//
// Created by Abhishek Biswas on 25/04/24.
//

import UIKit

class NotesListViewController: UIViewController {

private var noteArray : [NotesModel] = []
var buttonCallBack : ((UIButton) -> Void)?
var selectedIndex : Int?

private lazy var tableView : UITableView = {
let tableView = UITableView()
tableView.separatorStyle = .none
return tableView
}()

private lazy var floatingIndicatorView : UIView = {
let view = UIView()
view.backgroundColor = .clear
let button = UIFactoryClass.makeCustomButton(buttonTitle: "")
button.setImage(UIImage.init(systemName: "plus"), for: .normal)
button.tintColor = .white
button.addTarget(self, action: #selector(addBtnAction(_ :)), for: .touchUpInside)
view.addSubview(button)
NSLayoutConstraint.activate([
button.widthAnchor.constraint(equalToConstant: 70),
button.heightAnchor.constraint(equalToConstant: 70),
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 20),
button.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -10)
])
button.layer.cornerRadius = 35
return view
}()


private lazy var appBarView : UIView = {
let uiView = UIView()
uiView.backgroundColor = UIColor(red: 146/255, green: 150/255, blue: 206/255, alpha: 1.0)

let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 10
stackView.alignment = .trailing
stackView.translatesAutoresizingMaskIntoConstraints = false

let titleLabel = UILabel()
titleLabel.text = "TODO APP"
titleLabel.font = UIFont.systemFont(ofSize: 25, weight: .bold)
titleLabel.textColor = UIColor.white

self.buttonCallBack = { button in
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(button)
}
uiView.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: uiView.topAnchor, constant: 50),
stackView.leadingAnchor.constraint(equalTo: uiView.leadingAnchor, constant: 10),
stackView.trailingAnchor.constraint(equalTo: uiView.trailingAnchor, constant: -10),
stackView.bottomAnchor.constraint(equalTo: uiView.bottomAnchor, constant: -10)
])
uiView.isUserInteractionEnabled = true
return uiView
}()

override func viewDidLoad() {
super.viewDidLoad()
fetchModelFromDatabase()
self.tableView.backgroundColor = UIColor(red: 214/255, green: 215/255, blue: 237/255, alpha: 1)
self.view.addSubview(self.tableView)
self.view.addSubview(self.floatingIndicatorView)
self.view.addSubview(self.appBarView)
self.appBarView.translatesAutoresizingMaskIntoConstraints = false
self.tableView.translatesAutoresizingMaskIntoConstraints = false
self.floatingIndicatorView.translatesAutoresizingMaskIntoConstraints = false

let compltedTaskBtn = UIButton()
compltedTaskBtn.setTitle("", for: .normal)
compltedTaskBtn.frame.size.height = 55
compltedTaskBtn.frame.size.width = 55
compltedTaskBtn.imageView?.layer.transform = CATransform3DMakeScale(1.4, 1.4, 1.4)
compltedTaskBtn.tintColor = UIColor.white
compltedTaskBtn.setImage(UIImage.init(systemName: "checklist.checked"), for: .normal)
compltedTaskBtn.addTarget(self, action: #selector(compltedTaskBtnTapped(_ :)), for: .touchUpInside)
self.buttonCallBack?(compltedTaskBtn)
NSLayoutConstraint.activate([
self.appBarView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.appBarView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
self.appBarView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
self.appBarView.heightAnchor.constraint(equalToConstant: 120),

// Constraints for tableView
self.tableView.topAnchor.constraint(equalTo: self.appBarView.bottomAnchor),
self.tableView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
self.tableView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
self.tableView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),

// Constraints for floatingIndicatorView
self.floatingIndicatorView.widthAnchor.constraint(equalToConstant: 70),
self.floatingIndicatorView.heightAnchor.constraint(equalToConstant: 70),
self.floatingIndicatorView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor, constant: -35),
self.floatingIndicatorView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -10)
])
self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.register(NoteTableViewCell.nib(), forCellReuseIdentifier: NoteTableViewCell.cellId)
}

func fetchModelFromDatabase() {
if let noteLArray = CoreDataHelpers.shared.fetchAllNotesFromDatabase() {
self.noteArray = noteLArray
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}

@objc func addBtnAction(_ sender: UIButton) {
self.moveToAddAndEditVC(model: nil)
}


func moveToAddAndEditVC(model: NotesModel?) {
let addAndEdit = AddEditNoteController()
addAndEdit.currentEditingState = false
addAndEdit.appTitleName = "Add Task"
addAndEdit.delegate = self
if model != nil{
guard let model = model else {return}
addAndEdit.currentEditingState = true
addAndEdit.appTitleName = "Edit Task"
addAndEdit.noteModel = model
}

addAndEdit.modalPresentationStyle = .fullScreen
self.present(addAndEdit, animated: true)
}


@objc func compltedTaskBtnTapped(_ sender: UIButton){
var vcCompleted = NoteCompleteViewController()
vcCompleted.modalPresentationStyle = .fullScreen


self.present(vcCompleted, animated: true, completion: nil)
}
}

extension NotesListViewController : UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return noteArray.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: NoteTableViewCell.cellId, for: indexPath) as? NoteTableViewCell
cell?.titleLabel.text = self.noteArray[indexPath.row].noteName
cell?.descLabel.text = self.noteArray[indexPath.row].noteDesc

cell?.editBtn.tag = indexPath.row
cell?.deleteBtn.tag = indexPath.row
cell?.completedBtn.tag = indexPath.row
cell?.completedBtn.addTarget(self, action: #selector(updateModelAction(_ :)), for: .touchUpInside)
cell?.deleteBtn.addTarget(self, action: #selector(deleteModelAction(_ :)), for: .touchUpInside)
cell?.editBtn.addTarget(self, action: #selector(completeModelAction(_ :)), for: .touchUpInside)
return cell ?? UITableViewCell()
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
self.selectedIndex = indexPath.row
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}

@objc func deleteModelAction(_ sender: UIButton){
let selectedIndex = sender.tag
let model = self.noteArray[selectedIndex]
self.noteArray.remove(at: selectedIndex)
CoreDataHelpers.shared.deleteNoteInDatabase(model: model)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}

@objc func completeModelAction(_ sender: UIButton){
self.makeAlert(title: "Alert", message: "Do you want to make the current status as complete?", isCancelNeed: true) {
let selectedIndex = sender.tag
self.noteArray[selectedIndex].isCompleted = true
let model = self.noteArray[selectedIndex]
self.noteArray.remove(at: selectedIndex)
CoreDataHelpers.shared.updateNoteFromID(newModel: model)

DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}

@objc func updateModelAction(_ sender: UIButton){
let selectedIndex = sender.tag
self.moveToAddAndEditVC(model: self.noteArray[selectedIndex])
}
}


extension NotesListViewController : AddEditNoteControllerDelegate {
func uploadMainTableView(model: NotesModel) {
self.noteArray.append(model)
DispatchQueue.main.async {
self.tableView.reloadData()
}
}

func updateModel(model: NotesModel) {
for item in self.noteArray {
if item.noteId == model.noteId {
item.noteName = model.noteName
item.isCompleted = model.isCompleted
item.noteDesc = model.noteDesc
}
}

DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}

Completed Task Screen

The NoteCompleteViewController orchestrates the presentation of completed tasks within the ToDo app. Upon initialization, it retrieves the list of completed tasks from Core Data through the fetchDataFromCoreData method, ensuring that the view is populated with the latest data upon loading. The user interface consists of a table view, wherein each cell represents a completed task, displaying its title and description.

The table view’s delegate and data source are configured within the view controller to handle the rendering of task data. Cells are dynamically sized to maintain a consistent layout, with each task occupying a vertical space of 120 points. The absence of actionable buttons within the cell indicates that completed tasks are not subject to further modification, fostering a clear and concise presentation of task information.

Additionally, the view controller includes an app bar at the top, housing a title indicative of the screen’s purpose and a back button. This back button enables users to seamlessly navigate back to the previous screen, facilitating an intuitive user experience. Overall, the NoteCompleteViewController streamlines the presentation of completed tasks, leveraging Core Data to ensure data persistence and providing users with a straightforward interface for reviewing their accomplished endeavors.

//
// NoteCompleteViewController.swift
// TodoApp
//
// Created by Abhishek Biswas on 26/04/24.
//

import UIKit

class NoteCompleteViewController: UIViewController {

var noteArray : [NotesModel] = []
var appTitleName : String?

private lazy var tableView : UITableView = {
var tableView = UITableView()
tableView.separatorStyle = .none
return tableView
}()

private lazy var appBarView : UIView = {
let uiView = UIView()
uiView.backgroundColor = UIColor(red: 146/255, green: 150/255, blue: 206/255, alpha: 1.0)

let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 10
stackView.alignment = .fill
stackView.translatesAutoresizingMaskIntoConstraints = false

let titleLabel = UILabel()
titleLabel.text = "Completed Task"
titleLabel.font = UIFont.systemFont(ofSize: 25, weight: .bold)
titleLabel.textColor = UIColor.white
titleLabel.textAlignment = .left

let backButton = UIButton()
backButton.translatesAutoresizingMaskIntoConstraints = false
backButton.setTitle("", for: .normal)
backButton.tintColor = UIColor.white
backButton.setImage(UIImage(systemName: "arrow.left"), for: .normal)
backButton.setTitleColor(UIColor.white, for: .normal)
backButton.addTarget(self, action: #selector(backButtonTapped(_:)), for: .touchUpInside)
backButton.imageView?.layer.transform = CATransform3DMakeScale(1.4, 1.4, 1.4)
NSLayoutConstraint.activate([
backButton.widthAnchor.constraint(equalToConstant: 45),
backButton.heightAnchor.constraint(equalToConstant: 45),
])

stackView.addArrangedSubview(backButton)
stackView.addArrangedSubview(titleLabel)

uiView.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: uiView.topAnchor, constant: 50),
stackView.leadingAnchor.constraint(equalTo: uiView.leadingAnchor, constant: 5),
stackView.trailingAnchor.constraint(equalTo: uiView.trailingAnchor, constant: -5),
stackView.bottomAnchor.constraint(equalTo: uiView.bottomAnchor, constant: -10)
])
uiView.isUserInteractionEnabled = true
return uiView
}()

override func viewDidLoad() {
super.viewDidLoad()
fetchDataFromCoreData()
self.view.addSubview(self.tableView)
self.view.addSubview(self.appBarView)
self.appBarView.translatesAutoresizingMaskIntoConstraints = false
self.tableView.translatesAutoresizingMaskIntoConstraints = false

self.tableView.delegate = self
self.tableView.dataSource = self
self.tableView.register(NoteTableViewCell.nib(), forCellReuseIdentifier: NoteTableViewCell.cellId)
self.tableView.backgroundColor = UIColor(red: 214/255, green: 215/255, blue: 237/255, alpha: 1)
NSLayoutConstraint.activate([
self.appBarView.topAnchor.constraint(equalTo: self.view.topAnchor),
self.appBarView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
self.appBarView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
self.appBarView.heightAnchor.constraint(equalToConstant: 120),
self.tableView.topAnchor.constraint(equalTo: self.appBarView.bottomAnchor),
self.tableView.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor),
self.tableView.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor),
self.tableView.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
])
}

private func fetchDataFromCoreData() {
self.noteArray = CoreDataHelpers.shared.getAllCompletedTasksFromCoreData()
DispatchQueue.main.async {
self.tableView.reloadData()
}
}

@objc func backButtonTapped(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}
}

extension NoteCompleteViewController : UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: NoteTableViewCell.cellId, for: indexPath) as? NoteTableViewCell
cell?.titleLabel.text = self.noteArray[indexPath.row].noteName
cell?.descLabel.text = self.noteArray[indexPath.row].noteDesc
cell?.buttonStack.isHidden = true
return cell ?? UITableViewCell()
}


func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return noteArray.count
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 120
}
}

ToDo Model (aka NotesModel)

The NotesModel class encapsulates the properties and behavior of individual notes within the ToDo app. Each note is characterized by a unique identifier (noteId), a title (noteName), a description (noteDesc), and a completion status (isCompleted). Upon initialization, a note's identifier, title, and description are provided, with the completion status defaulting to false.

This model facilitates the organization and management of tasks within the app, allowing for the creation, modification, and tracking of individual notes. By maintaining a clear separation of concerns, the NotesModel class promotes code readability and maintainability, ensuring that the logic governing note-related operations remains cohesive and scalable.

Furthermore, the inclusion of a completion status enables users to differentiate between pending and completed tasks, providing valuable context within the app’s user interface. Through its structured representation of note data, the NotesModel class serves as a foundational element in the implementation of the ToDo app, supporting a seamless user experience and efficient task management.

//
// NotesModel.swift
// TodoApp
//
// Created by Abhishek Biswas on 25/04/24.
//

import Foundation

class NotesModel {
var noteId : String
var noteName : String
var noteDesc : String
var isCompleted : Bool = false


init(noteId: String, noteName: String, noteDesc: String) {
self.noteId = noteId
self.noteName = noteName
self.noteDesc = noteDesc
}
}

Core Data Helper

The CoreDataHelpers class acts as a vital intermediary for handling interactions with Core Data within the ToDo app. Implements a singleton pattern, ensuring a singular instance accessible throughout the application via the shared static property. Central to its functionality are methods for saving, fetching, updating, and deleting notes stored in Core Data.

For saving notes, the saveNotesInDataBase method accepts a NotesModel object, extracting its pertinent details such as identifier, title, description, and completion status. These details are then stored within the appropriate Core Data entity, NotesEntity, facilitating data persistence.

When it comes to retrieving notes, the fetchAllNotesFromDatabase method is responsible for fetching all pending (non-completed) notes from Core Data. It constructs an array of NotesModel instances based on the fetched Core Data entities, ensuring only pending notes are included for display.

For updating existing notes, the updateNoteFromID method targets a specific note within Core Data using its unique identifier. It then updates the note's properties with new data provided by a NotesModel object, subsequently saving the changes back to Core Data.

Similarly, the deleteNoteInDatabase method handles the deletion of notes from Core Data based on their unique identifier. Upon locating the note using its identifier, it removes the note from the context and saves the changes, effectively deleting the note from Core Data.

Lastly, the getAllCompletedTasksFromCoreData method retrieves all completed notes from Core Data. It constructs an array of NotesModel instances representing completed tasks, ensuring that only completed notes are included for display within the app.

Overall, the CoreDataHelpers class abstracts the intricacies of Core Data operations, providing a streamlined and reusable interface for managing note data within the ToDo app. Its methods facilitate efficient data manipulation, ensuring seamless task management and persistence throughout the application.

//
// CoreDataHelpers.swift
// TodoApp
//
// Created by Abhishek Biswas on 26/04/24.
//

import CoreData
import UIKit


class CoreDataHelpers {
static let shared = CoreDataHelpers()

func saveNotesInDataBase(model: NotesModel) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
print("AppDelegate is not accessible")
return
}

let context = appDelegate.persistentContainer.viewContext
let noteOffline = NotesEntity(context: context)

noteOffline.note_id = model.noteId
noteOffline.note_name = model.noteName
noteOffline.node_desc = model.noteDesc
noteOffline.node_is_completed = model.isCompleted
do {
try context.save()
print("Note saved successfully")
} catch {
print("Failed to save note: \(error.localizedDescription)")
}
}



func fetchAllNotesFromDatabase() -> [NotesModel]? {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
print("AppDelegate is not accessible")
return nil
}

let context = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<NotesEntity> = NotesEntity.fetchRequest()

do {
let fetchedNotes = try context.fetch(fetchRequest)
var notesModelArray = [NotesModel]()
for note in fetchedNotes {
if (note.node_is_completed == false) {
let noteModel = NotesModel(noteId: note.note_id ?? "", noteName: note.note_name ?? "", noteDesc: note.node_desc ?? "")
noteModel.isCompleted = note.node_is_completed
notesModelArray.append(noteModel)
}
}

return notesModelArray
} catch {
print("Failed to fetch notes: \(error.localizedDescription)")
return nil
}
}


func updateNoteFromID(newModel: NotesModel) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
print("AppDelegate is not accessible")
return
}

let context = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<NotesEntity> = NotesEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "note_id = %@", newModel.noteId)

do {
let fetchedNotes = try context.fetch(fetchRequest)
if let noteToUpdate = fetchedNotes.first {
noteToUpdate.note_name = newModel.noteName
noteToUpdate.node_desc = newModel.noteDesc
noteToUpdate.node_is_completed = newModel.isCompleted

try context.save()
print("Note updated successfully")
} else {
print("No note found with ID: \(newModel.noteId)")
}
} catch {
print("Failed to update note: \(error.localizedDescription)")
}
}

func deleteNoteInDatabase(model: NotesModel) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
print("AppDelegate is not accessible")
return
}

let context = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<NotesEntity> = NotesEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "note_id = %@", model.noteId)

do {
let fetchedNotes = try context.fetch(fetchRequest)
if let noteToDelete = fetchedNotes.first {
context.delete(noteToDelete)

try context.save()
print("Note deleted successfully")
} else {
print("No note found with ID: \(model.noteId)")
}
} catch {
print("Failed to delete note: \(error.localizedDescription)")
}
}



func getAllCompletedTasksFromCoreData() -> [NotesModel] {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
print("AppDelegate is not accessible")
return []
}
let context = appDelegate.persistentContainer.viewContext
let fetchRequest: NSFetchRequest<NotesEntity> = NotesEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "node_is_completed = YES")

do {
let fetchedNotes = try context.fetch(fetchRequest)
var notesModelArray = [NotesModel]()
for note in fetchedNotes {
let noteModel = NotesModel(noteId: note.note_id ?? "", noteName: note.note_name ?? "", noteDesc: note.node_desc ?? "")
noteModel.isCompleted = note.node_is_completed
notesModelArray.append(noteModel)
}
return notesModelArray
} catch {
print("Failed to fetch notes: \(error.localizedDescription)")
return []
}
}
}

NoteTableViewCell (aka ToDo Cell)

The NoteTableViewCell class represents a custom table view cell used to display individual notes within the ToDo app. It features several UI elements, including labels for the note's title and description (titleLabel and descLabel), as well as buttons for completing, deleting, and editing the note (completedBtn, deleteBtn, and editBtn). Additionally, it includes two views, topView and bottomView, for layout purposes, and a nDView representing the container for the note's content.

Upon initialization, the cell’s UI components are configured within the awakeFromNib() method. The background color of the cell's content view, topView, and bottomView is set to a consistent shade of light blue to maintain visual cohesion. Furthermore, the nDView is stylized with rounded corners and a subtle shadow effect, enhancing the visual presentation of each note within the table view.

The NoteTableViewCell class provides a reusable for displaying ToDo (notes) within the app, ensuring consistency and clarity in the user interface. By encapsulating the visual layout and styling of individual note cells, it contributes to a cohesive and user-friendly experience for managing tasks in the ToDo app.

//
// NoteTableViewCell.swift
// TodoApp
//
// Created by Abhishek Biswas on 25/04/24.
//

import UIKit


class NoteTableViewCell: UITableViewCell {

@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var descLabel: UILabel!
@IBOutlet weak var completedBtn: UIButton!
@IBOutlet weak var deleteBtn: UIButton!
@IBOutlet weak var editBtn: UIButton!
@IBOutlet weak var topView: UIView!
@IBOutlet weak var bottomView: UIView!
@IBOutlet weak var nDView: UIView!


static let cellId = "NoteTableViewCell"
static func nib() -> UINib {
return UINib(nibName: "NoteTableViewCell", bundle: nil)
}

override func awakeFromNib() {
super.awakeFromNib()
self.contentView.backgroundColor = UIColor(red: 214/255, green: 215/255, blue: 237/255, alpha: 1)
self.topView.backgroundColor = UIColor(red: 214/255, green: 215/255, blue: 237/255, alpha: 1)
self.bottomView.backgroundColor = UIColor(red: 214/255, green: 215/255, blue: 237/255, alpha: 1)
self.nDView.layer.cornerRadius = 15
self.nDView.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.25).cgColor
self.nDView.layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
self.nDView.layer.shadowOpacity = 1.0
self.nDView.layer.shadowRadius = 0.0
self.nDView.layer.masksToBounds = false
self.nDView.layer.cornerRadius = 10.0
}

@IBOutlet weak var buttonStack: UIStackView!

override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)

// Configure the view for the selected state
}

}

Add Notes screen

The AddEditNoteController class serves as a view controller responsible for adding or editing notes within the ToDo app. It features UI elements such as text fields for entering the note's title and details (titleTextField and descTxtField) and a button for submitting the note (submitButton). Additionally, it includes an app bar (appBarView) for displaying the title of the screen and navigation controls.

Upon initialization, the view controller configures its UI components and layout constraints within the viewDidLoad() method. Depending on whether the controller is in editing mode (currentEditingState), it either displays the "Add" button for creating a new note or the "Update" and "Cancel" buttons for modifying an existing note. If in editing mode, the text fields are populated with the existing note's title and details.

The class implements actions for handling user interactions, such as tapping the submit button to save a new note (submitButtonTapped(_:)), navigating back to the previous screen (backButtonTapped(_:)), updating an existing note (updateAction(_:)), and canceling the editing process (cancelAction(_:)). These actions involve validating user input, interacting with the data model (NotesModel), and utilizing the CoreDataHelpers class to save, update, or delete notes in the Core Data database.

import UIKit


protocol AddEditNoteControllerDelegate : AnyObject {
func uploadMainTableView(model : NotesModel)
func updateModel(model: NotesModel)
}

class AddEditNoteController: UIViewController {

private lazy var titleTextField: UITextField = {
let txtField = UIFactoryClass.makeCustomTxtField(hintString: "Title")
txtField.translatesAutoresizingMaskIntoConstraints = false
return txtField
}()

private lazy var descTxtField: UITextField = {
let txtField = UIFactoryClass.makeCustomTxtField(hintString: "Details")
txtField.translatesAutoresizingMaskIntoConstraints = false
return txtField
}()

private lazy var submitButton: UIButton = {
let button = UIFactoryClass.makeCustomButton(buttonTitle: "Add")
button.addTarget(self, action: #selector(submitButtonTapped(_ :)), for: .touchUpInside)
return button
}()

public var appTitleName : String?
weak var delegate : AddEditNoteControllerDelegate?
public var noteModel : NotesModel?
var buttonCallBack : ((UIButton) -> Void)?

private lazy var appBarView : UIView = {
let uiView = UIView()
uiView.backgroundColor = UIColor(red: 146/255, green: 150/255, blue: 206/255, alpha: 1.0)

let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 10
stackView.alignment = .center
stackView.translatesAutoresizingMaskIntoConstraints = false

let titleLabel = UILabel()
titleLabel.text = self.appTitleName ?? ""
titleLabel.font = UIFont.systemFont(ofSize: 25, weight: .bold)
titleLabel.textColor = UIColor.white

self.buttonCallBack = { button in
stackView.addArrangedSubview(button)
stackView.addArrangedSubview(titleLabel)
}
uiView.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.topAnchor.constraint(equalTo: uiView.topAnchor, constant: 50),
stackView.leadingAnchor.constraint(equalTo: uiView.leadingAnchor, constant: 10),
stackView.trailingAnchor.constraint(equalTo: uiView.trailingAnchor, constant: -10),
stackView.bottomAnchor.constraint(equalTo: uiView.bottomAnchor, constant: -10)
])
uiView.isUserInteractionEnabled = true
return uiView
}()

public var currentEditingState : Bool = false

private lazy var makeUpdateAndCancel : UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = 25
stackView.alignment = .fill
let updateBtn = UIFactoryClass.makeCustomButton(buttonTitle: "Update")
updateBtn.addTarget(self, action: #selector(updateAction(_ :)), for: .touchUpInside)
let cancelBtn = UIFactoryClass.makeCustomButton(buttonTitle: "Cancel")
cancelBtn.addTarget(self, action: #selector(cancelAction(_ :)), for: .touchUpInside)
stackView.addArrangedSubview(updateBtn)
stackView.addArrangedSubview(cancelBtn)
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
self.appBarView.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(titleTextField)
self.view.addSubview(descTxtField)
self.view.addSubview(submitButton)
self.view.addSubview(self.appBarView )
self.view.addSubview(makeUpdateAndCancel)


let backButton = UIButton()
backButton.setTitle("", for: .normal)
backButton.frame.size.height = 45
backButton.tintColor = UIColor.white
backButton.setImage(UIImage.init(systemName: "arrow.left"), for: .normal)
backButton.setTitleColor(UIColor.white, for: .normal)
backButton.addTarget(self, action: #selector(backButtonTapped(_ :)), for: .touchUpInside)
backButton.imageView?.layer.transform = CATransform3DMakeScale(1.4, 1.4, 1.4)

self.buttonCallBack?(backButton)
if (!self.currentEditingState){
self.makeUpdateAndCancel.isHidden = true
}else{
if let model = self.noteModel {
self.titleTextField.text = model.noteName
self.descTxtField.text = model.noteDesc
}
self.submitButton.isHidden = true
}

NSLayoutConstraint.activate([
appBarView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0),
appBarView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0),
appBarView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0),
appBarView.heightAnchor.constraint(equalToConstant: 110),

titleTextField.topAnchor.constraint(equalTo: appBarView.bottomAnchor, constant: 20),
titleTextField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
titleTextField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
titleTextField.heightAnchor.constraint(equalToConstant: 50),

descTxtField.topAnchor.constraint(equalTo: titleTextField.bottomAnchor, constant: 20),
descTxtField.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
descTxtField.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
descTxtField.heightAnchor.constraint(equalToConstant: 50),

submitButton.topAnchor.constraint(equalTo: descTxtField.bottomAnchor, constant: 25),
submitButton.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20),
submitButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
submitButton.heightAnchor.constraint(equalToConstant: 50),


(!currentEditingState) ? makeUpdateAndCancel.topAnchor.constraint(equalTo: submitButton.bottomAnchor, constant: 25) : makeUpdateAndCancel.topAnchor.constraint(equalTo: descTxtField.bottomAnchor, constant: 25),
(!currentEditingState) ? makeUpdateAndCancel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20) : makeUpdateAndCancel.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 25),
(!currentEditingState) ? makeUpdateAndCancel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20) : makeUpdateAndCancel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20),
makeUpdateAndCancel.heightAnchor.constraint(equalToConstant: 50)
])
titleTextField.addBottomBorder(color: .gray)
descTxtField.addBottomBorder(color: .gray)
}


@objc func submitButtonTapped(_ sender: UIButton) {
guard let titleTxtField = self.titleTextField.text, !titleTxtField.isEmpty else {
self.makeAlert(title: "Alert", message: "Title cannot be blank", isCancelNeed: false, onTap: nil)
return
}

guard let descTxtField = self.descTxtField.text, !descTxtField.isEmpty else {
self.makeAlert(title: "Alert", message: "Details cannot be blank", isCancelNeed: false, onTap: nil)
return
}
if let uuidV1 = UUID.timeBasedUUID()?.uuidString {
self.noteModel = NotesModel(noteId: uuidV1, noteName: titleTxtField, noteDesc: descTxtField)
if let model = self.noteModel {
self.delegate?.uploadMainTableView(model: model)
CoreDataHelpers.shared.saveNotesInDataBase(model: model)
}
}
self.dismiss(animated: true) {
self.titleTextField.text = ""
self.descTxtField.text = ""
}
}



@objc func backButtonTapped(_ sender: UIButton){
self.makeAlert(title: "Alert!!", message: "Do you want to go back!!!!", isCancelNeed: true) {
self.dismiss(animated: true) {
self.titleTextField.text = ""
self.descTxtField.text = ""
}
}
}


@objc func updateAction(_ sender: UIButton){
guard let titleTxtField = self.titleTextField.text, !titleTxtField.isEmpty else {
self.makeAlert(title: "Alert", message: "Title cannot be blank", isCancelNeed: false, onTap: nil)
return
}

guard let descTxtField = self.descTxtField.text, !descTxtField.isEmpty else {
self.makeAlert(title: "Alert", message: "Details cannot be blank", isCancelNeed: false, onTap: nil)
return
}

if let model = self.noteModel {
model.noteName = titleTxtField
model.noteDesc = descTxtField
CoreDataHelpers.shared.updateNoteFromID(newModel: model)
self.delegate?.updateModel(model: model)
}
self.dismiss(animated: true) {
self.titleTextField.text = ""
self.descTxtField.text = ""
}
}

@objc func cancelAction(_ sender: UIButton){
self.makeAlert(title: "Alert!!", message: "Do you want to Cancel the current Editing", isCancelNeed: true) {
self.dismiss(animated: true) {
self.titleTextField.text = ""
self.descTxtField.text = ""
}
}
}
}

Thanks for reading! My name is Abhishek, I have an passion of building app and learning new technology.

--

--