Hacking with Swift – Learning Project 7

So, what is the best way not to lose your work? Save it before you publish right? I have decided to try a new tool (new for me at least) for managing all of my writing even if it will be a paid tool. I am now writing my articles using Ulysses for the next 7 days until the trial expires and see what can be done

Setting up

This project will teach us how to take a bunch of data from a website and translate it into useful information for our users. We will be learning about the following things:

As usual, we create a Single View App in Xcode and we can either call it Project7 as Paul suggests or, as I did, following the title of the app itself, Whitehouse Petitions. Let’s get started!

Creating the basic UI: UITabBarController

By now we know the basic drill of all this so let’s just summarise what we are going to do right now:

  • inside ViewController.swift let’s change the class’ inheritance to UITableViewController and reflect the same change in the Identity Inspector of the view controller in the storyboard. We also set this view controller to be the Initial View Controller
  • select the prototype cell and change its reuse identifier to be “Cell”.
    • also change its Accessory to be “Disclosure Indicator” (that’s the small arrow pointing rightward) and its style from “Custom” to “Subtitle” (in this way we will have two rows of text, one bigger and another smaller).
  • As usual we wrap this view controller into a Navigation Controller and this last one in a Tab Bar Controller, which will manage an array of view controllers.
  • Change the UITabBarItem’s system item to “Most Recent”.
  • Select the navigation controller and, in its Identity Inspector, change its Storyboard ID to “NavController”.

ViewController.swift

  • Create a property to hold our data: var petitions = [String]()
  • add the numberOfRowsInSection method and return the .count property of the petitions array.
  • add and configure the cellForRowAt method, with a small difference compared to previous times:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        cell.textLabel?.text = "Title goes here"
        cell.detailTextLabel?.text = "Subtitle goes here"
        
        return cell
    }

Parsing JSON using the Codable protocol

Swift supports working with JSON by conforming to the Codable protocol.

Let’s create a struct called Petition in a new file also called “Petition.swift” and fill it with three properties: a title string, a body string and a signatureCount integer. Structs work wonder here because they give us a memberwise initialiser, that is a special feature that creates new instances of that structure automatically for us when we pass in values for it.

This is not enough, though: the array of petitions is actually inside a dictionary called “results” in the JSON data. To help Swift reading this we simply create a new file called “Petitions” and, inside, create a new struct also called “Petitions” which conforms to Codable and contains a property called results which is an array of our custom object [Petition].

Once this is done we also change the type of our petitions property in ViewController.swift to be an array of petitions itself.

Parsing the JSON

Inside viewDidLoad() let’s insert the following code:

  override func viewDidLoad() {
        super.viewDidLoad()

        let urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
        // try to convert that string into an URL
        if let url = URL(string: urlString) {
            // fetch that from the API
            if let data = try? Data(contentsOf: url) {
                parse(json: data)
            }
        }
    }

This does the following:

  • it creates a new string made of the url to the Hacking with Swift server
  • it tries to convert it into a real URL
  • if this succeeds it tries to fetch the data and store it into a data constant before finally trying to call the parse(json:) method which we have not written yet.

Here’s that method:

func parse(json: Data) {
        let decoder = JSONDecoder()
        
        if let jsonPetitions = try? decoder.decode(Petitions.self, from: json) {
            petitions = jsonPetitions.results
            tableView.reloadData()
        }
    }

  • we create an instance of JSONDecoder()
  • try to decode it the passed in data parameter
  • store the results inside our petitions property
  • reload the table-view data.

The last thing to do to have a working first screen is to modify the cellForRowAt method like this:

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


The second part of this project starts with Day34 of the 100 Days of Swift initiative. Todays topics are learning about charging HTML into a web view, getting confidence with UIStoryboards and adding tabs to a tab bar controller in code.

Rendering a petition: loadHTMLString

First thing to do: create a new detail view controller class to format the petitions’ content in a clean and nice way.

Let’s create a new Cocoa Touch Class file and name it “DetailViewController”. Inside we import WebKit, create a WKWebView implicitly unwrapped property (as it will be instantiated in viewDidLoad, create an optional Petition object and call the loadView() method with an instantiation of the WKWebView and the assignment of this last one to the view property of our view controller.

Copy-pasting the text of the petition in the web view will not work because it will just not look right, instead we have to use some HTML code inside viewDidLoad:

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

We end all this part with a call to webView.loadHTMLString(html, baseURL: nil). This is slightly different from the call we were using in project 4 because we are not directly using a website for it.

Instantiate view controller (or else?…)

This time we do not need to instantiate the view controller as a downcast class from our Main.storyboard. Instead we just have to create an instance of the “DetailViewController” class inside the didSelectRowAt method, set its detailItem property to the appropriate petitions element for that IndexPath and then finally push the view controller! That’s it!

Finishing touches: didFinishLaunchingWithOptions

Two things are still needed now: add another tab to the UITabBarController and make our code a bit more resilient (that is showing the user some error message when needed). The first step is done in “AppDelegate.swift”, and precisely in the didFinishLaunchingWithOptions method, which gets called by iOS when the app has completed the loading process. Inside it we perform the following actions:

  1. downcast the rootViewController of the view controller’s window as a UITabBarController. Assuming everything goes well…
  2. … we store our Main storyboard inside a constant called storyboard
  3. We instantiate a new view controller called “NavController” (now we understand!) and set its .tabBarItem to be equal to a UITabBarItem(tabBarSystemItem: .topRated, tag: 1) (because the first one we had was tag 0 and those tab bar items are an array of items!
  4. We append this view controller to the array of view controllers of the tab bar controller (a bit tong-twisting but nice!)

This code creates a duplicate view controller wrapped inside a navigation controller, equips it with a flashy new tab bar item so that it doesn’t get confused with the previous one and makes it visible.

Adding resiliency

To be complete our app need a bit more of safety and this is done by modifying slightly the string from where we fetch our data, like this:

let urlString: String

if navigationController?.tabBarItem.tag == 0 {
    // urlString = "https://api.whitehouse.gov/v1/petitions.json?limit=100"
    urlString = "https://www.hackingwithswift.com/samples/petitions-1.json"
} else {
    // urlString = "https://api.whitehouse.gov/v1/petitions.json?signatureCountFloor=10000&limit=100"
    urlString = "https://www.hackingwithswift.com/samples/petitions-2.json"
}

We also add a showError method to make the user know if something went adrift:

func showError() {
        let ac = UIAlertController(title: "Loading error", message: "There was a problem loading the feed; please check your connection and try again.", preferredStyle: .alert)
        ac.addAction(UIAlertAction(title: "OK", style: .default))
        present(ac, animated: true)
    }

The last thing to do is just to insert this method in an else statement in the URL summoning part!


This concludes our overview of Project7. As always do not forget to drop a hello and a thank you to Paul for his work on Twitter and to check his article where he explains in much greater detail what I covered here.

The GitHub repository for my version of this app is available here.

Thank you everyone!


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: