100 Days of Swift – Day 78-80

Reflections on Day 77

I am very satisfied with what I have done yesterday. I loved every bit of the learning process, I loved diving into Apple’s Documentation and trying to understand things myself. I found it very rewarding and I definitely feel that something has stuck in my mind.

In yesterday’s challenges I have tried to stretch myself a bit more and get where it was not needed but I really didn’t like to leave the app in the state where it was at the end. Also, the snooze functionality was not described in the Documentation so I had to delve a bit more into the open web to find a solution … which doesn’t work!

Wait … this morning at 9.30am I got a notification and… yes… it was the Guess the Flag app telling me I had to play. The only issue was that also the Remind me later… action brought me inside the app, which I would have liked to avoid for both challenge 2 and challenge 3. I have some ideas about why this could have happened, but I will probably solve it much later in my development journey.

Tomorrow I will leave for a three-day holiday and I will not bring the computer with me. This will be the first big pause I make in this great path and I sincerely believe it is needed. I agree that one should do something every day, but if you are in any real-life situation that is different from a plain CS Student one, you have to take a break once per week or once every 10-15 days. After that break you will be so much better than before!

My cello teacher used to say that we had to practice cello 6 hours per day, every day, for three months, then take a 7-10 day break and travel somewhere to breath some outstanding work of art and forget about the cello. So yes, that is about 90 days non-stop, but that is what you do all day long, not an extra skill you are adding when you are already dog-tired from the rest of the day.

Anyway, enough talking, I want to try and get Day 74 of the curriculum under my belt before leaving or, as it looks like a Consolidation Day, to do as much as possible before that.

Challenge

This day’s challenge is something I am sure I am going to love: recreate the iOS Notes app! As I use it so much I can’t wait to get my hands dirty with it.

Paul provided us with a list of to-do things that I plan to follow it as strictly as possible. Here is what we should do:

Once you’ve done those, try using Interface Builder to customise the UI – how close can you make it look like Notes?

Note: the official Apple Notes app supports rich text input and media; don’t worry about that, focus on plain text.

1. Create a table view controller that lists notes. Place it inside a navigation controller.

I’m back from these three days of holidays and I can resume the creation of this Notes clone app. Here is where I have gotten to before leaving. Some parts are really messy, especially in the Model part, but I count on solving them very soon.

Storyboard

Let’s start from here. I embedded the view controller in a navigation controller and then, selecting the navigation bar, I checked the Prefers Large Titles box. I know I can do that in code but I prefer this visual aid. Then, selecting the whole navigation controller and going to the Attributes Inspector I checked the Shows Toolbar option. I tried to do that in code but I didn’t find any way to succeed in doing so.

In the main view controller I changed the title of the Navigation Item to “Folders”, then changed the style of the prototype cell (with identifier “Note”) to Right Detail. I want the name of the folder to be on the left and the number of items inside it on the right. I also changed the style of the table view to be Grouped as I want to have different sections in it.

In the toolbar I added a flexible space bar button item and a bar button item called “New folder” (guess what this will do?!).

That’s it by now in the storyboard.

Da code

Inside ViewController.swift I created a folders variable of type [[String]] initialised with [["Notes"]]. Don’t yell at me just yet, I know this is messy and I will fix it very soon but that was how I figured out sections to be. I’m pretty sure to be wrong and that maybe a dictionary of type [String: [String]] could have been better but hey, I’m learning and testing on the go.

Inside viewDidLoad I created an editButtonItem and a normal UIBarButtonItem called deleteButton that would call the deleteRows selector method. I grouped them inside the navigationItem.rightBarButtonItems array and then set deleteButton.isEnabled = false (you will see why in just a minute).

Finally I set tableView.allowsMultipleSelectionDuringEditing to true.

I created an action from the “New folder” toolbar button item called newFolderTapped and inside it, summoned an alert controller with a single textfield and an action attached to a “Save” button. This would insert that text in the first section of the folders array. I would really like to make this better straight away but my skills are just where they are right now and I am already stretching quite a bit by doing these extra untaught things.

After inserting the item in the array I inserted the row in the table view. I tried to do it the nice way, that is inserting the row at the bottom but, I was getting endless crashes and I could not figure out how to do this by now.

I then created the deleteRows method that would be loop over the selected rows (more on how to select them later) and then remove them from the array and then call three methods on the table view: beginUpdates, deleteRows(at:with:) and endUpdates.

Table view data source methods

The numberOfSections returns folders.count, so the first level of the array. The titleForHeaderInSection will return headers[section], headers being a new array of strings just created and hosting the only string “iCloud”.

The numberOfRowsInSection returns folders[section].count, while the cellForRowAt dequeues a reusable cell with the “Note” identifier (which I guess I will change to “Folder” for coherence, set the text label’s text to be folders[indexPath.section][indexPath.row] and the detail text label to 0 (a temporary placeholder I would say).

Editing

This method is really nice because it comes free with table views! Just call override func setEditing(_:animated:) and, inside:

super.setEditing(editing, animated: true)
tableView.setEditing(tableView.isEditing, animated: true) // this will be automatically toggled on and off when pressing the Edit button!!
toolbarItems?[1].isEnabled.toggle() // this to disallow creation of a new folder while I am editing
navigationItem.rightBarButtonItems?[1].isEnabled.toggle() // this to activate the delete button when we are editing.

Make the Model better

I created a new Swift file called “Model.swift” and, inside that created three structs to hold the base of what I need. The first is a Directory struct with a “name” string and a [Folder] properties. Then we have the Folder struct which contains a name, an items count and an array of Note objects. Guess what, the next and last struct will be the Note one, with a title and a text string properties. Now I need to adjust the whole code on the other side, wish me luck!

First I created a directories property like this:

var directories: [Directory] = [Directory(name: "iCloud", folders: [])]

I then changed the numberOfSections to have directories.count and the titleForHeaderInSection to be directories[section].name.

This is provisory but it works. Now let’s move to the folder.

Inside the directories property I edited the empty array of folders adding a simple item: Folder(name: "Notes", notes: [], itemsCount: 0). I then edited the numberOfRowsInSection return value to directories[section].folders.count. Last but not least, the cellForRowAt method needed to be changed like this:

cell.textLabel!.text = directories[indexPath.section].folders[indexPath.row].name
cell.detailTextLabel!.text = String(directories[indexPath.section].folders[indexPath.row].itemsCount)        

Maybe it looks chaotic but it works so, until a better solution is proposed or found that is here to stay. I really doubt that Apple’s code for this app is much simpler.

Now, commenting out the folders property will cause a whole lot of troubles which we are going to address right now.

The deleteRows method needs to have the deleting code be changed to:

directories[indexPath.section].folders.remove(at: rowToDelete)

The part in the newFolderTapped is a bit more complicated because even if we have started to create support for multiple saving directories, we cannot let the user choose where to save new folders so, by now, we are just going to assume the user has a single directory on the Notes app. Changing this later will be easier, probably…possibly?.

By now I simply changed the code in the action closure to this:

let newFolder = Folder(name: noteTitle, notes: [], itemsCount: 0)
self?.directories[0].folders.insert(newFolder, at: 0) 

I tested it and it works.

I added the possibility to insert multiple directories and to give them names but I have to figure out how to let the user choose in which folder to insert the item.

Looking at how iOS does that I tried to replicate it. This had to be done in the newFolderTapped method. I created a control flow statement where the present code would be executed with directories.count <= 1 (I’ve not implemented the deletion of sections yet and possibly I will not do that in the present iteration of the project but, better to leave room for that in the future). In all other cases I would want an .actionSheet alert with a new action for every Directory element. I don’t want to bore you with endless streams of code here (which you can check in the GitHub repository anyway) but the best parts here were these lines:

for (index, directory) in directories.enumerated() {
// This let me keep an index for later use

self?.directories[index].folders.insert(newFolder, at: 0)
// see how this comes in handy?

I still wonder how I cannot insert an object at the end of an array. I used to know that doing the insertion at array.count would add an element, right? Instead… it just crashes my app. I mean, this is just working right?

So why is it not working in the app?

Never mind, let’s move on…

3. Saving the data

I want to do saving before the next screen, just because…

Paul says we can use UserDefaults and that’s fine but, how can we instead save everything to a file?

Anyway, I went with UserDefaults by now so, in order: I added the JSON decoding code into viewDidLoad, then moved the “Helper methods” mark section to the bottom of the view controller, then created the saveModel method with the JSON encoding code and, finally, called it in every possible sensible place.

Tried it, it works! Done! ✅

2A. Tapping on a note should slide in a detail view controller that contains a full-screen text view.

Now to the moment of truth, I need to create the second screen, where the folder is opened and it shows, inside, all the notes. But before, I need to fix a small but important bug. When I call the indexPathsForSelectedRows during the deletion process, it seems that this property respects the order in which items are selected. This means that crashes will occur in very precise selection orders as some index paths will just not be there anymore when we want to delete them.

Thanks to the help of the always present Rob Baldwin I added a sorting method so that, whichever the selection order, the index paths passed to the deleting code would be ordered and safe.

For this second screen I am now writing in Day 80 because this took me so much time to get it right. It was crashing and crashing again, and then it was not passing the data and so on and so forth until I found the light with delegation. I had tried it before but for some reason it was not working. Now it does and I sincerely hope it is not just luck.

I will recount here only the solution that works because it would have been useless to show every single misstep.

Storyboard

We start from the storyboard and add a new table view controller. We give the cell a reuse identifier of “Note” and a style of “Subtitle”. We then give the view controller a Storyboard ID of “FolderViewController” and create an accompanying class so that we can make it inherit from that one.

Now, mosto of the work will be done in code so let’s switch to FolderTableViewController.swift.

Folder View Controller

I created four properties: an array of Notes, an implicitly unwrapped origin index path to keep track of the index path we are coming from in the other view controller, an implicitly unwrapped ViewController called “delegate” and a toolbar label of size (200 x 21) which will keep count of the notes inside of the view controller.

Inside viewDidLoad I created an edit button item to the top right, a compose button which will call the composeNote method, a flexible space button, performed a few adjustments to the toolbar label, created the label button using the (customView:) initialiser of the UIBarButtonItem, created a delete button which will call the deleteNote method and disabled it, arranged all these things in the toolbarItems and, finally, allowed multiple selection during editing.

For the table view data source methods I returned notes.count from the numberOfRowsInSection, created an appropriate cell for the row at the index path that would show the title of the note as text label and the text of the note as the detail label before returning the cell, and overrode the commit editingStyle method just to get the swipe to delete functionality.

For the table view delegate methods I just called the setEditing method (which is actually a method common to all view controllers) and set it to toggle the isEnabled property of the first and last item in the toolbar items array (that is the compose and delete buttons).

I then created the two helper methods, composeNote() and deleteNote().

In the first one I created a temporary note to see if everything would be working in the form of Note(title: "Test title", text: "Testing the detail text item". I appended the note to the notes array, set the toolbarLabel.text to "\(notes.count) notes", called the still unwritten delegate.updateFolder(at: originIndexPath, with: notes) method and reloaded the table view data.

In the second one I repeated the deletion code with the appropriate calls and inserted a call to the delegate method in the middle. Both here and in the sibling method in ViewController.swift I called isEditing.toggle() at the end, to provide a better user experience.

View controller

Moving back to ViewController.swift I finally resorted on using the didSelectRowAt method instantiating a new view controller with the “FolderViewController” identifier, setting its title to the name of the folder, its notes property to the folder’s notes array and its originIndexPath property to the currently selected index path. I then set the present view controller to be the delegate of the folder view controller and then pushed the view controller on to the navigation controller.

What I like of delegation is that the name of the delegate property is completely arbitrary: one could call it endOfTheWorld and then you would call folderViewController.endOfTheWorld = self and it would still works. A pity that the computer would not understand the joke!

The only thing to add would be the implementation of the updateFolder(at:with:) method. In past tutorials I had seen this done with extensions and in much more complicated ways but this time I tried to implement Paul solution and … it just worked …! Simply amazing!

I set the appropriate notes folder to be equal to the notes parameter, the .itemsCount equal to notes.count, then reloaded the table view’s data and saved the model.

Given the amount of sweat and curses that came for this, it ended up in a much nicer way.

2B. Now for the true text view

The main issue now is that I can add many new notes but they are not really meaningful.

First let’s create a new view controller in the storyboard, give it a Storyboard ID of “NoteViewController” and add a text view inside it, pinning it to each corner of the parent view. Create a new Cocoa Touch Class that inherits from UIViewController and call it “NoteViewController.swift”.

Inside it, create an outlet for the text view and the following properties: a Note object (implicitly unwrapped), a weak FolderTableViewController! delegate property, and an originIndexPath (exactly as before).

Inside viewDidLoad set the largeTitleDisplayMode for the navigation item to be .never, as we do not want big titles for the notes, set the .text property of the text view to be equal to the text property of the Note object and set the font size to 18, to be a bit more legible. The way this last thing is done is quite inelegant: textView.font = textView.font?.withSize(18). Isn’t there really anything better than this?

Then, create a new right bar button item of type .done and of action #selector(done), we are going to fill it in in a minute. Finally, implement the two observers that will let us adjust the keyboard and the frame while typing. Also don’t forget to implement the adjustForKeyboard method at the end of the class.

Write the done method with a popViewController call and a delegate.updateNote(at:with:) one (which we are about to write). Pass the originIndexPath as first parameter and textView.text as second parameter.

Now move back to FolderTableViewController.swift and write the delegate method updateNote(at index: IndexPath, with text: String). Inside it the following code:

notes[index.row].text = text // this passes the text from the text view to the notes object inside our folder view controller
toolbarLabel.text = "\(notes.count) notes" // this is very important and I guess I should have made a property observer somewhere but, now that it works, let's leave it like it is
delegate.updateFolder(at: originIndexPath, with: notes) // this is fundamental because otherwise the data was being passed between the view controllers but not saved! Nice that recursivity is so powerful! 
tableView.reloadData() 

… and that’s it!

Now the app works perfectly in all its fields.

Fine, I have not implemented a way to delete directories but, by now, I don’t see the need to do so. It would be an easy add anyway later down the road.

4. Add some toolbar items to the detail view controller – “delete” and “compose” seem like good choices. (Project 4)

Done!

5. Add an action button to the navigation bar in the detail view controller that shares the text using UIActivityViewController. (Project 3)

Let’s do this!

First: let’s add a flexible space button and a share button (bar button item with .action type) to viewDidLoad and group them into the toolbarItems. Make the share button call the shareNote selector method.

Second: write the shareNote method: create a UIActivityViewController using [note.title, "\n", note.text] as the activityItems parameter and leave the second parameter as an empty array. Implement the code for the iPad (popoverPresentationController?.barButtonItem) and present the activity view controller.

Done

Finishing touches

Following the advise of Rob Baldwin I am implementing the search bar in every view following his own tutorial.

After a good hour of trial I abandoned the project here where it is because it is becoming a whole mess to track which array should be called when. I think this is a good example for implementing an extra view controller for the search results.

This app seems way to complex for the simplest implementation of the search bar.

So, in the end, this is the app for Consolidation Day VIII and here is the code on GitHub.

Please download it, try it out and let me know what you think of it.


Please don’t forget to drop a hello and a thank you to Paul for all his great work (you can find him on Twitter) and be sure to visit the 100 Days Of Swift initiative page. We are learning so much thanks to him and he deserves to know our gratitude.

He has about 20 great books on Swift, all of which you can check about here.

The 100 Days of Swift initiative is based on the Hacking with Swift book, which you should definitely check out.


If you like what I’m doing here please consider liking this article and sharing it with some of your peers. If you are feeling like being really awesome, please consider making a small donation to support my studies and my writing (please appreciate that I am not using advertisement on my articles).

If you are interested in my music engraving and my publications don’t forget visit my Facebook page and the pages where I publish my scores (Gumroad, SheetMusicPlus, ScoreExchange and on Apple Books).

You can also support me by buying Paul Hudson’s books from this Affiliate Link.

Anyways, thank you so much for reading!

Till the next one!

Published by Michele Galvagno

Professional Musical Scores Designer and Engraver Graduated Classical Musician (cello) and Teacher Tech Enthusiast and Apprentice iOS / macOS Developer Grafico di Partiture Musicali Professionista Musicista classico diplomato (violoncello) ed insegnante Appassionato di tecnologia ed apprendista Sviluppatore iOS / macOS

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: