Day 61 - 100 Days of Swift

6 minute read

Project 16 (part 2)

Day 61 is the second part of the sixteenth project. You review some of the MapKit stuff you learned yesterday, and he gives you three challenges. The first challenge is to change the tint of the pins on the map. The second is to present an alert controller that allows the user to change the style of the map. And the third is to present a new view controller with a web view that will load the related Wikipedia article when the user taps on the info button in the callout.

For the first one, I type-casted the view returned from dequeueReusableAnnotationView as an MKPinAnnotationView, and then set the pinTintColor:

1
2
3
4
5
6
// In mapView(_:viewFor)
var annotationView = mapView
            .dequeueReusableAnnotationView(withIdentifier: identifier) as? MKPinAnnotationView

// right before the return
annotationView?.pinTintColor = .cyan

It looks like this:

Screenshot of app with new pin tint

For the second challenge, I wrote an intermediary enum to hold the options I wanted to present the user:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum MapType: String, CaseIterable {
    case standard = "Standard"
    case muted = "Muted"
    case hybrid = "Hybrid"
    case satellite = "Satellite"

    func mapType() -> MKMapType {
        switch self {
        case .standard:
            return .standard
        case .muted:
            return .mutedStandard
        case .hybrid:
            return .hybrid
        case .satellite:
            return .satellite
        }
    }
}

Then I used that to build a UIAlertController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@objc private func presentMapTypeAlertController() {
    let alertController =
        UIAlertController(title: "Map Display Type",
                          message: "How would you like to display the map?",
                          preferredStyle: .actionSheet)

    for mapType in MapType.allCases {
        let action = UIAlertAction(title: mapType.rawValue,
                                   style: .default,
                                   handler: changeMapType)

        alertController.addAction(action)
    }

    let cancelAction = UIAlertAction(title: "Cancel",
                                     style: .cancel)
    alertController.addAction(cancelAction)

    present(alertController, animated: true)
}

Then I just wrote the handler function:

1
2
3
4
5
private func changeMapType(_ action: UIAlertAction) {
    let mapType = MapType(rawValue: action.title!)!

    mapView.mapType = mapType.mapType()
}

Using this method allows me to change the titles to whatever I want, while still being hooked up to the options in MKMapStyle. It also lets me present only the options that I want to present. It looks like this:

Screenshot of app with various map types.

For the third challenge I pulled in a modified version of the view controller we wrote in Project2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import UIKit
import WebKit

class WebViewController: UIViewController, WKNavigationDelegate {

    var webView: WKWebView!
    var progressView: UIProgressView!
    var selectedItem: String?

    override func loadView() {
        // Change the default view to be a WKWebView
        webView = WKWebView()
        webView.navigationDelegate = self
        view = webView
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        setupViews()
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)

        navigationController?.isToolbarHidden = true
    }

    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                               context: UnsafeMutableRawPointer?) {
        if keyPath == "estimatedProgress" {
            progressView.progress = Float(webView.estimatedProgress)
        }
    }

    func setupViews() {

        // Set up the progress view
        progressView = UIProgressView(progressViewStyle: .default)
        view.addSubview(progressView)
        progressView.translatesAutoresizingMaskIntoConstraints = false

        progressView.leadingAnchor
            .constraint(equalTo: view.leadingAnchor)
            .isActive = true
        progressView.trailingAnchor
            .constraint(equalTo: view.trailingAnchor)
            .isActive = true
        progressView.bottomAnchor
            .constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
            .isActive = true

        let keyPath =
            #keyPath(WKWebView.estimatedProgress)
        webView.addObserver(self,
                            forKeyPath: keyPath,
                            options: .new,
                            context: nil)

        // Set up the toolbar
        let backward =
            UIBarButtonItem(title: "←",
                            style: .plain,
                            target: webView,
                            action: #selector(webView.goBack))

        let spacer1 =
            UIBarButtonItem(barButtonSystemItem: .flexibleSpace,
                            target: nil,
                            action: nil)

        let refresh =
            UIBarButtonItem(barButtonSystemItem: .refresh,
                            target: webView,
                            action: #selector(webView.reload))
        let spacer2 =
            UIBarButtonItem(barButtonSystemItem: .flexibleSpace,
                            target: nil,
                            action: nil)

        let forward =
            UIBarButtonItem(title: "→",
                            style: .plain,
                            target: webView,
                            action: #selector(webView.goForward))

        toolbarItems = [backward, spacer1, refresh, spacer2, forward]
        navigationController?.isToolbarHidden = false


        // Load a default website
        if let item = selectedItem {
            title = item
            let url = URL(string: "https://wikipedia.org/wiki")?
                .appendingPathComponent(item)

            webView.load(URLRequest(url: url!))
            webView.allowsBackForwardNavigationGestures = true
        }
    }

    // MARK: - WK Navigation Delegate
    func webView(_ webView: WKWebView,
                 didFinish navigation: WKNavigation!) {
        title = webView.title
    }
}

I got rid of the stuff that checks what url they are going to, because I don’t really care. I also hardcoded the url for Wikipedia, and used appendingPathComponent instead of just appending the string, because that will url-encode the spaces and whatnot (e.g. in “Washington DC”).

Then I just needed to get rid of the alert controller when the info button is tapped, and replace it with this new view controller:

1
2
3
4
5
6
7
8
// In mapView(_:annotationView:calloutAccessoryControlTapped:)
let placeName = capital.title

let webViewController = WebViewController()
webViewController.selectedItem = placeName

navigationController?.pushViewController(webViewController,
                                         animated: true)

And with that, it looks like this:

Gif of working app.

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

Reflections

I will probably start moving a little slower through the 100 Days of Swift, because I just started my first development job at Madwire. That is why I haven’t posted a 100 Days of Swift post since last Saturday. I’m excited for the new job, and I’m learning a lot already, but it is very mentally demanding and this week I just didn’t have the capacity to also do these exercises. I am going to try to push through though, and hopefully get through a couple per week from here on out. So we’ll see how that goes. I at least have today’s written, and only another 39 to go.