Day 25 - 100 Days of Swift

5 minute read

Project 4 (part 2)

Day 25 is the second part of the fourth project. You extend the functionality of the basic web browser by adding a refresh button and a progress bar to the toolbar. You refactor your code to make it a little easier to change the list of approved websites, and you adopt another WKNavigationDelegate method to verify if the user is attempting to go to a website that is not on the approved list.

The first thing you do is add the refresh button to the toolbar in viewDidLoad and make the toolbar visible:

1
2
3
4
5
6
7
8
9
10
11
let spacer =
    UIBarButtonItem(barButtonSystemItem: .flexibleSpace,
                             target: nil,
                             action: nil)
let refresh =
    UIBarButtonItem(barButtonSystemItem: .refresh,
                    target: webView,
                    action: #selector(webView.reload))

toolbarItems = [spacer, refresh]
navigationController?.isToolbarHidden = false

Then you build a UIProgressView to display the progress as the page loads, and add that to the toolbar as well. You can’t just add a UIView to the toolbar though, so you have to wrap it in a UIBarButtonItem first:

1
2
3
4
5
6
7
progressView = UIProgressView(progressViewStyle: .default)
progressView.sizeToFit()
let progressButton = UIBarButtonItem(customView: progressView)

// ... other stuff is the same...

toolbarItems = [progressButton, spacer, refresh]

This displays a the progress view in the toolbar, but the progress is never actually being updated, so you add that next. First you add an observer to the key path for WKWebView’s estimatedProgress property:

1
2
3
4
5
let keyPath = #keyPath(WKWebView.estimatedProgress)
webView.addObserver(self,
                    forKeyPath: keyPath,
                    options: .new,
                    context: nil)

Now that our ViewController is observing something, we have to give it something to do when the value changes. You do that by overriding observeValue():

1
2
3
4
5
6
7
8
override func observeValue(forKeyPath keyPath: String?,
                           of object: Any?,
                           change: [NSKeyValueChangeKey : Any]?,
                           context: UnsafeMutableRawPointer?) {
    if keyPath == "estimatedProgress" {
        progressView.progress =Float(webView.estimatedProgress)
    }
}

This basically says “any time the value of any key we’re observing changes, call this function. If the key is ‘estimatedProgress’, we’ll update the progress view associated with it.”

The app works at this point and, from the user’s perspective, functions like it is supposed to. But it is still possible to get to other websites than the ones we want to allow by following links on the webpages themselves. So we make a few changes to remedy that. The first is to do some refactoring that will allow us to have one central place where we define what websites are allowed. First, we add this property:

1
var websites = ["dillon-mce.com", "apple.com"]

Then we change all of the places that we had previously hardcoded website names:

1
2
3
4
5
6
7
8
9
10
// In viewDidLoad()
let url = URL(string: "https://" + websites[0])!

// In openTapped()
for website in websites {
    let action = UIAlertAction(title: website,
                               style: .default,
                               handler: openPage)
    ac.addAction(action)
}

Now if we want to change what websites are allowed, we just need to update that array once. And, to stop users from going to websites that aren’t on that list, we adopt webView(_:decidePolicyFor:decisionHandler):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func webView(_ webView: WKWebView,
             decidePolicyFor navigationAction: WKNavigationAction,
             decisionHandler: @escaping (WKNavigationActionPolicy)-> Void) {
    let url = navigationAction.request.url

    if let host = url?.host {
        for website in websites {
            if host.contains(website) {
                decisionHandler(.allow)
                return
            }
        }
    }
    decisionHandler(.cancel)
}

This gets the host from the request and checks to see if it is in our approved list of hosts. If it is, it lets the action continue, otherwise it cancels it.

The finished app looks like this:

Screenshots of working app.

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

Reflections

Today was really cool! I’ve used UIProgressView before, but never with KVO. In fact, I have done very little KVO in Swift at all, so that was cool to see. On project 4 no less! I’ve also never really had much opportunity to use the toolbar that comes built in with UINavigationController. It was surprisingly easy to work with.

One thing that I’m really noticing as we go through the projects is that I already have a pretty good understanding of how this stuff works. At least, I have a better understanding than I feel like I do. Because when I see all the pieces together it totally makes sense, I just haven’t necessarily worked with that particular framework or API before. So I’m enjoying all this exposure to new parts of Apple’s frameworks. It’s a great excuse.