DEV Community

OLAWOYE OMOTAYO
OLAWOYE OMOTAYO

Posted on

Create a Note App with Swift and Core Data

In this tutorial, I’m going to walk you through how to create a note iOS app.

What you’ll learn:

Create UIKit layout programmatically
Data Persistence with Core data - Create, Read, Update and Delete notes.
Dismiss presented view controller when UIAlertAction is tapped.

Let’s Get Started!

Create A New Project

In your Main.storyboard file, Embed the View Controller inside a Navigation View Controller.

Embed View Controller inside a Navigation Controller

Click on the Navigation bar of the Navigation View Controller and toggle the Inspection Pane Open and under the style option, check prefer large titles.
Importantly, In the inspector pane, make sure the Navigation Controller has “Is Initial View Controller” option checked.

Set Large title

Now, let’s rename our default view controller created for us to NotesViewController To do that, Option click the ViewController class name as follows:

Rename View Controller

Inside our viewDidLoad method, let give the NotesViewController a title and a RightBarButtonItem to add new notes:

       title = "Notes"
       navigationItem.rightBarButtonItem = UIBarButtonItem(
            barButtonSystemItem: .add, 
            target: self, 
            action: #selector(didTapAddButton)
       )
Enter fullscreen mode Exit fullscreen mode

Note let’s create a private @objc function to handle our bar button item.

    @objc
    private func didTapAddButton() {

    }
Enter fullscreen mode Exit fullscreen mode

Let's run our app to see our progress. It should look something like this:

First App Run

You should see either of the screens above depending on whether your device is on light or dark mode. Cool

Now let’s go ahead to create the Add Note ViewController. To do that, create a new Cocoa Touch Class. Make sure not to check the “Also create XIB file” option, unless you’re confident about your Interface Builder skills.

Create AddNoteViewController

Now set the background colour for the AddNoteViewController to .systemBackground in side the viewDidLoad method as follows:

view.backgroundColor = .systemBackground

Without that, the view controller would be transparent and you wouldn't know it's presented.

Great! Now let's go back to the NotesViewController and add the following code snippet to the private method we created, didTapAddButton:

let addNoteVC = AddNoteViewController()
let navVC = UINavigationController(rootViewController: addNoteVC)
navVC.navigationBar.prefersLargeTitles = true
navVC.modalPresentationStyle = .formSheet
present(navVC, animated: true, completion: nil)
Enter fullscreen mode Exit fullscreen mode

Don't worry too much about the NavigationController. I added it because I wanted the view controller to have a Navigation bar.

Next thing is to create views on the add note view controller, but before that, let's create some convenience properties and method as an extension to UIView

Create a new swift file named Extensions and add the following:

    import UIKit

    extension UIView {
        /// Weight of view
        var width: CGFloat {
            frame.size.width
        }

        /// Height of view
        var height: CGFloat {
            frame.size.height
        }

        /// Left edge of view
        var left: CGFloat {
            frame.origin.x
        }

        /// Right edge of view
        var right: CGFloat {
            left + width
        }

        /// Top edge of view
        var top: CGFloat {
            frame.origin.y
        }

        /// Bottom edge of view
        var bottom: CGFloat {
            top + height
        }

        func addSubViews(views: UIView...) {
            for view in views {
                self.addSubview(view)
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Now let’s create our views inside Add Note ViewController:

First add the title textField as an anonymous closure.

private var titleField: UITextField = {
    let field = UITextField()
    field.placeholder = "Title"
    field.textColor = .label
    field.font = UIFont.systemFont(ofSize: 20, weight:.medium)
    return field
}()
Enter fullscreen mode Exit fullscreen mode

Then add the body text view.

    private var bodyTextView: UITextView = {
        let view = UITextView()
        view.text = "Type in here..."
        view.font = UIFont.systemFont(ofSize: 18)
        view.textColor = .placeholderText
        view.clipsToBounds = true
        return view
    }()

Enter fullscreen mode Exit fullscreen mode

Now override the viewWillLayoutSubviews by defining frame for our views in the view controller’s view.

First add the views as subviews to the controller's main view as follows:

view.addSubViews(views: titleField, bodyTextView)

Remember we created a convenience method as an extension of UIView called addSubviews() which can take a varying number of views instead of calling addSubview multiple times.

titleField.frame = CGRect(
     x: 20, 
     y: 120, 
     width: view.width - 40, 
     height: 44
)
bodyTextView.frame = CGRect(
     x: 16, 
     y: titleField.bottom + 20, 
     width: view.width - 32, 
     height: view.bottom - 250
)
Enter fullscreen mode Exit fullscreen mode

Now add the following inside viewDidLoad

    title = "Add Note"
    navigationItem.rightBarButtonItem = UIBarButtonItem(
            title: "Save", 
            style: .done, 
            target: self, 
            action: #selector(didTapSaveButton)
    )
    bodyTextView.delegate = self
    titleField.delegate = self
Enter fullscreen mode Exit fullscreen mode

We have now added a rightBarButtonItem to the navigation bar which we’ll tap to save the note.

Now create the didTapSaveButton method:

@objc
private func didTapSaveButton() {

}
Enter fullscreen mode Exit fullscreen mode

Your editor pane would be showing some errors right now, because we need to conform to the UITextFieldDelegate and the UITextViewDelegate

Now create an extension of the AddNoteViewController at the bottom of the screen outside the AddNoteViewController class as follows:

extension AddNoteViewController: UITextFieldDelegate, UITextViewDelegate {

}
Enter fullscreen mode Exit fullscreen mode

Now when you build and run, we should have something that looks as follows:

Add Note VC

Now inside the extension, let implement the delegate functions as follows:

    func textFieldDidEndEditing(_ textField: UITextField) {
        titleField.resignFirstResponder()
        if textField == titleField && 
            !titleField.text!.isEmpty {
            bodyTextView.becomeFirstResponder()
        }
    }

    func textFieldShouldReturn(_ textField: UITextField) -> 
        Bool {
        titleField.resignFirstResponder()
        bodyTextView.becomeFirstResponder()
        return true
    }

    func textViewDidBeginEditing(_ textView: UITextView) {
        if textView == bodyTextView && 
           bodyTextView.text == "Type in here..." {
            textView.text = ""
            bodyTextView.textColor = .label
        }
    }

    func textViewDidEndEditing(_ textView: UITextView) {
        if textView == bodyTextView && 
           bodyTextView.text.isEmpty {
            textView.text = "Type in here..."
            bodyTextView.textColor = .placeholderText
        }
    }
Enter fullscreen mode Exit fullscreen mode

So far we've been working on the UI, now to the part we've been waiting for.

PERSISTING DATA TO CORE DATA.

Core data is Apples framework for managing object graph and persisting data to disk. Although core data can persist data to disk, it is mainly for managing object graph in your application.

To use core data in our app, we need to first create a “Data Model file”. This is to define the structure of the necessary objects.
Remember when creating our project, we had the “Use core data” Option checked which creates this Data model file for us by default.

Data Model File

Now click on the Add Entity button and rename that to Note. Afterwards add attributes (with names and types) by clicking the plus button.
Lastly, toggle open your inspector pane AND CHANGE THE “Codegen” Option to “Manual/None” because we’ll like to create our model class ourselves.
You’ll see the reason for this in a sec.

Now let’s create a swift file named “model”. You could name it anything, I’m just naming it that since there’re not too many model in this sample app.
Inside the model file, let’s create an Enum and a class for our entity.

    import CoreData

    enum Section: Hashable {
        case main
    }

    @objc(Note)
    public class Note: NSManagedObject {

        @NSManaged public var title: String
        @NSManaged public var body: String
        @NSManaged public var created: Date

    }

    extension Note {
        @nonobjc public class func fetchRequest() -> 
             NSFetchRequest<Note> {
            NSFetchRequest<Note>(entityName: "Note")
        }
    }
Enter fullscreen mode Exit fullscreen mode

NSManagedObject is like an instance of your Entity (which can be liken to a class).

We’ll be using a Diffable DataSource and UICollectionView later on in our NotesViewController. The enum represents our section for the CollectionView.

Now that we’ve created an entity, let’s go back to our AddNoteViewController to complete adding notes to our app. Inside your didTapSaveButton method, let add the following snippet:

if titleField.text!.isEmpty || bodyTextView.text.isEmpty {
   let alertController = UIAlertController(
       title: "Fields Required",
       message: "Please enter a title and body for your 
                 note!",
       preferredStyle: .alert
   )

   let cancelAction = UIAlertAction(
       title: "Ok", 
       style: .cancel, 
       handler: nil
   )
   alertController.addAction(cancelAction)

   present(alertController, animated: true)
   return
}

guard let appDelegate = UIApplication.shared.delegate 
     as? AppDelegate else { return }

let managedContext = appDelegate
                        .persistentContainer
                        .viewContext

let note = Note(context: managedContext)

note.title = titleField.text!
note.body = bodyTextView.text
note.created = Date.now

do {
    try managedContext.save()

    let alertController = UIAlertController(
         title: "Note Saved", 
         message: "Note has been saved successfully!", 
         preferredStyle: .alert
    )

    let okayAction = UIAlertAction(
          title: "Ok", 
          style: .cancel) { [weak self] _ in
       guard let self = self else { return }
       self.dismiss(animated: true) {
           self.dismiss(animated: true, completion: nil)
       }
    }

    alertController.addAction(okayAction)
    present(alertController, animated: true)

} catch let error as NSError {
    fatalError("Error saving person to core data. 
             \(error.userInfo)")
}
Enter fullscreen mode Exit fullscreen mode

Wooops… Great one!

Now when we run our app and tap on the add button to add a note we’ll be able to add a new note and dismiss on success.

One last thing before we go back to our Home Screen (NotesViewController) which shows a list of all the notes we’ve added.

Create a custom delegate to notify the NotesViewController that we’ve successfully added a new note.

At the very top of the AddNoteViewController, just before the class definition, Add the following snippet:

    protocol AddNoteViewControllerDelegate {
        func didFinishAddingNote()
    }
Enter fullscreen mode Exit fullscreen mode

Now, inside the AddNoteViewController, let’s create delegate property and call the delegate function inside the okayAction of the didTapSaveButton method as follows:

class AddNoteViewController: UIViewController {

    var delegate: AddNoteViewControllerDelegate?

    ...

    @objc
    private func didTapSaveButton() {       
      ...
      let okayAction = UIAlertAction(
        title: "Ok",
        style: .cancel) { [weak self] _ in
            guard let self = self else { return }

            self.delegate?.didFinishAddingNote() 

            self.dismiss(animated: true) {
                self.dismiss(animated: true, completion: nil)
            }
        }
        ...
    }    
}
Enter fullscreen mode Exit fullscreen mode

WORKING WITH UICOLLECTIONVIEW LIST LAYOUT

Inside our NotesViewController, let add the following:

private var dataSource: 
    UICollectionViewDiffableDataSource<Section, Note>! = nil
private var notesCollectionView: UICollectionView! = nil    
private var notes: [Note]?

Enter fullscreen mode Exit fullscreen mode

Remember we create the Section and Note model, in our model.swift file.
Now, let configure the Collection View we’ve created by adding the following:

private func createLayout() -> UICollectionViewLayout {
    let config = UICollectionLayoutListConfiguration(
                    appearance: .plain)
    return UICollectionViewCompositionalLayout.list(
                    using: config)
}

private func configureCollectionView() {
    notesCollectionView = UICollectionView(
              frame: view.bounds, 
              collectionViewLayout: createLayout()
    )
    notesCollectionView.autoresizingMask = 
              [.flexibleWidth, .flexibleHeight]
    view.addSubview(notesCollectionView)
    notesCollectionView.delegate = self
}
Enter fullscreen mode Exit fullscreen mode

Now you need to conform to the UICollectionViewDelegate protocol, add the following outside the class definition of the NotesViewController:

extension NotesViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, 
        didSelectItemAt indexPath: IndexPath) {      
    }
}
Enter fullscreen mode Exit fullscreen mode

We have to conform to the didSelectItemAt indexPath method to go to the note detail screen.

Now, we’ve successfully configured our collection view, let’s also configure the dataSource. To do that, add the following method to NotesViewController:

private func configureDataSource() {
    let cellRegistration = UICollectionView
        .CellRegistration<UICollectionViewListCell, Note> { 
          cell, indexPath, note in
    var content = cell.defaultContentConfiguration()
    content.text = note.title
    content.textToSecondaryTextVerticalPadding = 8
    content.textProperties.font = UIFont.systemFont(
         ofSize: 18, 
         weight: .medium
    )
    content.textProperties.color = .label
    content.secondaryTextProperties.font = 
                  UIFont.systemFont(ofSize: 16)
    content.secondaryTextProperties.color = .secondaryLabel

    let bodyTextArray = note.body.components(separatedBy: " ")

    if (bodyTextArray.count > 8) {
       var bodyText = bodyTextArray[0...8]
                         .joined(separator: " ")
       bodyText.append("...")
       content.secondaryText = bodyText
    } else {
       content.secondaryText = note.body            
    }

    cell.contentConfiguration = content
    cell.accessories = [.disclosureIndicator()]
  }

dataSource = UICollectionViewDiffableDataSource<Section, Note> 
       (collectionView: notesCollectionView) {
       (collectionView: UICollectionView, 
        indexPath: IndexPath, 
        note: Note) -> UICollectionViewCell? in
    return collectionView.dequeueConfiguredReusableCell(
        using: cellRegistration, 
        for: indexPath, 
        item: note
    )
}

    var snapshot = 
      NSDiffableDataSourceSnapshot<Section, Note>()
     snapshot.appendSections([.main])

    if let notes = notes {
        snapshot.appendItems(notes)
    }
    dataSource.apply(snapshot, animatingDifferences: true)
}
Enter fullscreen mode Exit fullscreen mode

First, we registered the cell for the UICollection View and then configure the data source.

Build and run your app to be sure there’s no error.

Now let add the following two methods to our controller. First to fetch the data from Core Data and the other to update our UI

private func fetchNotes() {
    guard let appDelegate = UIApplication.shared.delegate 
        as? AppDelegate else { return }
    let managedContext = 
        appDelegate.persistentContainer.viewContext

    do {
        notes = try managedContext.fetch(Note.fetchRequest())
    } catch let error as NSError {
       fatalError("Unable to fetch. \(error) = 
              \(error.userInfo)")
    }
}

private func updateCollectionView() {
    guard let notes = notes else {
        return
    }
    var snapshot = dataSource.snapshot()
    snapshot.appendItems(notes)
    dataSource.apply(snapshot, animatingDifferences: true)
}
Enter fullscreen mode Exit fullscreen mode

Great work!

Now let’s call the configureCollectionView and configureDataSource methods from inside viewDidLoad as follows:

 override func viewDidLoad() {
     super.viewDidLoad()

     ...

     configureCollectionView()
     configureDataSource()
 }
Enter fullscreen mode Exit fullscreen mode

Now override the viewWillAppear method and call the fetchNotes method from inside it.

override func viewWillAppear(_ animated: Bool) {
    fetchNotes()
}
Enter fullscreen mode Exit fullscreen mode

Also override the viewDidAppear method and call the updateCollectionView method from inside it.

override func viewDidAppear(_ animated: Bool) {
    updateCollectionView()
}
Enter fullscreen mode Exit fullscreen mode

Great!!!

Now we can run our app. If everything is okay, we should see the following.

NotesViewController showing data

Finally, before we wrap it up for this part 1, remember we created a delegate in our AddNoteViewController which should notify the NotesViewController after adding a new note to the database.

Remember, we navigate to AddNoteVC by calling the didTapAddButton method. Now set the delegate for the “addNoteVC” instance as follows:

    @objc
    private func didTapAddButton() {
        let addNoteVC = AddNoteViewController()
        addNoteVC.delegate = self        
        ...
    }
Enter fullscreen mode Exit fullscreen mode

This will require us to conform to the AddNoteViewControllerDelegate and implement the didFinishAddingNote method as follows:

extension NotesViewController: AddNoteViewControllerDelegate {
    func didFinishAddingNote() {
        fetchNotes()
        updateCollectionView()
    }
}
Enter fullscreen mode Exit fullscreen mode

We again, call the fetchNotes and updateCollectionView methods from inside there.

Great! Now when we create a new note, it immediately update the NotesViewController.

Here's a link to source code on GitHub.

In the Second Part of this article, we’ll cover how to update and delete notes.

Top comments (0)