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!
In your Main.storyboard file, Embed the View Controller inside a Navigation View 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.
Now, let’s rename our default view controller created for us to NotesViewController
To do that, Option click the ViewController class name as follows:
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)
)
Note let’s create a private @objc
function to handle our bar button item.
@objc
private func didTapAddButton() {
}
Let's run our app to see our progress. It should look something like this:
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.
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)
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)
}
}
}
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
}()
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
}()
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
)
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
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() {
}
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 {
}
Now when you build and run, we should have something that looks as follows:
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
}
}
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.
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")
}
}
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)")
}
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()
}
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)
}
}
...
}
}
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]?
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
}
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) {
}
}
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)
}
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)
}
Great work!
Now let’s call the configureCollectionView
and configureDataSource
methods from inside viewDidLoad
as follows:
override func viewDidLoad() {
super.viewDidLoad()
...
configureCollectionView()
configureDataSource()
}
Now override the viewWillAppear
method and call the fetchNotes
method from inside it.
override func viewWillAppear(_ animated: Bool) {
fetchNotes()
}
Also override the viewDidAppear
method and call the updateCollectionView
method from inside it.
override func viewDidAppear(_ animated: Bool) {
updateCollectionView()
}
Great!!!
Now we can run our app. If everything is okay, we should see the following.
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
...
}
This will require us to conform to the AddNoteViewControllerDelegate
and implement the didFinishAddingNote
method as follows:
extension NotesViewController: AddNoteViewControllerDelegate {
func didFinishAddingNote() {
fetchNotes()
updateCollectionView()
}
}
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)