Chapter 13. Communication Between Objects

As soon as an app grows to more than a few objects, puzzling questions can arise about how to send a message or communicate data between one object and another. It may require some planning to construct your code so that all the pieces fit together and information can be shared as needed at the right moment. This chapter presents some organizational considerations that will help you arrange for one object to be able to communicate with another.

Visibility Through an Instance Property

The problem of communication often comes down to one object being able to see another: the object Manny needs to be able to find the object Jack repeatedly and reliably over the long term so as to be able to send Jack messages. One obvious solution is an instance property of Manny whose value is Jack.

An instance property is appropriate particularly when Manny and Jack share certain responsibilities or supplement one another’s functionality. Here are some cases where one object needs to have an instance property pointing at another:

  • The application object and its delegate

  • A table view and its data source

  • A view controller and the view that it controls

Manny may have an instance property pointing to Jack, but this does not necessarily imply that Manny needs to assert ownership of Jack as a matter of memory management policy (see Chapter 12):

  • An object does not typically retain its delegate or its data source.

  • An object that implements the target–action pattern, such as a UIControl, does not retain its target.

By using a weak reference and typing the property as an Optional, and then treating the Optional coherently and safely, Manny can keep a reference to Jack without owning Jack (while coping with the possibility that his supposed reference to Jack will turn out to be nil). On the other hand, sometimes ownership is appropriate and crucial. A view controller is useless without a view to control, and its view truly belongs to the view controller and to no one else; once a view controller has a view, it will retain it, releasing it only when it itself goes out of existence.

Objects can perform two-way communication without both of them holding references to one another. It may be sufficient for one of them to have a reference to the other — because the former, as part of a message to the latter, can include a reference to himself. Manny might send a message to Jack where one of the parameters is a reference to Manny; this might merely constitute a form of identification, or an invitation to Jack to send a message back to Manny if Jack needs further information while doing whatever this method does. Manny makes himself, as it were, momentarily visible to Jack; Jack should not wantonly retain Manny (especially since there’s an obvious risk of a retain cycle). Again, this is a common pattern:

  • The parameter of the delegate message textFieldShouldBeginEditing(_:) is a reference to the UITextField that sent the message.

  • The first parameter of a target–action message is a reference to the control that sent the message.

Visibility by Instantiation

Every instance comes from somewhere and at someone’s behest: some object sent a message commanding this instance to come into existence in the first place. The commanding object therefore has a reference to the new instance at the moment of instantiation. When Manny creates Jack, Manny has a reference to Jack.

That simple fact can serve as the starting point for establishing future communication. If Manny creates Jack and knows that he (Manny) will need a reference to Jack later on, Manny can keep the reference that he obtained by creating Jack in the first place.

Or it might be the other way around: Manny creates Jack and knows that Jack will need a reference to Manny later on, so Manny can supply that reference immediately after creating Jack, and Jack will then keep it. Delegation is a case in point. Manny may create Jack and immediately make himself Jack’s delegate, as in my example code in Chapter 11:

let cpc = ColorPickerController(colorName:colorName, color:c)
cpc.delegate = self

When Manny creates Jack, it might not be a reference to Manny himself that Jack needs, but to something that Manny knows or has. You will presumably endow Jack with a method or property so that Manny can hand over that information. In fact, if Jack simply cannot live without the information, it might be reasonable to endow Jack with an initializer that requires this information as part of the very act of creation.

This example (Chapter 11) comes from a table view controller. The user has tapped a row of the table. In response, we create a secondary table view controller, a TracksViewController instance; we hand it the data it will need, and display the secondary table view:

override func tableView(_ tableView: UITableView,
    didSelectRowAt indexPath: IndexPath) {
        delay(0.1) {
            let t = TracksViewController(
                mediaItemCollection: self.albums[indexPath.row])
            self.navigationController?.pushViewController(t, animated: true)
        }
}

In that code, I instantiate the TracksViewController by calling its initializer, init(mediaItemCollection:), which requires me to hand over the media item collection that the view controller will need as the basis of its table view. And where did this initializer come from? I made it up! I have deliberately devised TracksViewController to have a designated initializer init(mediaItemCollection:), making it virtually obligatory for a TracksViewController to have access, from the moment it comes into existence, to the data it needs.

In that example, I (self) create the TracksViewController instance, and so, for one brief shining moment, I have a reference to it. Therefore I take advantage of that moment to hand the TracksViewController instance the information it needs. There will be no better moment to do this. Knowing the moment, and taking care not to miss it, is part of the art of data communication.

Nib-loading is also a case in point. The loading of a nib is a way of instantiating objects from the nib. Proper preparation is essential in order to ensure that those objects are assigned to strong references, so that they don’t simply vanish in a puff of smoke (“Nib Loading and Memory Management”). At the moment the nib loads, the nib’s owner or the code that loads the nib is in contact with those objects; it takes advantage of that moment to secure those references.

Another key moment is when a segue in a storyboard is triggered. There are two view controllers that may need to meet — the view controllers at the two ends of the segue, the source view controller and the destination view controller. This is parallel to the situation where one view controller creates another view controller and presents it, but there’s an important difference: with a triggered segue, the source view controller doesn’t create the destination view controller. But it probably still needs a reference to the destination view controller, very early in the life of the latter, so that it can hand over any needed information. How will it get that reference?

At the moment the segue is triggered, the source view controller already exists, and the segue knows what view controller it is; and the segue itself instantiates the destination view controller. So the segue immediately turns to the source view controller and hands it a reference to the destination view controller — for example, by calling the source view controller’s prepare(for:sender:) method. This is the source view controller’s chance to obtain a reference to the newly instantiated destination view controller — and to make itself the destination view controller’s delegate, or hand it any needed information, and so forth.

Getting a Reference

A source of particular frustration arises when you know that another object exists somewhere out there but you don’t know how to refer to it.

Let’s say you’re a view controller and there’s some other view controller you need to talk to. But you didn’t instantiate the other view controller, and you are not the source view controller for the segue that instantiated the other view controller. You know a lot about this other view controller — you know what its class is, and you can probably see its view sitting there in the interface when you run the app — but you cannot get hold of it in code.

Here’s what not to do in that situation: You know the class of the view controller you’re looking for, so you make an instance of that class. That won’t do you any good! The instance you want to talk to is a particular instance that already exists. There’s no point making another instance. If you lose your car in a parking lot, the solution is not to build another car of the same model; the solution is to find your car.

For example, in a real-life iOS app, you will have a root view controller, which will be an instance of some type of UIViewController. Let’s say it’s an instance of the ViewController class. Once your app is up and running, this instance already exists. Now suppose we are in some other view controller, and we want to talk to the ViewController instance that is serving as the root view controller of the app. It would be counterproductive to try to speak to the root view controller by instantiating the ViewController class:

let theVC = ViewController() // legal but pointless

All that does is to make a second, different instance of the ViewController class, and your messages to that instance will be wasted, as it is not the instance of ViewController that you wanted to talk to. That particular instance already exists; what you want is to get a reference to that already existing instance. But how? Here are some considerations that will help you.

Visibility by Relationship

It is not the class of an already existing object that will get you a reference to that object, but rather the relationship between you and that object. Objects may acquire the ability to see one another automatically by virtue of their position in a containing structure. Before worrying about how to supply one object with a reference to another, consider whether there may already be a chain of references leading from one to the other.

A subview can see its superview, through its superview property. A superview can see all its subviews, through its subviews property, and can pick out a specific subview through that subview’s tag property, by calling the viewWithTag(_:) method. A subview in a window can see its window, through its window property. Working your way up or down the view hierarchy by means of these properties, it may be possible to obtain the desired reference.

A view controller can see its view through its view property, and from there can work its way down to subviews to which it may not have an outlet. What about going in the other direction? A responder (Chapter 11) can see the next object up the responder chain, through the next property — which also means, because of the structure of the responder chain, that a view controller’s main view can see the view controller.

View controllers are themselves part of a hierarchy and therefore can see one another. If a view controller is currently presenting a view through a second view controller, the latter is the former’s presentedViewController, and the former is the latter’s presentingViewController. If a view controller is the child of a UINavigationController, the latter is its navigationController. A UINavigationController’s visible view is controlled by its visibleViewController. And so forth.

Global Visibility

Some objects are globally visible — that is, they are visible to all other objects. Object types themselves are an important example. As I pointed out in Chapter 4, it is perfectly reasonable to use a Swift struct with static members as a way of providing globally available namespaced constants (“Struct as Namespace”).

Classes sometimes have class methods or properties that vend singleton instances. Some of these singletons, in turn, have properties pointing to other objects, making those other objects likewise globally visible. Any object can see the singleton UIApplication instance as UIApplication.shared. So any object can also see the app’s primary window, because that is the first element of the singleton UIApplication instance’s windows property, and any object can see the app delegate, because that is its delegate property. And the chain continues: any object can see the app’s root view controller, because that is the primary window’s rootViewController — and from there, as I said in the previous section, we can navigate the view controller hierarchy and the view hierarchy.

So now we know how to solve the problem I posed earlier of getting a reference to the app’s root view controller. We start with the globally visible shared application instance:

let app = UIApplication.shared

From there we can get the window:

let window = app.windows.first

That window owns the root view controller, and will hand us a reference to it through its rootViewController property:

let vc = window?.rootViewController

And voilà — a reference to our app’s root view controller. To obtain the reference to this persistent instance, we have created, in effect, a chain leading from the known to the unknown, from a globally available class to the particular desired instance.

You can make your own objects globally visible by attaching them to a globally visible object. For example, a public property of the app delegate, which you are free to create, is globally visible by virtue of the app delegate being globally visible (by virtue of the shared application being globally visible).

Another globally visible object is the shared defaults object obtained as UserDefaults.standard. This object is the gateway to storage and retrieval of user defaults, which is similar to a dictionary (a collection of values named by keys). The user defaults are automatically saved when your application quits and are automatically available when your application is launched again later, so they are one of the ways in which your app maintains information between launches. But, being globally visible, they are also a conduit for communicating values within your app.

In one of my apps there’s a preference setting I call Default.hazyStripy. This determines whether a certain visible interface object (a card in a game) is drawn with a hazy fill or a stripy fill. This is a setting that the user can change, so there is a preferences interface allowing the user to make this change. When the user displays this preferences interface, I examine the Default.hazyStripy setting in the user defaults to configure the preferences interface to reflect it in a segmented control (called self.hazyStripy):

func setHazyStripy () {
    let hs = UserDefaults.standard
        .object(forKey:Default.hazyStripy) as! Int
    self.hazyStripy.selectedSegmentIndex = hs
}

Conversely, if the user interacts with the preferences interface, tapping the hazyStripy segmented control to change its setting, I respond by changing the actual Default.hazyStripy setting in the user defaults:

@IBAction func hazyStripyChange(_ sender: Any) {
    let hs = self.hazyStripy.selectedSegmentIndex
    UserDefaults.standard.set(hs, forKey: Default.hazyStripy)
}

But here’s the really interesting part. The preferences interface is not the only object that uses the Default.hazyStripy setting in the user defaults; the drawing code that actually draws the hazy-or-stripy-filled card also uses it, so as to know how the card should draw itself! When the user leaves the preferences interface and the card game reappears, the cards are redrawn — consulting the Default.hazyStripy setting in UserDefaults in order to do so:

override func draw(_ rect: CGRect) {
    let hazy : Bool = UserDefaults.standard
        .integer(forKey:Default.hazyStripy) == HazyStripy.hazy.rawValue
    CardPainter.shared.drawCard(self.card, hazy:hazy)
}

There is no need for the card object and the view controller object that manages the preferences interface to be able to see one another, because they can both see this common object, the Default.hazyStripy user default. UserDefaults becomes, in itself, a global conduit for communicating information from one part of my app to another.

Notifications and Key–Value Observing

Notifications (Chapter 11) can be a way to communicate between objects that are conceptually distant from one another without bothering to provide any way for one to see the other. All they really need to have in common is a knowledge of the name of the notification. Every object can see the notification center — it is a globally visible object — so every object can arrange to post or receive a notification.

Using a notification in this way may seem lazy, an evasion of your responsibility to architect your objects sensibly. But sometimes one object doesn’t need to know, and indeed shouldn’t know, what object (or objects) it is sending a message to.

Recall the example I gave in Chapter 11. In a simple card game app, the game needs to know when a card is tapped. A card, when it is tapped, knowing nothing about the game, simply emits a virtual shriek by posting a notification; the game object has registered for this notification and takes over from there:

NotificationCenter.default.post(name: Self.tappedNotification, object: self)

Here’s another example, taking advantage of the fact that notifications are a broadcast mechanism. In one of my apps, the app delegate may detect a need to tear down the interface and build it back up again from scratch. If this is to happen without causing memory leaks (and all sorts of other havoc), every view controller that is currently running a repeating Timer needs to invalidate that timer (Chapter 12). Rather than my having to work out what view controllers those might be, and endowing every view controller with a method that can be called, I simply have the app delegate shout “Everybody stop timers!” by posting a notification. All my view controllers that run timers have registered for this notification, and they know what to do when they receive it.

By the same token, Cocoa itself provides notification versions of many delegate and action messages. The app delegate has a method for being told when the app goes into the background, but other objects might need to know this too; those objects can register for the corresponding notification.

Similarly, key–value observing can be used to keep two conceptually distant objects synchronized with one another: a property of one object changes, and the other object hears about the change. As I said in Chapter 11, entire areas of Cocoa routinely expect you to use KVO when you want to be notified of a change in an object property. You can configure the same sort of thing with your own objects.

The Combine Framework

There is some commonality between mechanisms such as the notification center and key–value observing. In both cases, you register with some other object to receive a certain message whenever that other object cares to send it. Basically, you’re opening and configuring a pipeline of communication, and leaving it in place until you no longer need it. Looked at in that way, notifications and key–value observing seem closely related to one another. In fact, the target–action mechanism of reporting control events seems related as well. So does a Timer. So does delayed performance.

New in Swift 5.1 and iOS 13, the Combine framework offers to unify these architectures (and others) under a single head. At its heart, Combine depends upon an abstract notion of publish-and-subscribe, and reifies that abstraction with two protocols, Publisher and Subscriber:

Publisher

A publisher promises to provide a certain kind of a value, perhaps repeatedly, at some time in the future.

Subscriber

A Subscriber registers itself with (subscribes to) a Publisher to receive its value whenever it comes along.

When a Subscriber subscribes to a Publisher, here’s what happens:

  1. The Publisher responds by handing the Subscriber a Subscription (yet another protocol).

  2. The Subscriber can then use the Subscription to ask the Publisher for a value.

  3. The Publisher can respond by sending, whenever it cares to, a value to the Subscriber. It can do this as many times as it likes.

  4. In some situations, the entire connection can be cancelled when the Subscriber no longer wishes to receive values.

That’s a rather elaborate-sounding dance, but in most cases you won’t experience it that way. You won’t experience the dance at all! Instead, you’ll just hook up a built-in subscriber directly to a built-in publisher and all the right things will happen.

To illustrate, I’ll start with a trivial example. One of the simplest forms of publisher is a Subject. Every time you call send(_:) on a Subject, handing it a value, it sends that value to its subscribers. There are just two kinds of Subject:

PassthroughSubject

Produces the value sent to it with send.

CurrentValueSubject

Like a PassthroughSubject, except that it has a value at the outset, and produces it to any new subscriber.

Let’s make a Subject:

let pass = PassthroughSubject<String,Never>()
pass.send("howdy")

That compiles and runs, but we don’t know that anything happened because we have no subscriber. There are just two built-in independent subscribers, and each can be created with a convenience method sent to a publisher; the method subscribes the subscriber to the publisher and returns it:

sink

Takes a function to be called whenever a value is received. The function takes a single parameter, namely the value.

assign

Takes a Swift key path and an object. Whenever a value is received, assigns that value to the property of that object designated by the key path.

So here’s a complete publish-and-subscribe example:

let pass = PassthroughSubject<String,Never>()
let sink = pass.sink { print($0) }
pass.send("howdy") // howdy

A Subject can also be a subscriber — and that completes the list of built-in subscribers. There are, on the other hand, a lot of built-in publishers. We can divide these into two broad categories:

Origins

An origin (a term I’ve made up) is a true source of values. A number of Foundation types vend publishers that act as origins. Here are some:

  • The notification center

  • A KVO compliant property

  • A computed property declared with the @Published property wrapper

  • The Timer class

  • A Scheduler (used for delayed performance; DispatchQueue, OperationQueue, and RunLoop are all Schedulers)

  • A URLSession (for obtaining a value via the network)

Operators

An operator is a Publisher that is somewhat like a Subscriber. You attach it to a Publisher, and it produces another Publisher.

The real power of the Combine framework lies in the operators (of which there are a great many). By chaining operators, you construct a pipeline that passes along only the information you’re really interested in; the logic of analyzing, filtering, and transforming that information is pushed up into the pipeline itself.

To illustrate, I’ll use the notification center as my origin. Let’s go back to my example of a Card view that emits a virtual shriek when it is tapped by posting a notification:

static let tapped = Notification.Name("tapped")
@objc func tapped() {
    NotificationCenter.default.post(name: Self.tapped, object: self)
}

Now let’s say, for purposes of the example, that what the game is interested in when it receives one of these notifications is the string value of the name property of the Card that posted the notification. Getting that information is a two-stage process. First, we have to register to receive notifications at all:

NotificationCenter.default.addObserver(self,
    selector: #selector(cardTapped), name: Card.tapped, object: nil)

Then, when we receive a notification, we have to look to see that its object really is a Card, and if it is, fetch its name property and do something with it:

@objc func cardTapped(_ n:Notification) {
    if let card = n.object as? Card {
        let name = card.name
        print(name) // or something
    }
}

Now let’s do the same thing using the Combine framework. We obtain a publisher from the notification center by calling its publisher method. But we don’t stop there. We don’t want to receive a notification if the object isn’t a Card, so we use the compactMap operator to cast it safely to Card (and if it isn’t a Card, the pipeline just stops as if nothing had happened). We only want the Card’s name, so we use the map operator to get it. Here’s the result:

let cardTappedCardNamePublisher =
    NotificationCenter.default.publisher(for: Card.tapped)
        .compactMap {$0.object as? Card}
        .map {$0.name}

Let’s say that cardTappedCardNamePublisher is an instance property of our view controller. Then what we now have is an instance property that publishes a string if a Card posts the tapped notification, and otherwise does nothing. Do you see what I mean when I say that the logic is pushed up into the pipeline? Just for completeness, let’s arrange to receive that string by subscribing to the publisher:

let sink = self.cardTappedCardNamePublisher.sink {
    print($0) // the string name of a card
}

Here’s another example. You may have noticed that I didn’t list controls (UIControl) among the built-in publishers. This means we can’t automatically replace the control target–action mechanism using the Combine framework. However, with just a little modification, we can turn a control into a publisher. I’ll demonstrate with a switch control (UISwitch). It has an isOn property, which is changed when the user toggles the switch on or off. The target–action way to learn that this has happened is through the switch’s .valueChanged control event. Instead, let’s write a UISwitch subclass where we vend a publisher and funnel the isOn value through it:

class MySwitch : UISwitch {
    required init?(coder: NSCoder) {
        super.init(coder:coder)
        self.isOnPublisher = self.isOn
        self.addTarget(self,
            action: #selector(didChangeOn), for: .valueChanged)
    }
    @Published var isOnPublisher = false
    @objc func didChangeOn() {
        self.isOnPublisher = self.isOn
    }
}

That code illustrates the @Published property wrapper. This creates a publisher behind the scenes, and vends it through the property wrapper’s dollar-sign projectedValue (“Property Wrappers”). With our subclass, the way to be kept informed about changes to the switch’s isOn property is to subscribe to its publisher, namely $isOnPublisher. Suppose we have an outlet to the switch:

@IBOutlet var mySwitch : MySwitch!

Then we can subscribe with sink once again:

let sink = self.mySwitch.$isOnPublisher.sink {
    print($0)
}

But wait — doesn’t that look awfully familiar? Yes, it does — and that’s the point. Using Combine, we’ve effectively reduced the notification center mechanism and the control target–action mechanism to the same mechanism. Moreover, we made our UISwitch send us messages when a property changes, without bothering to use key–value observing. We could use key–value observing — there is an NSObject publisher(for:) method that lets us specify a key–value observable property — but it’s simpler to use the @Published property wrapper, and unlike key–value observing, it works without our object being an NSObject (or even a class).

The real power of the Combine framework emerges when we build complex pipelines. To illustrate, let’s combine (sorry about that) the notification center pipeline and the switch pipeline. Imagine that our interface consists of Cards along with a switch. When the switch is on, the cards are interactive: the user can tap one, and we hear about it. When the switch is off, the user’s taps do nothing.

To implement this, we can put the notification center publisher and the switch publisher together into a single pipeline. The Combine publisher that does that is CombineLatest:

lazy var combination =
    Publishers.CombineLatest(
        self.cardTappedCardNamePublisher, self.mySwitch.$isOnPublisher)

What we now have is a publisher that channels the pipelines from our other two publishers into one. It remembers every value that last arrived from either source, and when it gets a new value, it emits a tuple consisting of both values. In our case, that’s a (String,Bool).

However, that’s not what we actually want to have coming down the pipeline at us. We still want just the string name of the tapped card. So we’ll use the compactMap operator to extract it:

lazy var combination =
    Publishers.CombineLatest(
        self.cardTappedCardNamePublisher, self.mySwitch.$isOnPublisher)
            .compactMap { $0.0 }

Now we’re getting just the string name, but we’re getting too many string names; the switch isn’t having any effect. The pipeline is emitting values in response to user taps even when the switch is off! The whole idea of combining these two publishers was to eliminate any output when the switch is off. So we’ll interpose the filter operator to block any tuples whose Bool is false:

lazy var combination =
    Publishers.CombineLatest(
        self.cardTappedCardNamePublisher, self.mySwitch.$isOnPublisher)
            .filter { $0.1 }
            .compactMap { $0.0 }

This is looking much better. If the user taps while the switch is on, we get the card name. If the user taps while the switch is off, nothing happens. But there’s still one little problem. The CombineLatest publisher publishes if it gets a value from either of its source publishers. That means we don’t just get a value when the user taps a Card; we also get a value when the user toggles the switch. We don’t want that value to come out the end of the pipeline; we just want to use it to allow or prevent the arrival of the Card name.

What we want to do here is compare two values: the new tuple coming down the pipeline, and the previous tuple that came down the pipeline most recently. If the difference between the new tuple and the old tuple is merely that the Bool changed, we don’t want to emit a value from the pipeline. The way to compare the current value with the previous value is with the scan operator. I’ll use that operator to replace the card name string with nil whenever the Bool changes; the compactMap operator will catch this nil and block it from coming out the end of the pipeline:

lazy var combination =
    Publishers.CombineLatest(
        self.cardTappedCardNamePublisher, self.mySwitch.$isOnPublisher)
            .scan((nil,true)) { $0.1 != $1.1 ? (nil,$1.1) : $1 }
            .filter { $0.1 }
            .compactMap { $0.0 }

Our goal is accomplished. If the user taps a Card while the switch is on, the pipeline produces its name. If the user taps a Card while the switch is off, or toggles the switch, nothing happens.

These examples have only scratched the surface of what the Combine framework can do; but they demonstrate its spirit. And the potential benefits are profound. In Chapter 11 I complained that the event-driven nature of the Cocoa framework means that you’re bombarded with events through different entry points at different times, so that state has to be maintained in shared instance properties, and understanding the implications of any single entry point method call can be difficult. The Combine framework offers the potential of funneling events into pipelines whose logic can be manipulated internally, so that what comes out is just the information you need when you need it.

The Promise of SwiftUI

The SwiftUI framework is a wholesale alternative to Cocoa. Whether it talks to Cocoa under the hood or replaces it altogether isn’t clear and doesn’t matter. It operates on a programming paradigm that’s completely different from Cocoa’s, and offers the promise of writing iOS apps in a totally different way — not to mention that the same code might be reusable on Apple TV, Apple Watch, and desktop Macs. SwiftUI as a whole is outside the scope of this book; it needs a book of its own. The subject here is how objects see and communicate with one another.

To illustrate how SwiftUI deals with communication, let’s start with the prototypical “Hello World” app:

struct ContentView : View {
    var body: some View {
        Text("Hello World")
    }
}

That code puts the text “Hello World” in the middle of the screen. But how? It doesn’t seem to contain any runnable code. Well, actually it does: body is a computed property, and the curly braces that surround Text("Hello World") are its getter (with return omitted). The interface is constructed in code and returned. But that’s not quite accurate; it isn’t the interface that’s returned — it’s a description of the interface.

In SwiftUI, there is no storyboard; there are no nibs; there are no outlets. There is no UIViewController; there isn’t even a UIView. Text is a mere struct, and View is just a protocol. A SwiftUI View is extremely lightweight, and is barely persistent. There are no entry points other than the body property getter, which is merely the answer to an occasional question, “What should this view look like at this moment?”

The most pervasive object-oriented pattern in SwiftUI is that one View instantiates another View. Our simple “Hello World” app consists entirely of our ContentView instantiating a Text object in its body getter. This is the same pattern of visibility by instantiation that I discussed at the start of this chapter: when Manny creates Jack, Manny has a reference to Jack and can hand Jack any information that Jack needs in order to do his job. We do not have to get a reference to something in order to customize its appearance; we customize its appearance as part of its initialization. There is no need for the ContentView to get a reference to the Text in order to tell it what its visible content (“Hello World”) should be; the ContentView creates the Text, and creates it with that content.

Function Builders and Modifiers

SwiftUI’s syntax for constructing interfaces is declarative and functional rather than imperative and sequential. To illustrate, I’ll add a button to the interface. I’ll declare the button without giving it any functionality:

struct ContentView : View {
    var body: some View {
        HStack {
            Text("Hello World")
            Spacer()
            Button("Tap Me") {
                // does nothing
            }
        }.frame(width: 200)
    }
}

The Text object returned by the body getter has been replaced by an HStack, which lines up views horizontally. Inside the HStack’s curly braces are three objects in series: a Text, a Spacer, and a Button. That seems impossible syntactically. What’s happening? The curly braces after HStack are the body of an anonymous function, supplied using trailing closure syntax as a parameter to HStack’s initializer. That initializer is allowed to “list” three objects because it is fed to a ViewBuilder, which is a function builder (“Function Builders”); the ViewBuilder wraps up those objects in a TupleView, and that is what is returned from the anonymous function.

The frame method being called on the HStack determines the width of the HStack on the screen. What’s interesting about it is that it is a method. Instead of getting a reference to the HStack object and setting a property of that object, we apply a method directly to that object. This sort of method is called a modifier, and it returns in effect the very same instance to which it was sent. Therefore, modifiers can be chained (just like operators in the Combine framework). If we wanted our text-and-button HStack to have a yellow background with 20-pixel margins, we could write:

HStack {
    Text("Hello World")
    Spacer()
    Button("Tap Me") {
        // does nothing
    }
}.frame(width: 200)
    .padding(20)
    .background(Color.yellow)

State Properties

At this point, you may be saying: “Fine, I see how visibility by instantiation is sufficient when all you want to do is create the app’s initial interface; you configure a view’s initial appearance in its initializer. But what about when the interface needs to change over the course of the app’s lifetime?” Amazingly, the answer is the same: SwiftUI still handles everything through a view’s initializer. But how can that be?

To demonstrate, let’s give our button some functionality. In particular, the user should be able to tap the button to toggle the text between “Hello World” and “Goodbye World.” Here’s how to do that:

struct ContentView : View {
    @State var isHello = true 1
    var greeting : String {
        self.isHello ? "Hello" : "Goodbye" 2
    }
    var body: some View {
        HStack {
            Text(self.greeting + " World")
            Spacer()
            Button("Tap Me") {
                self.isHello.toggle() 3
            }
        }.frame(width: 200)
            .padding(20)
            .background(Color.yellow)
    }
}
1

We have declared an isHello instance property on which the interface depends — in this case, whether the text should read “Hello World” or “Goodbye World.” It is declared with the @State property wrapper. This means that if the value of isHello changes, and if it is accessed from within the body getter, the body getter will be called again.

2

For simplicity and clarity, we also declare a computed instance property greeting that translates the Bool of the @State property isHello into a corresponding string.

3

The button’s action — what it should do when tapped — is supplied as an anonymous function, using trailing closure syntax, as part of its initializer. That action changes the value of the @State property isHello. When the @State property isHello changes in response to the tapping of the button, the body getter is called again, and the Text content is calculated again, and takes on its new value. That is what we set out to accomplish: tapping the button changes the text displayed on the screen.

Notice what did not happen in that example:

  • The button did not use an action–target architecture: there is no separate target to send a message to, and there is no separate action function. Instead, the action function is part of the button.

  • The action function did not talk to the Text to change what it displays; it talked only to the @State property.

  • The @State property has no setter observer that talks to the Text. Instead, the change in the @State property effectively flows “downhill” to the body of the View, automatically.

In that code, then, there are no event handlers, no events, and no action handlers. And there are no references from one object to another! There is no problem of communicating data from one object to another, because objects don’t try to communicate with one another. There is just a View and its state at any given moment.

Moreover, state in SwiftUI can be maintained only through @State properties. That’s because a View stored property can’t be settable; a view is a struct and isn’t mutable. A @State property, on the other hand, is a computed property backed by a property wrapper whose underlying State struct is mutable. In this way, SwiftUI forces you to clarify the locus of state throughout your app.

Bindings

Some views have even tighter coupling with a @State. To illustrate, I’ll replace the Button in our example with a Toggle:

struct ContentView : View {
    @State var isHello = true
    var greeting : String {
        self.isHello ? "Hello" : "Goodbye"
    }
    var body: some View {
        VStack {
            Text(self.greeting + " World")
            Spacer()
            Toggle("Friendly", isOn: $isHello) // *
        }.frame(width: 150, height: 100)
            .padding(20)
            .background(Color.yellow)
    }
}

A Toggle is drawn as a labelled UISwitch. When the user changes the switch, the Text changes in the interface. But how?

A Toggle takes a Binding in its initializer. The State property wrapper vends a Binding property as its dollar-sign projectedValue. Our @State property is isHello, so its binding is $isHello. We handed that binding to the Toggle when we initialized it. When the user changes the UISwitch value, that binding’s value is toggled. That change takes place in the @State property, and so the Text changes accordingly.

That is somewhat similar to what we did in the earlier discussion of the Combine framework, where we modified a UISwitch to vend a Publisher of its own isOn value — except that in SwiftUI, the communication between the @State property and the Toggle is two-way and automatic by way of the binding. Our Toggle has no action function, and doesn’t need one, because it is tightly integrated with a Bool property through a binding.

Passing Data Downhill

So far, the only objects our ContentView has created are instances of built-in types — Text, Button, Spacer, Toggle. But what if you wanted to create an instance of a custom type? How would you pass data from the View that does the creating to the View that is created? In exactly the same way that we’ve been doing it up to now! You give your custom type a property, and you set that property as part of the custom type’s initialization.

That is legal, even though a View is an immutable struct whose stored properties cannot be set, because we are not mutating the struct; we are initializing it. It is also easy, because a View is just a struct. Typically, you won’t even bother to write an initializer for your custom View; the implicit memberwise initializer will be sufficient.

In this example, we present modally a secondary view, an instance of a Greeting struct that we ourselves have defined:

struct ContentView : View {
    @State var isHello = true
    var greeting : String {
        self.isHello ? "Hello" : "Goodbye"
    }
    @State var showSheet = false
    var body: some View {
        VStack {
            Button("Show Message") {
                self.showSheet.toggle()
            }.sheet(isPresented: $showSheet) {
                Greeting(greeting: self.greeting)
            }
            Spacer()
            Toggle("Friendly", isOn: $isHello)
        }.frame(width: 150, height: 100)
            .padding(20)
            .background(Color.yellow)
    }
}
struct Greeting : View {
    let greeting : String
    var body: some View {
        Text(greeting + " World")
    }
}

The sheet modifier is the SwiftUI equivalent of a Cocoa presented view controller. It describes a view that we intend to present modally. Whether it is actively presenting that view modally depends upon a binding to a Bool, which we have supplied by adding a @State property called showSheet. The Button toggles showSheet to true, and the binding $showSheet toggles to true in response, and causes the view to be presented.

The view we want to present is a wrapper for a Text that will display the “Hello World” or “Goodbye World” greeting; we have named that wrapper view Greeting, and we instantiate it in an anonymous function that we supply as the last parameter to the sheet modifier, using trailing closure syntax. When we instantiate Greeting, we must also configure the Greeting instance we are creating. We do that through the Greeting initializer. The Greeting struct belongs to us, so we’re free to give it a greeting property, and Swift synthesizes the memberwise initializer with a greeting: parameter. All we have to do is set that property as we create the Greeting. Once again, the data flows “downhill.”

Passing Data Uphill

What about when the data needs to flow “uphill” out of the secondary view back to the view that presented it? This is the sort of problem that you’d solve in Cocoa programming using the protocol-and-delegate pattern (“Implementing Delegation”). In SwiftUI, you simply “lend” the secondary view the binding from a @State property.

Suppose our Greeting view is to contain a text field (SwiftUI TextField) in which is to be entered the user’s name, and that this information is to be communicated back to our ContentView. Then ContentView would contain another @State property:

@State var name = ""

And Greeting would contain a @Binding property:

@Binding var username : String

When ContentView initializes Greeting, the memberwise initializer now has a username: parameter that takes a string Binding; we hand it the binding from the @State property:

Button("Show Message") {
    self.showSheet.toggle()
}.sheet(isPresented: $showSheet) {
    Greeting(greeting: self.greeting,
             username: self.$name)
}

And Greeting’s TextField is initialized with that binding:

TextField("Your Name", text:$username)
    .frame(width:200)
    .textFieldStyle(RoundedBorderTextFieldStyle())

Whatever the user types in this text field in the Greeting view flows “uphill” through the username binding, which is the @State property name binding, and changes the value of the @State property name back in the ContentView. And now the data flows “downhill” once more: the ContentView body getter will be called again, and all views that depend upon this @State property will change to match.

Once again, what’s most significant in that example is what we didn’t do. We didn’t get a reference from the Greeting back to the ContentView. The Greeting didn’t call any method of the ContentView. It didn’t set a property of the ContentView. It set its own property, username. Communication between objects takes place through bindings in SwiftUI. They are like little pipelines from one object to another — and the object at one end (our Greeting) doesn’t have to know anything about what’s at the other end.

Custom State Objects

You can construct your own state object by writing a custom class that conforms to the ObservableObject protocol. For example, your app’s data might reside in an ObservableObject. To use it, you assign an instance of your class into an @ObservedObject property. Then you can access its properties directly. Those properties work like @State properties: when a property changes, your body getter that references it is called again. Moreover, you can access an associated binding through the dollar-sign projectedValue of the underlying ObservedObject property wrapper struct.

To illustrate, suppose we want the name entered into the text field by the user to persist between launches. We can use the global UserDefaults for this. One way to implement access to UserDefaults is through an ObservableObject. I’ll write a simple Defaults class with a username property that is a computed property backed by a UserDefaults entry:

class Defaults : ObservableObject {
    let objectWillChange = ObservableObjectPublisher()
    var username : String {
        get {
            UserDefaults.standard.string(forKey: "name") ?? ""
        }
        set {
            self.objectWillChange.send()
            UserDefaults.standard.set(newValue, forKey: "name")
        }
    }
}

The sole requirement of the ObservableObject protocol is an objectWillChange property that is a Publisher (as I described in the earlier discussion of the Combine framework). The minimal Publisher is a simple ObservableObjectPublisher; it notifies its subscribers whenever its send method is called. We call send whenever our username computed property is set, fulfilling the requirement.

In our ContentView, there is no longer a @State property called name; it is replaced by an @ObservedObject property of type Defaults:

@ObservedObject var defaults = Defaults()

When we need to access the value of the Defaults username property, we do so directly:

Text(self.defaults.username.isEmpty ? "" :
    greeting + ", " + self.defaults.username)

When we need a binding to the Defaults username property, we pass through the binding from defaults, namely $defaults:

Button("Show Message") {
    self.showSheet.toggle()
}.sheet(isPresented: $showSheet) {
    Greeting(greeting: self.greeting,
             username: self.$defaults.username)
}

Global Objects

SwiftUI has a notion of global objects that all views can access. This is comparable to the notion of “global visibility” from earlier in this chapter.

SwiftUI itself maintains an EnvironmentValues object containing a miscellaneous grab-bag of information and settings. Any view can reach into this object through a property that uses the @Environment property wrapper, whose initializer takes a key path. For example, if our view is going to need access to the current time zone, we can provide that through an @Environment property:

@Environment(\.timeZone) var timeZone

There is also a global Environment into which you can inject your own objects with the environmentObject modifier. Such an object needs to be an ObservableObject. Objects that you inject flow “downhill” through the view hierarchy, so if you want an object to be global to your entire app, call environmentObject on the root object created in the scene delegate:

window.rootViewController =
    UIHostingController(
        rootView: ContentView()
           .environmentObject(Defaults())
)

A view retrieves an Environment object through a property that uses the @EnvironmentObject property wrapper; simple declaration of the object’s type is sufficient to capture a reference to the instance:

@EnvironmentObject var defaults : Defaults

The examples in this section have demonstrated the heart of SwiftUI, namely its object and data flow architecture. SwiftUI’s heart is in the right place! The designers of SwiftUI have understood that in Cocoa, being bombarded by events, and having to maintain consistent state, and coordinating that state with the interface, is hard. The SwiftUI approach promises a welcome paradigm shift to a completely different way of communicating data and coordinating state and interface.