Day 35 - 100 Days of Swift

6 minute read

Project 7 (part 3)

Day 35 is the third part of the seventh project. He gives you three challenges to take on on your own. The first is to add a “Credits” button to the navigation bar that presents an alert saying where the data comes from when you tap on it. The second is to let users filter what petitions they see, based on what they type into a text field. The third is to mess around with the HTML a little bit and see if you can make things look a little nicer.

For the first challenge, I just added a UIBarButtonItem to the navigationItem in ViewController:

1
2
3
4
5
navigationItem.rightBarButtonItem =
    UIBarButtonItem(title: "Credits",
                    style: .plain,
                    target: self,
                    action: #selector(presentCreditAlert))

And then I added a presentCreditAlert function:

1
2
3
4
5
6
7
8
9
@objc private func presentCreditAlert() {
    let alertController = UIAlertController(title: "Credits",
                                            message: "Data comes from We The People API of the Whitehouse (api.whitehouse.gov)",
                                            preferredStyle: .alert)
    let action = UIAlertAction(title: "Ok",
                               style: .default)
    alertController.addAction(action)
    present(alertController, animated: true)
}

I noticed that this was almost identical to showError() from yesterday, so I did a little refactoring. First I added a new function called presentInformationalAlert:

1
2
3
4
5
6
7
8
9
10
private func presentInformationalAlert(title: String,
                                             message: String? = nil) {
    let alertController = UIAlertController(title: title,
                                            message: message,
                                            preferredStyle: .alert)
    let action = UIAlertAction(title: "Ok",
                               style: .default)
    alertController.addAction(action)
    present(alertController, animated: true)
}

Then I added an extension on String to hold the titles and messages:

1
2
3
4
5
6
extension String {
    static let loadingErrorTitle = "Loading error"
    static let loadingErrorMessage = "There was a problem loading the feed. Please check your connection and try again."
    static let creditsTitle = "Credits"
    static let creditsMessage = "Data comes from We The People API of the Whitehouse (api.whitehouse.gov)"
}

Then I changed showError and presentCreditAlert:

1
2
3
4
5
6
7
8
9
private func showError() {
    presentInformationalAlert(title: .loadingErrorTitle,
                              message: .loadingErrorMessage)
}

@objc private func presentCreditAlert() {
    presentInformationalAlert(title: .creditsTitle,
                              message: .creditsMessage)
}

I liked this method because it leads to less repetition and it puts all my messages in one central place, if I ever want to change any of them in the future.

For the second challenge he suggests presenting an alert with a text field to allow the user to filter. I’m assuming that is just because he hasn’t covered search bars yet, because it doesn’t feel like the most user-friendly method to use. I decided to implement a UISearchController instead. First, I gave myself a property to reference the search controller, adopted UISearchResultsUpdating, and then set it up in setupViews:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class ViewController: UITableViewController, UISearchResultsUpdating {

var searchController: UISearchController!

// In setupViews()
searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self

searchController.dimsBackgroundDuringPresentation = false
searchController.searchBar.placeholder = "Search Petitions"

navigationItem.searchController = searchController

definesPresentationContext = true

The first two lines (6&7) set up the search controller to use this view controller to be the presenter of the results. The third line (9) stops it from dimming the “background” when results are being presented, because we are using the “background” itself to present the results. The fifth line (12) adds it to the navigation item.

Then, I implemented updateSearchResults:

1
2
3
func updateSearchResults(for searchController: UISearchController) {
    filterPetitions(with: searchController.searchBar.text)
}

And added the filterPetitions method. I had to adopt Equatable for the comparisons of Petitions to work, but I didn’t have to implement anything because the compiler was able to synthesize it for me:

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
// At top of class
var filteredPetitions: [Petition] = []

private func filterPetitions(with string: String?) {
    // Make sure there is a search term,
    // otherwise set the filtered petitions to all the petitions
    guard let searchTerm = string?.lowercased(),
        !searchTerm.isEmpty else {
        self.filteredPetitions = self.petitions
        return
    }

    // Get the petitions who's titles match
    let titlesMatch = self.petitions.filter {
        $0.title.lowercased().contains(searchTerm)
    }
    // Get the petitions who's bodies match
    // and aren't in the first group
    let bodiesMatch = self.petitions.filter {
        $0.body.lowercased().contains(searchTerm) &&
            titlesMatch.firstIndex(of: $0) == nil
    }

    // Add them together and put them in the filtered array
    self.filteredPetitions = titlesMatch + bodiesMatch
}

Then I needed to have the table view pull its data from the filtered array instead of the unfiltered array:

1
2
3
4
5
6
7
8
// In tableView(_:numberOfRowsInSection:)
return filteredPetitions.count

// In tableView(_:cellForRowAt:)
let petition = filteredPetitions[indexPath.row]

// In tableView(_:didSelectRowAt:)
detailVC.detailItem = filteredPetitions[indexPath.row]

Finally, I needed to set filteredPetitions initially, and then make sure that the table view reloaded whenever the filteredPetitions changed. I accomplished both of those with didSet observers:

1
2
3
4
5
6
7
8
9
10
var petitions: [Petition] = [] {
    didSet {
        filterPetitions(with: searchController?.searchBar.text)
    }
}
var filteredPetitions: [Petition] = [] {
    didSet {
        tableView.reloadData()
    }
}

Finally, for the HTML section, I didn’t do a whole lot. But I did add a url property to Petition, so that I could display the link to the actual petition in DetailView, I made the signatures line bold, and I customized the appearance of the new link a little bit:

1
2
3
4
5
6
// In Petition
var url: String

// In DetailViewController
<p><strong>Signatures: \(detailItem.signatureCount)</strong></p>
<p><a href="\(detailItem.url)" style="color: #007AFF; text-decoration: none">\(detailItem.url)</a></p>

When you put it all together, it looks like this:

Screenshots of working app.

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

Reflections

I always enjoy the days where we get to mess around and try stuff ourselves. I almost always end up spending way more time on those days, but I always learn something. Today I learned about setting the color of a link in HTML, and I learned the hex value of Apple’s “systemBlue” is #007AFF (for now at least). They do tell you not to hard code it in your app, but I couldn’t think of a simple way get the API value into HTML. I also learned a lot about how UISearchController works.