25.
The Tag Location Screen
Written by Eli Ganim
There is a big button on the main screen of the app that says Tag Location. It only becomes active when GPS coordinates have been captured, and you use it to add a description and a photo to that location.
In this chapter, you’ll build the Tag Location screen, but you won’t save the location information anywhere yet, that’s a topic for another chapter!
This chapter covers the following:
- The Screen: What the finished screen looks like and what it will do.
- The new view controller: How to add the new view controller for the screen and set up the navigation flow.
- Make the cells: Create the table view cells for displaying information.
- Display location info: Display location info on screen via the new view.
- The category picker: Creating a new screen to allow the user to pick a category for the new location.
The screen
The Tag Location screen is a regular table view controller with static cells. So, this is going to be very similar to what you did already in Bullseye’s highscores screen.
The finished Tag Location screen will look like this:
The description cell (the empty area above the Category cell) at the top contains a UITextView for text. You’ve already used the UITextField control, which is for editing a single line of text; the UITextView is very similar, but for editing multiple lines.
Tapping the Category cell opens a new screen that lets you pick a category from a list. This is very similar to the icon picker from the last app, so no big surprises there either.
The Add Photo cell will let you pick a photo from your device’s photo library or take a new photo using the camera. You’ll skip this feature for now and build that later on. Let’s not get ahead of ourselves and try too much at once!
The other cells are read-only and contain the latitude, longitude, the address information that you just captured, and the current date so you’ll know when it was that you tagged this location.
Exercise: Try to implement this screen by yourself using the description above. You don’t have to make the Category and Add Photo buttons work yet. Yikes, that seems like a big job! It sure is, but you should be able to pull this off. This screen doesn’t do anything you haven’t done previously. So if you feel brave, go ahead!
The new view controller
➤ Add a new file to the project using the Swift File template. Name the file LocationDetailsViewController.
You know what’s next: create outlets and connect them to the controls on the storyboard. In the interest of saving time, I’ll just give you the code that you’re going to end up with.
➤ Replace the contents of LocationDetailsViewController.swift with the following:
import UIKit
class LocationDetailsViewController: UITableViewController {
@IBOutlet weak var descriptionTextView: UITextView!
@IBOutlet weak var categoryLabel: UILabel!
@IBOutlet weak var latitudeLabel: UILabel!
@IBOutlet weak var longitudeLabel: UILabel!
@IBOutlet weak var addressLabel: UILabel!
@IBOutlet weak var dateLabel: UILabel!
// MARK:- Actions
@IBAction func done() {
navigationController?.popViewController(animated: true)
}
@IBAction func cancel() {
navigationController?.popViewController(animated: true)
}
}
Nothing special here, just a bunch of outlet properties and two action methods that both go back to the previous view in the navigation stack.
➤ In the storyboard, select the Current Location View Controller (the Tag Scene), and choose Editor ▸ Embed In ▸ Navigation Controller from Xcode’s menu bar to put it inside a new navigation controller. (This sets up all the views on that particular tab of the tab view controller to be part of a navigation stack.)
➤ Drag a new Table View Controller on to the canvas and put it next to the Tag Scene.
➤ In the Identity inspector, change the Class attribute of the table view controller to LocationDetailsViewController to link it with the source code file you just created.
➤ Control-drag from the Tag Location button on the Tag Scene to the new view controller and create a Show segue. Give the segue the identifier TagLocation.
➤ Add a Navigation Item to the Location Details View Controller, and change the title to Tag Location.
➤ Switch the table content to Static Cells and its style to Grouped.
The storyboard should now looks like this:
Hiding the navigation bar
You’ll notice that the Tag Scene (the Current Location View Controller) now has an empty navigation bar area. This is because it is now embedded in a Navigation Controller. You can either set the title (and/or make it a large title), or, you can hide the navigation bar altogether for the first view.
For this particular app design, having no titles would look the best. So, you now have to hide the navigation bar at runtime for only the Tag Scene. How do you do it?
Simple enough. It’s just a code change!
➤ Switch to CurrentLocationViewController.swift and add a new viewWillAppear implementation:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = true
}
All you do is ask the navigation controller to hide the navigation bar when this particular view is about to appear. Simple as that!
➤ Run the app and make sure the Tag Location button works.
Do you notice an issue when you switch to the Location Details View Controller via the Tag Location button?
The navigation bar on the new screen is hidden as well! Can you guess why this is?
Yep, it’s because you hid the navigation controller’s navigation bar in the previous screen. That setting is not a per-screen setting. It affects the navigation bar for the navigation controller from that point onwards for all views displayed by the navigation controller.
So how do you fix it? Simple enough, ask the navigation controller to start showing the navigation bar as soon as you exit the view where you hide the navigation bar. And there is a handy viewWillDisappear method that you can override in UIViewController that’s just the place for this kind of code.
➤ Add the following method to CurrentLocationViewController.swift:
override func viewWillDisappear(_ animated: Bool) {
super.viewWillAppear(animated)
navigationController?.isNavigationBarHidden = false
}
You simply reverse what you did previously in viewWillAppear by asking the navigation controller to show the navigation bar each time the current view is about to disappear from view — usually, either because another view appeared on top of it, or because this view was dismissed in order to go back to a previous view.
➤ Run the app again and make sure that the navigation flow (and the showing/hiding of the navigation bar) works correctly.
Adding navigation buttons
Of course, the new screen won’t do anything useful yet. Let’s add some buttons.
➤ Drag a Bar Button Item on to the left slot (where the Back button currently is) of the navigation bar. Make it a Cancel button and connect it to the cancel action. If you’re using the Connections inspector, the thing that you’re supposed to connect is the Bar Button Item’s “selector,” under Sent Actions.
Note: A navigation bar usually has left and right navigation item positions where you can drag either bar button items or views on to. If you are unable to drag an item on to the left/right positions of a navigation bar and the scene has a navigation bar, it is possible that the scene is missing a Navigation Item. Then, you have to first drag a Navigation Item on to the scene.
➤ Also drag a Bar Button Item on to the right slot. Set both the Style and System Item attributes to Done, and connect it to the done action.
➤ Run the app again and make sure you can close the Tag Location screen from both buttons after you’ve opened it.
Making the cells
There will be three sections in this table view:
- The description text view and the category cell. These can be changed by the user.
- The photo. Initially this cell says Add Photo but once the user has picked a photo, you’ll display the actual photo inside the cell. It’s good to have that in a section of its own.
- The latitude, longitude, address, and date rows. These are read-only information.
➤ Open the storyboard. Select the table view and go to the Attributes inspector. Change the Sections field from 1 to 3.
When you do this, the contents of the first section are automatically copied to the new sections. That isn’t quite what you want. So, you’ll have to remove some rows here and there. The first section will have 2 rows, the middle section will have just 1 row, and the last section will have 4 rows.
➤ Select one cell in the first section and delete it. If it won’t delete, make sure you selected the whole Table View Cell and not its Content View. The Document Outline can be very useful here.
➤ Delete two cells from the middle section.
➤ Select the last Table View Section object — use the Document Outline for easy selection — and in the Attributes inspector set its Rows to 4.
Alternatively, you can drag a new Table View Cell from the Object Library on to the section.
The right detail cells
The second row from the first section, and the first, second and fourth rows in the last section will all use a standard cell style.
➤ Select these cells — you can select multiple items via the Document Outline by Command-clicking — and set their Style attribute to Right Detail.
The labels in these standard cell styles are regular UILabels. So, you can select them and change their properties.
➤ Change the titles for the labels on the left, from top to bottom to: Category, Latitude, Longitude, and Date.
(If Xcode moves the label when you type into it or cuts off the text, then change the cell style to Left Detail and back again to Right Detail. That seems to fix it.)
➤ Drag a new Label into the cell in the middle section (the one that’s still empty). You cannot use a standard cell style for this cell. So, you’ll design it yourself. Name this label Add Photo. (Later on you’ll also add an image view to this cell.)
➤ Make sure the font of the label is System, size 17, so it’s the same size as the labels from the Right Detail cell style. If necessary, use Editor ▸ Size to Fit Content to resize the label to its optimal size.
➤ Add a left Auto Layout Constraint — with a value of 0, and have Constrain to margins checked — and also add a constraint to center Vertically in Container.
This will add some of the Auto Layout constraints you need to position the label, but not all of them. You will notice that you have a warning still at this point — this is due to the label not having a right constraint. Since we’ll be adding an image to this cell later and that would require changes to the right constraint, we will live with the warning for the time being…
The table should now look like this:
Note: You’re going to make a bunch of changes that are the same for each cell. For some of these, it is easier if you select all the cells at once and then change the setting. That will save you some time.
Unfortunately, some menu items and options are grayed out when you have a multiple selection, so you’ll still have to change some of the settings for each cell individually.
Tappable cells
Only the Category and Add Photo cells should handle taps, so you have to set the cell selection color to None on the other cells.
➤ Select all the cells except Category and Add Photo. In the Attributes inspector, set Selection to None.
➤ Select the Category and Add Photo cells and set Accessory to Disclosure Indicator.
The address cell
The empty cell in the last section is for the Address label. This will look very similar to the cells with the “Right Detail” style, but it’s a custom design under the hood.
➤ Drag a new Label into that cell and set its title to Address.
➤ Add a left Auto Layout constraint (of 0) to the label and also center Vertically in Container.
➤ Drag another Label into the same cell and title it Detail.
➤ Add a right Auto Layout constraint (of 16) to the label and again, center Vertically in Container.
➤ Control-drag from the Address label to the Detail label and select Horizontal Spacing fom the pop up. This will set up the current spacing between the two items as the default spacing. You don’t want that since you want the Detail label to display an address and so it should have room to breath.
➤ Select the Address label, switch to the Size inspector, selec the trailing space constraint and edit the constraint so that the Constant is >= 8 (instead of =). Note that you have to change the operator as well as the numeric constant value.
➤ Make sure the font of both labels is System, size 17.
➤ Change the Alignment of the address detail label to right-aligned.
The detail label is special. Most likely the street address will be too long to fit in that small space. So, you’ll configure this label to have a variable number of lines. This requires a bit of programming in the view controller to make it work, but you also have to set up this label’s attributes properly.
➤ In the Attributes inspector for the address detail label, set Lines to 0 and Line Break to Word Wrap. When the number of lines is 0, the label will resize vertically to fit all the text that you put into it, which is exactly what you need.
The description cell
So far, you’ve left the cell at the top empty. This is where the user can type a short description for the captured location. Currently, there is not much room to type anything. So first, you’ll make the cell larger.
➤ Click on the top cell to select it, then go into the Size inspector and type 88 into the Row Height field.
You can also drag the cell to this new height by the sizing handle at its bottom, but I prefer to simply type in the new value.
The reason to use 88 is that quite a few iOS screen elements have a size of 44 points. The navigation bar is 44 points high, regular table view cells are 44 points high, and so on. Choosing 44 or a multiple of it keeps the UI looking balanced.
➤ Drag a Text View into the cell and add Auto Layout constraints for left: 16, top: 10, right: 16, and bottom: 10, with Constrain to margins unchecked.
➤ By default, Interface Builder puts a whole bunch of Latin placeholder text (Lorem ipsum dolor, etc) into the text view. Replace that text with (Description goes here). The user will never see that text, but it’s handy to remind yourself what this view is for.
➤ Set the font to System, size 17.
One more thing to do, and then the layout is complete. Because the top cell doesn’t have a label to describe what it does — and the text view will initially be empty as well — the user may not know what it is for.
There really isn’t any room to add a label in front of the text view, as you’ve done for the other rows. So, let’s add a header to the section. Table view sections can have a header and footer, and these can either be text or complete views with controls of their own.
➤ Select the top-most Table View Section and in its Attributes inspector type Description into the Header field:
That’s the layout done. The Tag Location screen should look like this in the storyboard:
Now you can actually make the screen do stuff.
Connecting outlets
➤ Connect the Detail labels and the text view to their respective outlets. It should be obvious which one goes where. (Tip: Control-drag from the round yellow icon that represents the view controller to each of the labels. That’s the quickest way.)
If you look at the Connections inspector for this view controller, you should see the following:
➤ Run the app to test whether everything works.
Of course, the screen still says “Detail” in the labels instead of the location’s actual coordinates and address because you haven’t passed in any data yet. Let’s fix that now.
Displaying location info
➤ Add two new properties to LocationDetailsViewController.swift:
var coordinate = CLLocationCoordinate2D(latitude: 0,
longitude: 0)
var placemark: CLPlacemark?
You’ve seen the CLPlacemark class before. It contains the address information — street name, city name, and so on — that you’ve obtained through reverse geocoding. This is an optional because there is no guarantee that the geocoder finds an address for the given coordinates.
CLLocationCoordinate2D is new. This contains the latitude and longitude from the CLLocation object that you received from the location manager. You only need the latitude and longitude, so there’s no point in sending along the entire CLLocation object. The coordinate is not an optional, so you must give it an initial value.
Exercise: Why is coordinate not an optional?
Answer: You cannot tap the Tag Location button unless GPS coordinates have been found. So, you’ll never open the LocationDetailsViewController without a valid set of coordinates.
During the segue from the Current Location screen to the Tag Location screen you will fill in these two properties, and then the Tag Location screen can put these values into its labels.
Xcode isn’t happy with the two lines you just added. It complains about “Use of unresolved identifier CLLocationCoordinate2D” and “CLPlacemark.” That means Xcode does not know anything about these types yet.
That’s because they are part of the Core Location framework – and before you can use anything from a framework, you first need to import it.
➤ Add the following import to the file:
import CoreLocation
Now Xcode’s error messages should disappear after a second or two. If they don’t, use ⌘+B to build the app again.
Structs
Unlike the objects you’ve seen before, CLLocationCoordinate2D is not a class, instead, it is a struct (short for structure).
Structs are like classes, but a little less powerful. They can have properties and methods, but unlike classes, they cannot inherit from one another.
The definition for CLLocationCoordinate2D is as follows:
struct CLLocationCoordinate2D {
var latitude: CLLocationDegrees
var longitude: CLLocationDegrees
}
This struct has two fields, latitude and longitude. Both these fields have the data type CLLocationDegrees, which is a synonym for Double:
typealias CLLocationDegrees = Double
As you probably remember from before, the Double type is one of the primitive types built into Swift. It’s like a Float but with higher precision.
Don’t let these synonyms confuse you; CLLocationCoordinate2D is basically this:
struct CLLocationCoordinate2D {
var latitude: Double
var longitude: Double
}
The reason the designers of Core Location used CLLocationDegrees instead of Double is that “CL Location Degrees” tells you what this type is intended for: it stores the degrees of a location from the Core Location framework.
Underneath the hood it’s a Double, but as a user of Core Location all you need to care about when you want to store latitude or longitude is that you can use the CLLocationDegrees type. The name of the type adds meaning.
UIKit and other iOS frameworks also use structs regularly. Common examples are CGPoint and CGRect. In fact, Array and Dictionary are also structs.
Structs are more lightweight than classes. If you just need to pass around a set of values it’s often easier to bundle them into a struct and pass that struct around, and that is exactly what Core Location does with coordinates.
Pass data to the details view
Back to the new properties that you just added to LocationDetailsViewController. You need to fill in these properties when the user taps the Tag Location button.
➤ Switch to CurrentLocationViewController.swift and add the following code:
// MARK:- Navigation
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
if segue.identifier == "TagLocation" {
let controller = segue.destination
as! LocationDetailsViewController
controller.coordinate = location!.coordinate
controller.placemark = placemark
}
}
You’ve seen how this works before. You use some casting magic to obtain the proper destination view controller and then set its properties. Now when the segue is performed, the coordinate and address are passed on to the Tag Location screen.
Because location is an optional, you need to unwrap it before you can access its coordinate property. It’s perfectly safe to force unwrap at this point because the Tag Location button that triggers the segue won’t be visible unless a location is found. At this point, location will never be nil.
The placemark variable is also an optional, but so is the placemark property on LocationDetailsViewController, so you don’t need to do anything special here. You can always assign the value of one optional to another optional without problems.
Now that you have the values, you need to display them in the Tag Location screen.
Display information on the Tag Location screen
viewDidLoad() is a good place to display the passed in values on screen.
➤ Add the following code to LocationDetailsViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
descriptionTextView.text = ""
categoryLabel.text = ""
latitudeLabel.text = String(format: "%.8f",
coordinate.latitude)
longitudeLabel.text = String(format: "%.8f",
coordinate.longitude)
if let placemark = placemark {
addressLabel.text = string(from: placemark)
} else {
addressLabel.text = "No Address Found"
}
dateLabel.text = format(date: Date())
}
This simply sets a value for every label. It uses two helper methods that you haven’t defined yet: string(from:) to format the CLPlacemark object into a string, and format(date:) to do the same for a Date object.
➤ Add the string(from:) method:
// MARK:- Helper Methods
func string(from placemark: CLPlacemark) -> String {
var text = ""
if let s = placemark.subThoroughfare {
text += s + " "
}
if let s = placemark.thoroughfare {
text += s + ", "
}
if let s = placemark.locality {
text += s + ", "
}
if let s = placemark.administrativeArea {
text += s + " "
}
if let s = placemark.postalCode {
text += s + ", "
}
if let s = placemark.country {
text += s
}
return text
}
This is fairly straightforward. It is similar to how you formatted the placemark on the main screen, except that you also include the country here.
Note: You might have noticed the
// MARKcomments all over the previous sections of code in this chapter. You already know what the// MARKcomment does. So, I’m not going to explain that again.You can feel free to leave the comments out when you type in your own code, but it’s recommended to organize code into identifiables sections as seen above so that you can navigate the code easily. It’s totally up to you whether you use this, create an organization style of your own, or use no organization at all…
Date formatting
To format the date, you’ll use a DateFormatter object. You’ve seen this class at work in the previous app. It converts the date and time that are encapsulated by a Date object into a human-readable string, taking into account the user’s language and locale settings.
For Checklists you created a new instance of DateFormatter every time you wanted to convert a Date to a string. Unfortunately, DateFormatter is a relatively expensive object to create. In other words, it takes a while to initialize this object. If you do that many times over, then it may slow down your app (and drain the phone’s battery faster).
It is better to create DateFormatter just once and then re-use that same object over and over. The trick is that you won’t create the DateFormatter object until the app actually needs it. This principle is called lazy loading and it’s a very important pattern for iOS apps — the work that you don’t do won’t cost any battery power.
In addition, you’ll only ever create one instance of DateFormatter. The next time you need to use DateFormatter you won’t make a new instance but re-use the existing one.
To pull this off you’ll use a private global constant. That’s a constant that lives outside of the LocationDetailsViewController class (global) but it is only visible inside the LocationDetailsViewController.swift file (private).
➤ Add the following to the top of LocationDetailsViewController.swift, in between the import and class lines:
private let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
formatter.timeStyle = .short
return formatter
}()
What is going on here? You’re creating a new constant named dateFormatter of type DateFormatter, that much should be obvious. This constant is private so it cannot be used outside of this Swift file. (Remember the discussion about private and public attributes in the previous chapter?)
You’re also giving dateFormatter an initial value, but what follows the = is not an ordinary value — it looks like a bunch of source code in between { } brackets. That looks like a clousre, doesn’t it? That’s because it is a closure.
Normally, you’d create a new object like this:
private let dateFormatter = DateFormatter()
But to initialize the date formatter it’s not enough to just make an instance of DateFormatter, you also want to set the dateStyle and timeStyle properties of this instance.
To create the object and set its properties in one go, you can use a closure:
private let dateFormatter: DateFormatter = {
// the code that sets up the DateFormatter object
return formatter
}()
The closure contains the code that creates and initializes the new DateFormatter object, and then returns it. This returned value is what gets put into dateFormatter.
The trick to making this work is the () at the end. Closures are like functions, and to perform the code inside the closure you call it just like you’d call a function.
Note: If you leave out the
(), Swift thinks you’re assigning the closure itself todateFormatter— in other words,dateFormatterwill contain a block of code, not an actualDateFormatterobject. That’s not what you want.Instead, you want to assign the result of that closure to
dateFormatter. To make that happen, you use the()to perform or evaluate the closure — this runs the code inside the closure and returns aDateFormatterobject.
Using a closure to create and configure an object all at once is a nifty trick; you can expect to see this often in Swift programs.
In Swift, globals are always created in a lazy fashion, which means the code that creates and sets up this DateFormatter object isn’t performed until the very first time the dateFormatter global is used in the app.
That happens inside the new format(date:) method.
➤ Add the new method — this code goes inside the class (and would generally be put in the helper methods section created in the previous // MARK comment, for organizational purposes):
func format(date: Date) -> String {
return dateFormatter.string(from: date)
}
How simple is that? It just asks the DateFormatter to turn the Date into a String and returns that.
Exercise: How can you verify that the date formatter is really only created once?
Answer: Add a print() just before the return formatter line in the closure. This print() text should appear only once in the Xcode Console.
➤ Run the app. Choose the Apple location from the Simulator’s Debug menu. Wait until the street address is visible and then press the Tag Location button.
The coordinates, address and date are all filled in:
The address seems to be having some trouble fitting in!
Content Compression Resistance
You earlier configured the label to fit multiple lines of text, but the problem is that the two labels in the addres row don’t know how to get along with each other — the detail label is too full of itself and encroaches on the space of the Address label.
The solution is simple enough — Content Compression Resistance. Quite a mouthful, and not very illuminating, right?
Let me try to shed some light.
➤ Select the Address label, switch to the Size inspector and scroll to the bottom. You should see a section named Content Compression Resistance Priority.
This section determines how easily the selected control allows other controls to push it (and its content) out of the way to present their own content. The higher the priority, the less likely this control is to be pushed out of the way. All controls have a horizontal and vertical content compression resistance value set and this is by default set to 750. All we need to do is increase the Address label’s vertical content resistance priority so that it doesn’t get pushed around.
➤ Change the Horizontal value to 751.
➤ Run the app. Now the reverse geocoded address should completely fit in the Address cell (even on larger screens). Try it out with a few different locations.
The category picker
When the user taps the Category cell, the app should show a list of category names:
The view controller class
This is a new screen, so you need a new view controller. The way this works is very similar to the icon picker from Checklists. I’m just going to give you the source code and tell you how to hook it up.
➤ Add a new file to the project named CategoryPickerViewController.swift.
➤ Replace the contents of CategoryPickerViewController.swift with:
import UIKit
class CategoryPickerViewController: UITableViewController {
var selectedCategoryName = ""
let categories = [
"No Category",
"Apple Store",
"Bar",
"Bookstore",
"Club",
"Grocery Store",
"Historic Building",
"House",
"Icecream Vendor",
"Landmark",
"Park"]
var selectedIndexPath = IndexPath()
override func viewDidLoad() {
super.viewDidLoad()
for i in 0..<categories.count {
if categories[i] == selectedCategoryName {
selectedIndexPath = IndexPath(row: i, section: 0)
break
}
}
}
// MARK:- Table View Delegates
override func tableView(_ tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
return categories.count
}
override func tableView(_ tableView: UITableView,
cellForRowAt indexPath: IndexPath) ->
UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: "Cell",
for: indexPath)
let categoryName = categories[indexPath.row]
cell.textLabel!.text = categoryName
if categoryName == selectedCategoryName {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
override func tableView(_ tableView: UITableView,
didSelectRowAt indexPath: IndexPath) {
if indexPath.row != selectedIndexPath.row {
if let newCell = tableView.cellForRow(at: indexPath) {
newCell.accessoryType = .checkmark
}
if let oldCell = tableView.cellForRow(
at: selectedIndexPath) {
oldCell.accessoryType = .none
}
selectedIndexPath = indexPath
}
}
}
There’s nothing special going on here. This is a table view controller that shows a list of category names. The table gets its rows from the categories array.
The only thing worth noting is the selectedIndexPath instance variable. When the screen opens, it shows a checkmark next to the currently selected category. This comes from the selectedCategoryName property, which is filled in when you segue to this screen.
When the user taps a row, you want to remove the checkmark from the previously selected row and put it in the new row.
In order to be able to do that, you need to know which row is the currently selected one. You can’t use selectedCategoryName for this because that is a string, not a row number. Therefore, you first need to find the row number — or index-path — for the selected category name.
That happens in viewDidLoad(). You loop through the array of categories and compare the name of each category to selectedCategoryName. If they match, you create an index-path object and store it in the selectedIndexPath variable. Once a match is found, you can break out of the loop because there’s no point in looping through the rest of the categories.
Now that you know the row number, you can remove the checkmark for this row in tableView(_:didSelectRowAt:) when another row gets tapped.
It’s a bit of work for such a small feature, but in a good app it’s the details that matter.
There are several different ways of looping through the contents of an array.
You’ve already seen for...in, which is used as follows:
for category in categories {
This puts the name of each category into a temporary constant named category.
However, in order to make the index-path object, you don’t want the name of the category but the index of that category in the array. So you’ll have to loop in a slightly different fashion:
for i in 0..<categories.count {
let category = categories[i]
. . .
}
Thanks to the half-open range operator ..<, i is a number that increments from 0 to categories.count – 1. This is a very common pattern for looping through an array if you want to have the index as well.
Another way to do this is to use the enumerated() method, for which you’ll see an example when you get to the next app. As a quick preview, this is how you’d use it:
for (i, category) in categories.enumerated() {
. . .
}
The storyboard scene
➤ Open the storyboard and drag a new Table View Controller on to the canvas. Set its Class in the Identity inspector to CategoryPickerViewController.
➤ Change the Style of the prototype cell to Basic, and give it the re-use identifier Cell.
➤ Control-drag from the Category cell on the Location Details View Controller to this new view controller and choose Selection Segue — Show.
➤ Give the segue the identifier PickCategory.
The Category Picker View Controller now has a navigation bar at the top. You could change its title to “Choose Category,” but Apple recommends that you do not give view controllers a title if their purpose is obvious.
This helps to keep the navigation bar uncluttered.
That’s enough for the storyboard. Now all that remains is to handle the segue.
The Segue
➤ Switch back to LocationDetailsViewController.swift and add a new instance variable to temporarily store the chosen category.
var categoryName = "No Category"
Initially you set the category name to “No Category,” which is the category at the top of the list in the category picker.
➤ Change viewDidLoad() to put categoryName into the label:
override func viewDidLoad() {
. . .
categoryLabel.text = categoryName // change this line
. . .
➤ Finally, add the segue handling code:
// MARK:- Navigation
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
if segue.identifier == "PickCategory" {
let controller = segue.destination as!
CategoryPickerViewController
controller.selectedCategoryName = categoryName
}
}
This simply sets the selectedCategoryName property of the category picker. And with that, the app has categories.
➤ Run the app and play with the category picker.
Hmm, it doesn’t seem to work very well. You can choose a category, but the screen doesn’t close when you tap a row. When you press the back button, the category you picked isn’t shown on the parent screen.
Exercise: Which piece of the puzzle is missing?
Answer: The CategoryPickerViewController currently does not have a way to communicate back to the LocationDetailsViewController about the user selection.
At this point you might be thinking, “Of course, dummy! You forgot to give the category picker a delegate protocol. That’s why it cannot send any messages to the other view controller.” (If so, awesome! You’re getting the hang of this.)
A delegate protocol is a fine solution indeed, but there’s a handy storyboarding feature that can accomplish the same thing with less work: unwind segues.
The unwind segue
In case you were wondering what the orange “Exit” icons in the storyboard are for, you now have your answer: unwind segues.
Where a regular segue is used to open a new screen, an unwind segue closes the active screen. Sounds simple enough. However, making unwind segues is not very intuitive.
The orange Exit icons don’t appear to do anything. Try Control-dragging from the prototype cell to the Exit icon, for example. It won’t let you make a connection.
First, you have to add a special type of action method to the destination of the unwind segue.
➤ In LocationDetailsViewController.swift, add the following method:
@IBAction func categoryPickerDidPickCategory(
_ segue: UIStoryboardSegue) {
let controller = segue.source as! CategoryPickerViewController
categoryName = controller.selectedCategoryName
categoryLabel.text = categoryName
}
You can see that this is an action method because it has the @IBAction annotation. What’s different from a regular action method is the parameter, a UIStoryboardSegue object.
Normally, if an action method has a parameter, it points to the control that triggered the action, such as a button or slider. But in order to make an unwind segue, you need to define an action method that takes a UIStoryboardSegue parameter.
What happens inside the method is pretty straightforward. You look at the view controller that sent the segue (the source), which of course is the CategoryPickerViewController, and then read the value of its selectedCategoryName property. That property contains the category that the user picked.
Now, to use this new method in the storyboard…
➤ Open the storyboard. Control-drag from the prototype cell in the Category Picker scene to the Exit button. This time it allows you to make a connection:
From the pop-up choose Selection Segue — categoryPickerDidPickCategory:, the name of the unwind action method you just added.
If Interface Builder doesn’t let you make a connection, then make sure you’re really Control-dragging from the Cell, not from its Content View or the label.
Now when you tap a cell in the category picker, the screen closes and this new method is called.
➤ Run the app to try it out.
That was easy! Well, not quite. Unfortunately, the chosen category is ignored…
That’s because categoryPickerDidPickCategory() looks at the selectedCategoryName property, but that property isn’t set anywhere in your code yet.
You need some kind of mechanism that is invoked when the unwind segue is triggered, at which point you can fill in the selectedCategoryName based on the row that was tapped.
What might such a mechanism be called? prepare(for:sender:), of course! This works for segues in both directions.
➤ Add the following method to CategoryPickerViewController.swift:
// MARK:- Navigation
override func prepare(for segue: UIStoryboardSegue,
sender: Any?) {
if segue.identifier == "PickedCategory" {
let cell = sender as! UITableViewCell
if let indexPath = tableView.indexPath(for: cell) {
selectedCategoryName = categories[indexPath.row]
}
}
}
This looks at the selected index-path and puts the corresponding category name into the selectedCategoryName property.
This logic assumes the unwind segue is named “PickedCategory,” so you still have to set an identifier on the unwind segue.
Unfortunately, there is no visual representation of that unwind segue in the storyboard. There is no nice, big arrow that you can click on. To select the unwind segue you have to locate it in the Document Outline:
➤ Select the unwind segue and go to the Attributes inspector. Give it the identifier PickedCategory.
➤ Run the app. Now the category picker should work properly. As soon as you tap the name of a category, the screen closes and the new category name is displayed.
Unwind segues are pretty cool and are often easier than using a delegate protocol, especially for simple picker screens such as this one.
You can find the project files for this chapter under 25 - Tag Location Screen in the Source Code folder.