-
Notifications
You must be signed in to change notification settings - Fork 472
Passing Data Propagating Events
- Overview
- Basic example
- Using segues in a storyboard
- The delegate pattern in iOS
- Passing blocks
- The target-action pattern
- Key-Value observing
- Broadcasting messages with
NSNotificationCenter
While your application runs, as events are triggered and processed, you'll need a way for objects in your application to propagate these events and to get the data they need from each other in order to respond properly.
In iOS, there are quite a few standard ways to pass data and propagate events between objects:
- segues in storyboards
- delegate pattern
- passing blocks (closures) around
- target-action pattern
- key-value observation
- a publish/subscribe message bus with
NSNotificationCenter
This guide gives an high-level overview of each of these mechanisms with a focus on passing data and propagating events between two different view controllers and between view controllers and views. Generally when working with view controllers and views, the following steps are typical
- A view controller
VC1
configures a view or another view controllerV2
. This can be done by calling an initializer or by obtaining a reference to and setting properties onV2
. - The second view or view controller
V2
is loaded and shown on the screen. AdditionallyV2
may request more information fromVC1
while it's on the screen. - The user triggers an event in
V2
that needs to be handled inVC1
. This means thatV2
will need to propagate the event and possibly send some information about it's current state toVC1
.
To demonstrate this basic use case, we'll use a standard example throughout. We'll build a demo app that lets the user change the background color of the main view controller by opening up a secondary color picker view controller.
When working in a project with storyboards, the most common way to
transition two view controllers is via a segue (pronounced seg-way).
Segues are a way to create and visualize the transition inside your
storyboard. They provide a way to initialize the destinationView
controller in prepareForSegue
. They also provide
for a way to propagate events and information back to the original
source view controller via unwind segues.
We create our example app by starting with Single View Application
project template. We add a single button "Open Color
Picker" to the root view controller ViewController
. We add a second
view controller with custom class ColorPickerViewController
with an
@IBOutlet
for the segmented control that will contain our color
choices.
In the storyboard we create a modal segue from the "Open Color Picker" button to the color picker view controller by control-dragging.
We also create an unwind segue by control dragging from the "Done"
button to the red exit door above the color picker view controller. An
unwind segue is a way for a view controller to respond to an event by
navigating back to the view controller that caused it to be loaded. In
order for any unwind segues to be available we must first add an
@IBAction
method to the original source view controller that will be
unwound to taking a single UIStoryboardSegue
parameter. In our case
this means we added the didPickColorUnwind
method to the
ViewController
class (see code below).
In order to able to idenfy a storyboard segue in our class code we must provide it with an identifier by selecting the segue and using the Attributes Inspector.
In our source view controller ViewController
we prepare for the segue
by configuring the color picker view controller's initally selected
color with our current background color. Our
ColorPickerViewController
provides us with a way to obtain the
currently selected color when the unwind segue is triggered with the
colorFromSelection
method. We set the selected color be our
background color when the unwind segue happens.
class ViewController: UIViewController {
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "OpenColorPickerSegue" {
let colorPickerVC = segue.destinationViewController as ColorPickerViewController
colorPickerVC.startingColor = view.backgroundColor
}
}
@IBAction func didPickColorUnwind(segue: UIStoryboardSegue) {
let colorPickerVC = segue.sourceViewController as ColorPickerViewController
if let selectedColor = colorPickerVC.colorFromSelection() {
view.backgroundColor = selectedColor
}
}
}
In ColorPickerViewController
initialize our segmented control with our
color choices in viewDidLoad
. We also sync the selected segment on
our segmented control with the inital startingColor
.
The colorFromSelection
allows the unwind segue handler in
ViewController
to obtain the currently selected color.
class ColorPickerViewController: UIViewController {
@IBOutlet weak var colorsSegmentedControl: UISegmentedControl!
let colors = [("Cyan", UIColor.cyanColor()), ("Magenta", UIColor.magentaColor()), ("Yellow", UIColor.yellowColor())]
var startingColor: UIColor?
override func viewDidLoad() {
super.viewDidLoad()
// initalize segmented control and select the starting color if it is one of our segments
colorsSegmentedControl.removeAllSegments()
var selectedIndex = UISegmentedControlNoSegment
for (index, color) in enumerate(colors) {
if color.1.isEqual(startingColor) {
selectedIndex = index
}
colorsSegmentedControl.insertSegmentWithTitle(color.0, atIndex: index, animated: false)
}
colorsSegmentedControl.selectedSegmentIndex = selectedIndex
}
func colorFromSelection() -> UIColor? {
let selectedIndex = colorsSegmentedControl.selectedSegmentIndex
if selectedIndex != UISegmentedControlNoSegment {
return colors[selectedIndex].1
}
return nil
}
}
For another example of working with segues see our navigation controller guide.
Since segues are only available in storyboards, we'll need a different way to coordinate the interaction between view controllers in a non-storyboard application. One very common method to do this in iOS is the [delegate pattern]. [delegatepattern]: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html
The delegate pattern works by having a delegating object maintain a reference to another delegate object that is able to provide data and handle events in cases it is inconvenient for the delegating object to do so. A typical use of this to coordinate events between multiple view-controllers or between view controllers and views is as folows:
- A view controller
VC1
instantiates another view controller (or view)V2
that won't have all the information it needs to configure itself (or wants to propagate events to the original view controller). -
VC1
implements a delegate protocol corresponding to theV2
and sets itself asV2
's delegate -
V2
may call its delegate (in this caseVC1
) to obtain data it needs to show itself on the screen -
V2
may respond to events by calling delegate methods and thus propagating the event/offloading the responsiblity toVC1
.
You probably have already seen this pattern in action with
UITableViewDataSource
and UITableViewDelegate
.
The delegate pattern is ubiquitous throughout the iOS frameworks and libraries. It is useful for creating a well defined (compile-time) interface for communication between objects. You might also prefer it if there are many different kinds of messages that you might want to pass between objects.
We modify our example from above to use the delegate
pattern as follows. We remove the segues from our story board, and have
the main ViewController
instantiate and present the
ColorPickerViewController
manually.
Importantly, we also have the ViewController
class implement
ColorPickerDelegate
(see next code block) and set itself as the color
picker view controller's delegate before presenting it to the user. We
implement the delegate method didPickColor
to respond by setting the
our background color to the selected color and dismissing the color
picker view controller. Finally we provide our background color as the
initial color by implementing the delegate method initialColor
.
class ViewController: UIViewController, ColorPickerDelegate {
@IBAction func openColorPickerTapped(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let colorPickerVC = storyboard.instantiateViewControllerWithIdentifier("ColorPicker") as ColorPickerViewController
colorPickerVC.delegate = self
presentViewController(colorPickerVC, animated: true, completion: nil)
}
func colorPicker(picker: ColorPickerViewController, didPickColor color: UIColor?) {
if let selectedColor = color {
view.backgroundColor = selectedColor
}
dismissViewControllerAnimated(true, completion: nil)
}
func initialColor() -> UIColor? {
return view.backgroundColor
}
}
Our ColorPickerDelegate
has two methods. The didPickColor
method
lets ColorPickerViewController
notify its delegate when the user has
selected a color by tapping on the "Done" button. The initialColor
color method lets the delegate inform ColorPickerViewController
of
which color to set as its initially selected color in viewDidLoad
.
protocol ColorPickerDelegate: class {
func colorPicker(picker: ColorPickerViewController, didPickColor color:UIColor?)
func initialColor() -> UIColor?
}
class ColorPickerViewController: UIViewController {
@IBOutlet weak var colorsSegmentedControl: UISegmentedControl!
let colors = [("Cyan", UIColor.cyanColor()), ("Magenta", UIColor.magentaColor()), ("Yellow", UIColor.yellowColor())]
weak var delegate: ColorPickerDelegate?
override func viewDidLoad() {
super.viewDidLoad()
// initalize segmented control and select the starting color if it is one of our segments
colorsSegmentedControl.removeAllSegments()
var selectedIndex = UISegmentedControlNoSegment
let initialColor = delegate?.initialColor()
for (index, color) in enumerate(colors) {
if color.1.isEqual(initialColor) {
selectedIndex = index
}
colorsSegmentedControl.insertSegmentWithTitle(color.0, atIndex: index, animated: false)
}
colorsSegmentedControl.selectedSegmentIndex = selectedIndex
}
func colorFromSelection() -> UIColor? {
let selectedIndex = colorsSegmentedControl.selectedSegmentIndex
if selectedIndex != UISegmentedControlNoSegment {
return colors[selectedIndex].1
}
return nil
}
@IBAction func doneButtonTapped(sender: AnyObject) {
delegate?.colorPicker(self, didPickColor: colorFromSelection())
}
}
Closures in Swift and blocks in Objective-C are first class concepts. This means that you can pass closures as parameters and assign them to variables to be exectuted later. These concepts allow you to implement a basic callback mechanism that can be used to propagate and pass on the responsibility of handling an event.
Passing blocks is common in iOS frameworks and libraries where a single callback (as opposed to a protocol containing many methods used with delegates) is required. It also provides the advantage of being able to define the closure inline so that it closes over the current environment and hence you will have access to any variables that are currently in scope.
An example of an iOS library that uses blocks is
NSURLConnection
. It allows you
to specify a block that will be executed asynchronously when a network
request returns. Another example is the
NSNotificationCenter.addObserverForName:object:queue:usingBlock:
method called below.
One downside to using blocks is that it may be difficult to get the type definitions correct for complicated closures having optionals, collection types, or other closures as parameters or return values.
We can see block callbacks in action continuing with our running
example. We still have our ViewController
instantiate and configure the ColorPickerViewController
. The
ColorPickerViewController
now has a doneHandler
which we set to be a
block that calls our method didPickColor
. We present the
ColorPickerViewController
to the user after finishing our
configuration.
As before, the method didPickColor
updates our background color and
dismisses the ColorPickerViewController
.
class ViewController: UIViewController {
@IBAction func openColorPickerTapped(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let colorPickerVC = storyboard.instantiateViewControllerWithIdentifier("ColorPicker") as ColorPickerViewController
colorPickerVC.initialColor = view.backgroundColor
colorPickerVC.doneHandler = {(color: UIColor?) -> Void in
self.didPickColor(color)
}
presentViewController(colorPickerVC, animated: true, completion: nil)
}
func didPickColor(color: UIColor?) {
if let selectedColor = color {
view.backgroundColor = selectedColor
}
dismissViewControllerAnimated(true, completion: nil)
}
}
In ColorPickerViewController
we propagate the action of the user
clicking on the "Done" button to ViewController
by executing the
doneHandler
that was set.
class ColorPickerViewController: UIViewController {
@IBOutlet weak var colorsSegmentedControl: UISegmentedControl!
let colors = [("Cyan", UIColor.cyanColor()), ("Magenta", UIColor.magentaColor()), ("Yellow", UIColor.yellowColor())]
var initialColor: UIColor?
var doneHandler: ((UIColor?) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// initalize segmented control and select the starting color if it is one of our segments
colorsSegmentedControl.removeAllSegments()
var selectedIndex = UISegmentedControlNoSegment
for (index, color) in enumerate(colors) {
if color.1.isEqual(initialColor) {
selectedIndex = index
}
colorsSegmentedControl.insertSegmentWithTitle(color.0, atIndex: index, animated: false)
}
colorsSegmentedControl.selectedSegmentIndex = selectedIndex
}
func colorFromSelection() -> UIColor? {
let selectedIndex = colorsSegmentedControl.selectedSegmentIndex
if selectedIndex != UISegmentedControlNoSegment {
return colors[selectedIndex].1
}
return nil
}
@IBAction func doneButtonTapped(sender: AnyObject) {
doneHandler?(colorFromSelection())
}
}
An another construct used to propagate events in iOS is the
target-action pattern. This is an older Objective-C
pattern that allows a class to register a method (action or
selector) to be executed on some object (target) at some point
later. This pattern is used throughout many iOS libraries for example
the following code tells the button
to call myObject.myMethod()
when
it is tapped.
let button = UIButton()
button.addTarget(myObject, action: "myMethod", forControlEvents: .TouchUpInside)
// add button to view hierarchy
In Swift, it is not possible to implement a method that can accept a
target-action since NSObject
's performSelector
methods and the NSInvocation
APIs are disabled in
Swift. This was because these APIs do not allow the compiler to check
for type safety. Another downside is that its is not easy to use the
target-action pattern to invoke methods that require more than two
parameters.
TODO: rewrite example in Objective-C to use target-action pattern
iOS provides a way for any object to listen to changes in another object's properties via a powerful mechanism known as Key-Value observing (KVO). The basic outline for using KVO is
- Register an object for observation by calling
addObserver:forKeyPath:
- Implement
observeValueForKeyPath
in the class doing the observing - Update properties in object being observed with the corresponding
keyPath
.
In our running example, we have ViewController
observe the selectedColor
property on our ColorPickerViewController
.
We set up the observation when initializing the
ColorPickerViewController
by adding ourself (ViewController
) as an
observer.
When this property is set, we know that the user has finished selecting
a color. We then remove the observer and call didPickColor
to set the
background color and dismiss the ColorPickerViewController
.
It is important that we remove the observer once we are done. Failure to properly remove observers may cause unexpected behavior or your app to crash.
private var colorPickerObservationContext = 0
private let selectedColorKeyPath = "selectedColor"
class ViewController: UIViewController {
@IBAction func openColorPickerTapped(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let colorPickerVC = storyboard.instantiateViewControllerWithIdentifier("ColorPicker") as ColorPickerViewController
colorPickerVC.initialColor = view.backgroundColor
colorPickerVC.addObserver(self, forKeyPath: selectedColorKeyPath, options: .New, context: &colorPickerObservationContext)
presentViewController(colorPickerVC, animated: true, completion: nil)
}
func didPickColor(color: UIColor?) {
if let selectedColor = color {
view.backgroundColor = selectedColor
}
dismissViewControllerAnimated(true, completion: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if context == &colorPickerObservationContext && keyPath == selectedColorKeyPath
&& object is ColorPickerViewController {
let colorPickerVC = object as ColorPickerViewController
colorPickerVC.removeObserver(self, forKeyPath: selectedColorKeyPath, context: &colorPickerObservationContext)
let selectedColor = change[NSKeyValueChangeNewKey] as UIColor
didPickColor(selectedColor)
}
}
}
In ColorPickerViewController
we never explicitly call any method or
invoke any closure that refers directly to ViewController
. We simply
introduce a property selectedColor
that is only set once the user taps
on the "Done" button. We already registered the ViewController
as
listener to changes in this property above. Any change will then
automatically trigger the observeValueForKeyPath
method in
ViewController
.
class ColorPickerViewController: UIViewController {
@IBOutlet weak var colorsSegmentedControl: UISegmentedControl!
let colors = [("Cyan", UIColor.cyanColor()), ("Magenta", UIColor.magentaColor()), ("Yellow", UIColor.yellowColor())]
var initialColor: UIColor?
dynamic var selectedColor: UIColor?
override func viewDidLoad() {
super.viewDidLoad()
// initalize segmented control and select the starting color if it is one of our segments
colorsSegmentedControl.removeAllSegments()
var selectedIndex = UISegmentedControlNoSegment
for (index, color) in enumerate(colors) {
if color.1.isEqual(initialColor) {
selectedIndex = index
}
colorsSegmentedControl.insertSegmentWithTitle(color.0, atIndex: index, animated: false)
}
colorsSegmentedControl.selectedSegmentIndex = selectedIndex
}
func colorFromSelection() -> UIColor? {
let selectedIndex = colorsSegmentedControl.selectedSegmentIndex
if selectedIndex != UISegmentedControlNoSegment {
return colors[selectedIndex].1
}
return nil
}
@IBAction func doneButtonTapped(sender: AnyObject) {
selectedColor = colorFromSelection()
}
}
Finally, iOS provides a mechanism for implementing a basic subcribe/publish message queue via notification centers. The basic usage is
- Define an identifer to name this particular type of notification
- Subscribe to this kind of notification by adding an observer to a
notification center for notifications with the given name. You can
instatiate new notification centers, but it is common to use
NSNotificationCenter.defaultCenter
. - Publish notifications to the notification center. You might include
additional data in a
userInfo
dictionary.
The NSNotificationCenter
API is normally used to handle app-wide
events that may be relevant to multiple interested—possibly
unrelated—view controllers (e.g. the logged in state of a user).
It is not normally used to pass information between two specific view
controllers. Nevertheless, we can adapt it our running
example as follows.
We define ColorPickerNotification
as an identifier that will be used
as the name for notifications from our ColorPickerViewController
.
In ViewController
, before presenting the ColorPickerViewController
,
we register an observer for notifications with this name. In the block
that is triggered when the notification fires, we extract the selected
color from the userInfo
dictionary and call didPickColor
. The
didPickColor
method sets the background color and dismisses the
ColorPickerViewController
.
class ViewController: UIViewController {
@IBAction func openColorPickerTapped(sender: AnyObject) {
let storyboard = UIStoryboard(name: "Main", bundle: NSBundle.mainBundle())
let colorPickerVC = storyboard.instantiateViewControllerWithIdentifier("ColorPicker") as ColorPickerViewController
colorPickerVC.initialColor = view.backgroundColor
NSNotificationCenter.defaultCenter().addObserverForName(ColorPickerNotification, object: nil, queue: NSOperationQueue.mainQueue()) { (notification: NSNotification!) -> Void in
let userInfo = notification?.userInfo
let selectedColor: UIColor? = userInfo?[ColorPickerSelectedColorKey] as? UIColor
self.didPickColor(selectedColor)
}
presentViewController(colorPickerVC, animated: true, completion: nil)
}
func didPickColor(color: UIColor?) {
if let selectedColor = color {
view.backgroundColor = selectedColor
}
dismissViewControllerAnimated(true, completion: nil)
}
}
In ColorPickerViewController
, when the "Done" button is tapped we
construct a userInfo
dictionary containing the currently selected
color. Then we fire a notification with the key
ColorPickerNotification
let all subscribers know that the a color has
been picked by the user.
let ColorPickerNotification = "com.codepath.ColorPickerViewController.didPickColor"
let ColorPickerSelectedColorKey = "com.codepath.ColorPickerViewController.selectedColor"
class ColorPickerViewController: UIViewController {
@IBOutlet weak var colorsSegmentedControl: UISegmentedControl!
let colors = [("Cyan", UIColor.cyanColor()), ("Magenta", UIColor.magentaColor()), ("Yellow", UIColor.yellowColor())]
var initialColor: UIColor?
override func viewDidLoad() {
super.viewDidLoad()
// initalize segmented control and select the starting color if it is one of our segments
colorsSegmentedControl.removeAllSegments()
var selectedIndex = UISegmentedControlNoSegment
for (index, color) in enumerate(colors) {
if color.1.isEqual(initialColor) {
selectedIndex = index
}
colorsSegmentedControl.insertSegmentWithTitle(color.0, atIndex: index, animated: false)
}
colorsSegmentedControl.selectedSegmentIndex = selectedIndex
}
func colorFromSelection() -> UIColor? {
let selectedIndex = colorsSegmentedControl.selectedSegmentIndex
if selectedIndex != UISegmentedControlNoSegment {
return colors[selectedIndex].1
}
return nil
}
@IBAction func doneButtonTapped(sender: AnyObject) {
var selectionInfo: [NSObject : AnyObject] = [:]
if let selectedColor = colorFromSelection() {
selectionInfo[ColorPickerSelectedColorKey] = selectedColor
}
NSNotificationCenter.defaultCenter().postNotificationName(ColorPickerNotification, object: self, userInfo: selectionInfo)
}
}