One of the most common mistakes I see iOS programming beginners make on Stack Overflow is creating an instance when they intend to refer to an instance that already exists.
This mistake typically involves a view controller. People say
let vc = MyViewController()
when that isn’t really what they want to say at all. There are two things that they usually mean when they make this mistake:
-
They want to refer to an existing view controller that’s already in the interface (the view controller hierarchy).
-
They want to refer to a storyboard view controller instance.
In this post, I’ll talk about the first version of the mistake. In the next post, I’ll talk about the second version.
Here’s an actual example from Stack Overflow. As so often happens, the issue comes in two parts, so bear with me.
This programmer has two view controllers, a parent view controller and a child view controller. He wants the parent to be able to send a message to the child. So he has used the protocol-and-delegate pattern. He has created a Delegate protocol and he’s given the parent a delegate
property:
protocol Delegate : class{
func protocolMethod(count:Int)
}
class ParentVC : UIViewController{
var delegate : Delegate?
var count : Int = 1
func myMethod(){
self.delegate?.protocolMethod(count : self.count)
}
}
The idea is that when myMethod
is called in the parent view controller, it sends a message to the parent view controller’s delegate
. Sounds great, right? The problem, though, is that in real life nothing happens. This line is not having any effect:
self.delegate?.protocolMethod(count : self.count)
The programmer has done some debugging and has figured out why: self.delegate
is nil
. The delegate
is never getting set to anything, so there is no one to send the protocolMethod
message to!
Why isn’t the delegate
being set? Well, let’s look at the code where the parent’s delegate
property is supposed to be set:
class ChildVC : UIViewController, Delegate{
func protocolMethod(count : Int){
print(count)
}
override func viewDidLoad() {
super.viewDidLoad()
let pvc = ParentVC() // this is the mistake!
pvc.delegate = self
}
}
When the ChildVC comes into existence and its view is loaded, its viewDidLoad
is called. As everyone knows, that’s a typical moment to perform initializations. So the ChildVC talks to the ParentVC (pvc
) and sets its delegate
to self
. Right?
Wrong! This is the mistake I’m talking about. The bad line here is this one:
let pvc = ParentVC()
Saying ParentVC()
is not a way of talking to the ParentVC. It does something totally different and unwanted: it creates a second ParentVC. Meanwhile, the “real” ParentVC is just sitting there, still with no delegate.
If you don’t understand why that’s happening, I need to start at the very beginning, with the nature of object-oriented programming. I need to tell you a little about classes and instances.
Classes and instances
A class is basically just a type of thing, a set of instructions for what a thing should do when it exists. For example, we might know that a Dog should have four legs and bark, but that doesn’t mean there are actually any dogs in existence. An instance is an actual thing that does exist. You can make as many instances of a class as you like; each instance that you make is a different thing from the other instances of the same class.
In Swift and iOS programming, there are two main ways of making an instance. One is to call the class’s init
, which is usually done just by putting parentheses after the class name:
let fido = Dog() // creates a Dog instance
let rover = Dog() // creates a new different Dog instance
After that code, fido
and rover
are two different Dog instances.
Okay, but now let’s go back to the programmer’s code:
let pvc = ParentVC()
What does that code do? It makes a new different instance of ParentVC. Different from what? Different from the instance the programmer actually wants to talk to, the one that already exists, the one whose view is already present and visible in the interface!
That’s right. The problem is that there are now two ParentVC instances, and in the next line, when he sets the delegate, the programmer is talking to the wrong instance:
let pvc = ParentVC() // creates a whole new ParentVC instance
pvc.delegate = self // talking to that new ParentVC instance
So where is the right instance, and how did it come into existence in the first place? Why is this programmer so confused about this? It’s probably because of storyboards…
How storyboards confuse people
Storyboards (actually, nibs in general) are an important source of instances in iOS programming — especially instances of UIViewController subclasses. This is another way that instances come into existence in iOS programming. A “scene” in a storyboard is loaded and the runtime makes a view controller instance for you.
Let’s say, for example, that this app has a main storyboard with a ParentVC as its initial view controller. So the app launches, the storyboard’s initial scene is loaded automatically, and a ParentVC instance is created automatically. This instance is retained automatically, and its view appears in the interface automatically.
You, the programmer, didn’t do any of that directly, so you may not be conscious of what has happened. But that ParentVC — the one that got created from the storyboard, the one that is in the interface already — that is the ParentVC we now need to talk to in order to set its delegate.
Getting a reference to an existing view controller
Okay, but you may now be thinking that this leaves us no better off than we were before. We now understand what the programmer was doing wrong. But what is the right answer? We know that the programmer needs to talk to the already existing ParentVC instance, the one whose view is already visible in the app’s interface. And we know that saying ParentVC()
is not how to do that. But how do we do that?
The answer is: I don’t know! It all depends on the structure of your app. It is up to you to know where the view controller is, in the hierarchical architecture of view controllers in your running app, and to work out a way to talk to it when you need to.
View controllers have lots of “relationship” properties built right in, allowing you to locate them in the view controller hierarchy. For example:
-
The root view controller of the entire app is
UIApplication.shared.keyWindow?.rootViewController
. -
If a view controller presents another view controller, they are one another’s
self.presentedViewController
andself.presentingViewController
. -
If a view controller is in a UINavigationController interface and it pushes another view controller, the newly pushed view controller is
self.navigationController?.viewControllers.last
(as seen from any other view controller on the navigation controller’s stack). Getting the next to last view controller on the navigation controller’s stack is a bit more elaborate; you might say something like this:if let vcs = self.navigationController?.viewControllers { let prevVC = vcs[vcs.count-2] }
-
If a view controller is a UIPageViewController’s child, the page view controller is its
self.parent
. -
Similarly, if you have a custom parent view controller and its child view controller, they are one another’s
self.parent
andself.children[0]
.
Using those and similar techniques, you can usually find your way around the view controller hierarchy to get a reference to the already existing view controller you want to talk to.
(By the way, I suspect that in the situation this programmer was asking about, a Delegate protocol was always the wrong way to go. The two view controllers in question already stand in some definite relationship to one another, such as parent and child; that should be enough for them to be able to locate and talk to one another.)