Loading images with UIImage
Loading images with UIImage êŽë š
At this point we have our original table view controller full of pictures to select, plus a detail view controller in our storyboard. The next goal is to show the detail screen when any table row is tapped, and have it show the selected image.
To make this work we need to add another specially named method to ViewController
. This one is called tableView(_, didSelectRowAt:)
, which takes an IndexPath
value just like cellForRowAt
that tells us what row weâre working with. This time we need to do a bit more work:
- We need to create a property in
DetailViewController
that will hold the name of the image to load. - Weâll implement the
didSelectRowAt
method so that it loads aDetailViewController
from the storyboard. - Finally, weâll fill in
viewDidLoad()
insideDetailViewController
so that it loads an image into its image view based on the name we set earlier.
Letâs solve each of those in order, starting with the first one: creating a property in DetailViewController
that will hold the name of the image to load.
This property will be a string â the name of the image to load â but it needs to be an optional string because when the view controller is first created it wonât exist. Weâll be setting it straight away, but it still starts off life empty.
So, add this property to DetailViewController
now, just below the existing @IBOutlet
line:
var selectedImage: String?
Thatâs the first task done, so onto the second: implement didSelectRowAt
so that it loads a DetailViewController
from the storyboard.
When we created the detail view controller, you gave it the storyboard ID âDetailâ, which allows us to load it from the storyboard using a method called instantiateViewController(withIdentifier:)
. Every view controller has a property called storyboard
that is either the storyboard it was loaded from or nil. In the case of ViewController
it will be Main.storyboard
, which is the same storyboard that contains the detail view controller, so weâll be loading from there.
We can break this task down into three smaller tasks, two of which are new:
- Load the detail view controller layout from our storyboard.
- Set its
selectedImage
property to be the correct item from thepictures
array. - Show the new view controller.
The first of those is done by calling instantiateViewController
, but it has two small complexities. First, we call it on the storyboard
property that we get from Appleâs UIViewController
type, but itâs optional because Swift doesnât know we came from a storyboard. So, we need to use ?
just like when we were setting the text label of our cell: âtry doing this, but do nothing if there was a problem.â
Second, even though instantiateViewController()
will send us back a DetailViewController
if everything worked correctly, Swift thinks it will return back a UIViewController
because it canât see inside the storyboard to know whatâs what.
This will seem confusing if youâre new to programming, so let me try to explain using an analogy. Letâs say you want to go out on a date tonight, so you ask me to arrange a couple of tickets to an event. I go off, find tickets, then hand them to you in an envelope. I fulfilled my part of the deal: you asked for tickets, I got you tickets. But what tickets are they â tickets for a sporting event? Tickets for an opera? Train tickets? The only way for you to find out is to open the envelope and look.
Swift has the same problem: instantiateViewController()
has the return type UIViewController
, so as far as Swift is concerned any view controller created with it is actually a UIViewController
. This causes a problem for us because we want to adjust the property we just made in DetailViewController
. The solution: we need to tell Swift that what it has is not what it thinks it is.
The technical term for this is âtypecastingâ: asking Swift to treat a value as a different type. Swift has several ways of doing this, but weâre going to use the safest version: it effectively means, âplease try to treat this as a DetailViewController, but if it fails then do nothing and move on.â
Once we have a detail view controller on our hands, we can set its selectedImage
property to be equal to pictures[indexPath.row]
just like we were doing in cellForRowAt
â thatâs the easy bit.
The third mini-step is to make the new screen show itself. You already saw that view controllers have an optional storyboard
property that either contains the storyboard they were loaded from or nil. Well, they also have an optional navigationController
property that contains the navigation controller they are inside if it exists, or nil otherwise.
This is perfect for us, because navigation controllers are responsible for showing screens. Sure, they provide that nice gray bar across the top that you see in lots of apps, but they are also responsible for maintaining a big stack of screens that users navigate through.
By default they contain the first view controller you created for them in the storyboard, but when new screens are created you can push them onto the navigation controllerâs stack to have them slide in smoothly just like you see in Settings. As more screens are pushed on, they just keep sliding in. When users go back a screen â i.e. by tapping Back or by swiping from left to right â the navigation controller will automatically destroy the old view controller and free up its memory.
Those three mini-steps complete the new method, so itâs time for the code. Please add this method to ViewController.swift
â Iâve added comments to make it easier to understand:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// 1: try loading the "Detail" view controller and typecasting it to be DetailViewController
if let vc = storyboard?.instantiateViewController(withIdentifier: "Detail") as? DetailViewController {
// 2: success! Set its selectedImage property
vc.selectedImage = pictures[indexPath.row]
// 3: now push it onto the navigation controller
navigationController?.pushViewController(vc, animated: true)
}
}
Letâs look at the if let
line a bit more closely for a moment. There are three parts of it that might fail: the storyboard
property might be nil (in which case the ?
will stop the rest of the line from executing), the instantiateViewController()
call might fail if we had requested âFzzzzzâ or some other invalid storyboard ID, and the typecast â the as?
part â also might fail, because we might have received back a view controller of a different type.
So, three things in that one line have the potential to fail. If youâve followed all my steps correctly they wonât fail, but they have the potential to fail. Thatâs where if let
is clever: if any of those things return nil (i.e., they fail), then the code inside the if let
braces wonât execute. This guarantees your program is in a safe state before any action is taken.
Thereâs only one small thing left to do before you can take a look at the results: we need to make the image actually load into the image view in DetailViewController
.
This new code will draw on a new data type, called UIImage
. This doesn't have "View" in its name like UIImageView
does, so it's not something you can view â it's not something that's actually visible to users. Instead, UIImage
is the data type you'll use to load image data, such as PNG or JPEGs.
When you create a UIImage
, it takes a parameter called named
that lets you specify the name of the image to load. UIImage
then looks for this filename in your app's bundle, and loads it. By passing in the selectedImage
property here, which was sent from ViewController
, this will load the image that was selected by the user.
However, we canât use selectedImage
directly. If you remember, we created it like this:
var selectedImage: String?
That ?
means it might have a value or it might not, and Swift doesnât let you use these âmaybesâ without checking them first. This is another opportunity for if let
: we can check that selectedImage
has a value, and if so pull it out for usage; otherwise, do nothing.
Add this code to viewDidLoad()
inside DetailViewController
, after the call to super.viewDidLoad()
:
if let imageToLoad = selectedImage {
imageView.image = UIImage(named: imageToLoad)
}
The first line is what checks and unwraps the optional in selectedImage
. If for some reason selectedImage
is nil (which it should never be, in theory) then the imageView.image
line will never be executed. If it has a value, it will be placed into imageToLoad
, then passed to UIImage
and loaded.
OK, thatâs it: press play or Cmd+R now to run the app and try it out! You should be able to select any of the pictures to have them slide in and displayed full screen.
Notice that we get a Back button in the navigation bar that lets us return back to ViewController
. If you click and drag carefully, youâll find you can create a swipe gesture too â click at the very left edge of the screen, then drag to the right, just as you would do with your thumb on a phone.