Day 34 - 100 Days of Swift

4 minute read

Project 7 (part 2)

Day 34 is the second part of the seventh project. You make a DetailViewController that renders some HTML to display the information from the petition, you add the second tab which displays the popular petitions, and you add a simple error message to be displayed in the case the the petitions can’t be loaded.

The first thing you do is make a DetailViewController file and give it a WKWebView as its main view:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import UIKit
import WebKit

class DetailViewController: UIViewController {

    var webView: WKWebView!
    var detailItem: Petition?

    override func loadView() {
        webView = WKWebView()
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()
    }

}

Next, you build the HTML to display and display it. I pulled this out into a setupViews() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
guard let detailItem = detailItem else { return }

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>
<p>Signatures: \(detailItem.signatureCount)</p>
</body>
</html>
"""
webView.loadHTMLString(html, baseURL: nil)

And then called it from viewDidLoad:

1
2
3
4
override func viewDidLoad() {
    super.viewDidLoad()
    setupViews()
}

Then, all you have to do is instantiate a new DetailViewController in tableView(_:didSelectRowAt:), give it the right petition, and present it:

1
2
3
4
5
6
7
override func tableView(_ tableView: UITableView,
                        didSelectRowAt indexPath: IndexPath) {
    let detailVC = DetailViewController()
    detailVC.detailItem = petitions[indexPath.row]
    navigationController?.pushViewController(detailVC,
                                             animated: true)
}

To add the second tab you instantiate a new view controller in application(_:didFinishLaunchingWithOptions:), set its UITabBarItem, and add it to the main UITabBarController’s array of view controllers:

1
2
3
4
5
6
if let tabBarController = window?.rootViewController as? UITabBarController {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let vc = storyboard.instantiateViewController(withIdentifier: "NavController")
    vc.tabBarItem = UITabBarItem(tabBarSystemItem: .topRated, tag: 1)
    tabBarController.viewControllers?.append(vc)
}

With that, you have two tabs, but they both load the same list of petitions. To get them to load the right lists you make a slight tweak to loadPetitions in ViewController:

1
2
3
4
5
6
7
let urlString: String

if navigationController?.tabBarItem.tag == 0 {
    urlString = "https://api.whitehouse.gov/v1/petitions.json?limit=100"
} else {
    urlString = "https://api.whitehouse.gov/v1/petitions.json?signatureCountFloor=100000&limit=100"
}

Finally, to display the error message, you add a helper function called showError():

1
2
3
4
5
6
7
let alertController = UIAlertController(title: "Loading error",
                                        message: "There was a problem loading the feed. Please check your connection and try again.",
                                        preferredStyle: .alert)
let action = UIAlertAction(title: "Ok",
                           style: .default)
alertController.addAction(action)
present(alertController, animated: true)

And then you call it in the else blocks of loadPartitions() and parse(json:):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// In loadPartitions
if let url = URL(string: urlString),
    let data = try? Data(contentsOf: url) {
    parse(json: data)
} else {
    showError()
}

// In parse(json:)
if let jsonPetitions = try? decoder.decode(Petitions.self,
                                           from: json) {
    petitions = jsonPetitions.results
    tableView.reloadData()
} else {
    showError()
}

With all that done, it looks like this:

Screenshots of working app.

You can find my version of this project at the end of day 34 on Github here.

Reflections

I thought .loadHTMLString was pretty cool. It is very weird to write HTML inside a Swift string, and then render that to the display, but it works pretty nicely if that is what you need to do. I feel like I got the smallest little taste of what some of the cross platform frameworks are doing behind the scenes to get stuff to work in multiple places.