Hacking with Swift – Challenge 7

I know, I am at least one day late, but here in Belgrade there is so much to do—beside going to the dentist!— that it is really difficult to find the time! Anyway…

Today is challenge day for the 7th project! First, though, let’s review all the awesome things we learned:

  • JSON decoding using Swift’s Data type
  • use the Codable protocol to convert our data into Swift objects we defined.
  • UITabBarController, UIStoryboard, …

Review

  • Creating a URL from a string might fail.
  • String, Int, [String], and more all conform to Codable.
  • Swift’s Data type can hold any kind of binary data.
  • The Codable protocol can convert Swift types to and from JSON.
  • Storyboard identifiers allow us to create storyboard view controllers in code.
  • UITabBarController is able to store multiple view controllers for the user to select from.
  • UIStoryboard can load storyboards from our bundle and create view controllers from there.
  • Apple provides a handful of built-in UITabBarItem types for common uses.
  • The JSONDecoder type does the hard work of converting JSON to Swift values.
  • We can create a Data instance directly from a URL.
  • JSON is a lightweight way to store and send data.
  • Table view cells with subtitles show two different text labels.

Commentary

Of course I have only inserted the correct answers here but, please, if you have time, go to the review page for the Hacking with Swift initiative and give a look at the other answers. Many are straightforward, but many others are not… This is what makes of Paul a great teacher! It gives you some hints but you have to dig the golden nugget out of the ground!

Challenges

Challenge 1: add a Credits button to the top-right corner using UIBarButtonItem. When this is tapped, show an alert telling users the data comes from the We The People API of the Whitehouse

First things first: let’s create a button. And here I have to thank Paul once more because without thinking too much I knew where to go, what to write and how to do so! Here is the code that I put in viewDidLoad():

navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Credits", style: .plain, target: self, action: #selector(showCredits))

This will create a button with the .plain style (that is, just a button with a text), self (the view controller) as target and as action a method we still need to write.

Here is the showCredits action:

  @objc func showCredits() {
        let ac = UIAlertController(title: "Data source", message: "These petitions come from the \nWe The People API of the Whitehouse", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
        
        present(ac, animated: true)
    }

I confess, I wanted to create something more complicated and I will probably come back to it after I have completed the other two challenges. My idea was to add another action the Alert Controller (because the text label instead the alert does not directly support links) so that when the user clicks on it a new view controller will load with a webView containing the main website of the petitions. This seems within my grasp but I have zero time now… will try it later!

While I was writing this the app built and loaded. To be sincere the “OK” title looks quite meh… so I will replace it with “Thank you!”, as it sounds much more patriotic!

Credits alert!
Credits alert!

Challenge 2: let users filter the petitions they see. This involves creating a second array of filtered items that contains only petitions matching a string the user entered. Use a UIAlertController with a text field to let them enter that string.

So… this is advertised as a tough one. I have my mind rushing and I would like to jump straight into code and write tens of lines but let’s do one thing at a time. First, we still have the left bar button item slot available so let’s use it.

navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Filter", style: .plain, target: self, action: #selector(filterPetitions))

This looks like a good start. Now I need a refresher on how to get the text field into the alert controller so I will go back and give a look at the Word Scrambler project.

insert 20 minutes here!

Here I am, back for you and here is what I have achieved (nothing really, but at least it is a start!)

    var filteredPetitions = [Petition]()

This is the second array of filtered items that will contains petitions matching the entered string. Then, here is the filterPetitions action.

@objc func filterPetitions() {
        let ac = UIAlertController(title: "Filter petitions", message: "Type in to filter...", preferredStyle: .alert)
        ac.addTextField()
        
        let filterAction = UIAlertAction(title: "Filter", style: .default) {
            [weak self, weak ac] _ in
            guard let filterWord = ac?.textFields?[0].text else { return }
            self?.showPetitions(for: filterWord)
        }
        
        ac.addAction(filterAction)
        present(ac, animated: true)
    }
    
    func showPetitions(for filter: String) {
        
    }

I created an alert controller, added a text field and programmed a filterAction to call the showPetitions(for:) method using the passed in word. I then added the action to the alert controller and presented it.

Now I need to write the showPetitions(for:) method…

I wrote this:

func showPetitions(for filter: String) {
        for petition in petitions {
            if petition.title.contains(filter) {
                filteredPetitions.append(petition)
            } else {
                continue
            }
        }
        tableView.reloadData()
    }

This should append to the second array every item in the original petition array which contains the filter string in the title. I wrote the else continue just in case but it is possibly not needed and, maybe also the reloadData() call is not needed. I am not sure. What worries me is what comes next: I have to show the actual data in the table view, which I guess should be done in the cellForRowAt method but, if I do so…

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let petition = petitions[indexPath.row]
        let filteredPetition = filteredPetitions[indexPath.row]
        
        if !filteredPetitions.isEmpty {
            cell.textLabel?.text = filteredPetition.title
            cell.detailTextLabel?.text = filteredPetition.body
        } else {
            cell.textLabel?.text = petition.title
            cell.detailTextLabel?.text = petition.body
        }
        
        return cell
    }

…asking to load the second array if it is not empty, I get an out-of-index error…

The reason why this happens is that, at the moment of viewDidLoad the filteredPetitions array is empty. So I should just delay the creation of that property. Let’s try.

…a few minutes later…

Nothing, putting the filteredPetition property inside the control flow statement doesn’t help. That is, the app loads, the filter works, apparently, but then I cannot load the results … Let’s try again, with a different approach…

… another bunch of minutes later …

It seems I am quite in the wrong direction… But now I have to leave so I will continue tonight…


8 hours later and a broken iPhone screen…

So it happened to me as well… I fell on some stairs in a really ugly part of Belgrade which I think saw the carpenter last time a few decades ago and, while trying not to fall and to destroy my legs my iPhone XS jumped out of the jacket’s pocket and fell to the ground… on a first inspection nothing had happened but, rotating it, the camera was completely scratched. Trying to launch the Camera app resulted in a black screen… Sayonara baby…

If this was not enough I accidentally invoked Siri while being outside of any Wi-Fi memorised by my device (and I do not have a Serbian sim-card) and apparently didn’t dismiss it before putting the phone back into my pocket.

After a few minutes I felt a burning wave in my pocket and I immediately grasped my phone which said “Emergency: the iPhone needs to cool down before being used”… I immediately though of a battery failure due to the fall and I shut down the device. Now everything seems to be working, except for the camera.

I have the 2-years Apple Care+ coverage so the only question is:

  • should I bring it to a “Premium Reseller” here in Belgrade or
  • wait for coming back to Torino to have it restored there?

Anyway, back to the challenge and, yes, I gave a look at Paul’s suggestions but … I am not pretty sure how this is going to work. Let’s erase at least part of the previous code and get moving from here.

We created an array of [Petition] to hold the filtered data and we learn from Paul that our filtered array should be the same as the main one. So, I suppose I have to get this done in viewDidLoad.

Fine, I tried a few things: first I put a filteredPetitions = petitions after the call to parse(json: data). I guess this is what Paul said when saying

At first your filtered petitions array will be the same as the main petitions array, so just assign one to the other when your data is ready.

Then I modified the cellForRowAt method to this:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
//        let petition = petitions[indexPath.row]
        let petition = filteredPetitions[indexPath.row]
        
        cell.textLabel?.text = petition.title
        cell.detailTextLabel?.text = petition.body
        
        return cell
    }

Paul said:

Your table view should load all its data from the filtered petitions array

So, I simply changed the source of the data which, having been assigned before, should be good (and running this app works, all petitions are still there).

Now I need a bar button item to show an alert controller that the user can type into (and I have already done so as the alert appears and the text field is indeed responsive).

The only thing that doesn’t work is the last part, that is the showPetitions method which should get called when the user presses the “Filter” alert action. Here is what Paul says (which confuses me):

Once that’s done, go through all the items in your petitions array, adding any you want to the filtered petition.

But how can we add the filtered items to an array that contains already all of the petitions? After all we did filteredPetions = petitions which put the content of the second one inside the first one, right? So then … how to do that?

I have seen another solution that uses a whole lot of extensions to the ViewController’s class but then uses a UISearchBarController, which is not what we were tasked with. It works, of course but…

…still trying something else and not understanding…

Maybe I am the only one here but I am not understanding what is wrong…

A little progress: I moved the filteredPetitions = petitions into the parse method just before the call to reloadData and managed not to crash the app right now… still, whatever I do I still get an out of bound error … should be quite easy to spot, right? Something is out of bound you idiot! And still … following the instructions just made my life worse …

I tried some basic debugging like printing the filteredPetition array after calling the .filter closure on it with { $0.title.contains(filter) } and it actually prints the correct array. So the issue must be somewhere else.

How can the index be out of bound if the array is dynamically loaded every time? Should I erase the table view?

Very interesting, look at this:

func showPetitions(for filter: String) {
        filteredPetitions = filteredPetitions.filter { $0.title.contains(filter) }
        print(filteredPetitions)
    }

If I leave it like this the array gets correctly printed out, but if I add tableView.reloadData() below the app crashes with the out-of-bound error and the array gets printed as this [], that is, empty… why is it so?

Nothing… I am following all of the hints, using all what I know and all what I do not know and can look for in a reasonable way and … nothing … either it crashes or it doesn’t reload the table-view… this is very frustrating and not funny at all…

How I solved it…

As it usually happens, the answer is there under your eyes and you were doing almost everything right … just … now I know that when the index out of bound error comes then the issue is simply the numberOfRowsInSection method, which I had forgot to change to filteredPetitions.count.

When that was changed I could easily add the self?.tableView.reloadData() to the action closure.

I then launched my app and saw that once a filter was called there was no way to go back to the full list. I then decided to experiment a bit with bar button items.

I stored the left bar button item and an extra bar button item with a resetList action in an array of bar button items and then called it as a navigation item. This is how it looks:

let filterButton = UIBarButtonItem(title: "Filter", style: .plain, target: self, action: #selector(filterPetitions))
        let resetButton = UIBarButtonItem(title: "Reset", style: .plain, target: self, action: #selector(resetList))
            
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Credits", style: .plain, target: self, action: #selector(showCredits))
        navigationItem.leftBarButtonItems = [filterButton, resetButton]

…and here is the resetList method:

@objc func resetList(action: UIAlertAction) {
        filteredPetitions = petitions
        tableView.reloadData()
    }

So… now everything works… look at this beauty:

Challenge completed
Challenge completed

Challenge 3: experiment with the HTML to tinker with the layout a little…

It is 1.17am here and I am not sure I want to dive into this but I will try…

I am very tired but I managed to do this:

let html = """
        <html>
        <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style> body { font-size: 150%; } </style>
        </head>
        <body>
        
        <h3>\(detailItem.title)</h3>
        <p>\(detailItem.body)</p>
        
        </body>
        </html>
        """

Now the detail view controller shows a big bold title and a paragraph following it with the body of the petition.

Nice!

I will for sure experiment more on this but … when I will need it!

As always, you can find the code for this whole challenge here on GitHub.

Let me know what you think of it.


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

Create your website with WordPress.com
Get started
%d bloggers like this: