NSNotificationCenter is one way you can communicate with other objects in your project. Alternatives include KVO and delegates. Rather than go in to all the details and comparisons of each and when you might opt for a delegate model vs NSNotificationCenter, what I will quickly say is that if you want to simply share a message that a job has completed and perhaps have several other classes listening out then NSNotificationCenter might be the best option for you.
In todays example we are going to create a simple app that lets you search for an address or point of interest on a map. When that search is complete, a pin or several pins will be dropped on the map with the appropriate details for a callout to pop up when a pin is tapped on.
Why we need to use NSNotificationCenter
Before starting with the tutorial I want to quickly explain why NSNotificationCenter is needed in this scenario. As part of the project we will be using MKLocalSearch from the MapKit framework. When performing a search from a string of text you just received from the user, you need to feed in that text to a completion handler. This “block” of code runs asynchronously and returns a result when finished. Because the code running in a block, the rest of the method completes before the block finishes executing. When you come to display the information you find that the results are still “nil” because the completion handler hasn’t finished what it is doing. You end up with no annotations because of the coordinates not being available when needed.
To get around this, we use NSNotificationCenter (this is one way of doing it). What we do is at the end of the block of code we send out a notification that the search is completed and that the results are ready. The receiver of the message is then responsible for calling the method which adds the annotations.
Setting Up the Project
Lets start by creating a single view application. You can opt for iPad or iPhone. I’ll be using iPhone here, but essentially the project is the same except for it will run on a larger screen if you select iPad. Drag an MKMapView out on to the View Controller and size it to almost fill the screen. At the top of the screen drag a UITextField and resize so it is large enough to perform an address search. I made mine just fill the width of the screen with margins.
CTRL + Drag from the MKMapView to the ViewController.h file between @interface and @end. I called the @property mapView. When this is done, your header code will look like the first example below.
Next you need to switch to your implementation file of the ViewController and CTRL+Drag from the UITextField to somewhere between the @implementation and @end. Set the options as seen in the image below:
You should now have the following in your code:
ViewController.h
#import
#import
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@end
Line 2 we import the MapKit framework in to the project (make sure you link it as well in the Targets section of your app).
Line 6 is the IBOutlet that was created for us when we CTRL+dragged from MKMapView to .h. An alternate way would be to manually type in the line above and then drag from the small circle to the left of it to the MKMapView. Either way is fine and its personal preference for you.
ViewController.m should now look like this:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (IBAction)search:(UITextField *)sender {
NSLog(@"%@", sender.text);
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
You will notice the -(IBAction)search:… method which was added when we CTRL+dragged in. Just to make sure we selected the correct event in the popup (did end on exit in this case), I have NSLogged the text property of sender. When running the app, you will be able to type anything in to the search box and when you hit enter, it should be logged so you can see it is working.
Creating the Coordinate Finder
Now that we have the basic app in place, we now need to add the logic and other code in to make it use that sender.text property and turn that in to a search request which will send back some coordinates and other information.
Create another class by pressing CMD+N. Call it CoordinateFinder and make it a subclass of NSObject. Add the following code to the header file:
#import
#import
@interface CoordinateFinder : NSObject
@property (nonatomic, readonly) NSMutableArray *mapItems;
- (id)initWithAddressOrPOI:(NSString *)address address:(MKCoordinateRegion)inRegion;
@end
We import the MapKit framework again so that we can use the MKLocalSearch class.
We add a readonly property called mapItems which is an NSMutableArray. I created it as an NSMutableArray so we can just keep adding objects to it as needed.
We declared our own public init method which is called initWithAddressOrPOI which requires an NSString which is the address (or POI) being searched for. We also supply a region if we wish so that the search can be localised based on where you are or where you have scrolled to on the map.
Moving over to our implementation, we need to add the following just after the #import and just before the @implementation.
@interface CoordinateFinder ()
@property (readwrite) NSMutableArray *mapItems;
@end
Adding a readwrite version of mapItems lets us internally set this property but still keeps it readonly for those who are using the class. Basically, we are preventing them from medaling with our properties.
Next we create the init method which is done as follows:
- (id)initWithAddressOrPOI:(NSString *)address address:(MKCoordinateRegion)inRegion
{
[self forwardGeocodeAddress:address inRegion:inRegion];
self = [super init];
if (self) {
//
}
return self;
}
On line 3 we are calling a method that we haven’t create yet, but will in a moment. We are also providing the address and inRegion to that method. We are also calling super init as is required.
Implementing the forwardGeocodeAddress method can be done this way:
- (void)forwardGeocodeAddress:(NSString *)address inRegion:(MKCoordinateRegion)inRegion {
MKLocalSearchRequest *searchRequest = [[MKLocalSearchRequest alloc] init];
self.mapItems = [[NSMutableArray alloc] init];
searchRequest.naturalLanguageQuery = address;
searchRequest.region = inRegion;
MKLocalSearch *localSearch = [[MKLocalSearch alloc] initWithRequest:searchRequest];
[localSearch startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
if (!error) {
int i = 0;
do {
MKMapItem *mapItem = [response.mapItems objectAtIndex:i];
[self.mapItems addObject:mapItem];
i++;
} while (i < response.mapItems.count);
[[NSNotificationCenter defaultCenter] postNotificationName:@"Address Found" object:self];
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Not Found" object:self];
}
}];
}
On line 2 we alloc/init an MKLocalSearchRequest.
Line 3 we alloc/init the NSMutableArray (self.mapItems... remember that we can write to it because of what we added at the top of the implementation).
Line 4 we are setting the naturalLanguageQuery property of searchRequest to the address (we are simply feeding it with an NSString containing our search that we will type in to the UITextField).
Line 5 we set the region property.
Line 6 we alloc/init an MKLocalSearch with the searchRequest we just created and set the properties on.
Line 7 we start the search with the startWithCompletionHandler method which requires a block to be executed.
Lines 8 - 19 is our block of code which handles the response and error if applicable. What we do here is first check for an error. If there is no error we use a do/while loop that runs through the correct number of objects that were returned. We learn from the documentation that the MKLocalSearchReponse provides an NSArray of mapItems. The do/while loop iterates through the correct amount of items in that array by comparing the int i to the .count property of mapItems.
On line 12 we are adding these individual mapItems to the self.mapItems NSMutableArray we created. An alternative way would be to just copy the NSArray of mapItems to an NSArray at self.mapItems. I'll let you experiment with the different ways this can be done. The object pulled from the NSArray on this line is determined on line 11 when we use the objectAtIndex:i method.
If this part of code is run, we post a notification on line 15 called @"Address Found". If this section isn't run (because of an error) then the other notification is posted called @"Not Found". Common reasons for errors include no network access which means the search cannot be done, or perhaps you just searched for something that isn't known to Apple Maps.
A closer look at the NSNotificationCenter Code
We just called this line of code on line 15 above:
[[NSNotificationCenter defaultCenter] postNotificationName:@"Address Found" object:self];
What we do here is call the class method called defaultCenter of the NSNotificationCenter. This returns that default center which is the only one we typically use. We then postNotificationName and provide an NSString to name the notification. We do this on object:self (which means the CoordinateFinder class was the one that sent this notification). There are two other options for posting notifications and you can use which ever suits your need. The documentation is found here.
Our CoordinateFinder class is now ready to use. Lets do that now by switching back to ViewController and importing it in so we can start using it as intended. In this next part of the tutorial we will also add an observer so that we can receive the notifications as sent.
In our ViewController.m (implementation) file add the following just below the @implementation line:
CoordinateFinder *coordinateFinder;
Add this at the top of the file:
#import "CoordinateFinder.h"
Add this to the search method:
- (IBAction)search:(UITextField *)sender {
MKCoordinateRegion currentRegion = MKCoordinateRegionMake(self.mapView.centerCoordinate, MKCoordinateSpanMake(0.58, 0.58));
[self.mapView removeAnnotations:self.mapView.annotations];
coordinateFinder = [[CoordinateFinder alloc] initWithAddressOrPOI:sender.text address:currentRegion];
}
Next, add the following to the search method we created earlier.
Line 2 we create a region which is based on the visible portion of the map on the screen. We also create a span on the same line with values of 0.58 on each axis which makes it about 40 miles or so. This region is used to search your current region. Note that when you specify region, it isn't a hard limit but rather a suggestion which Apple uses when searching for results. An example of when a region might be discarded by Apple is when you search for London while looking at Australia. It will still provide London, UK as the answer unless you have another smaller version of London within the visible area of the map.
Line 3 we are telling it to remove any previous annotations added. This line is optional.
Line 4 we are creating a coordinateFinder and using alloc/init with the sender.text (what the user typed in to the search box) and also providing the region. We are not expecting a result back from this method which is why it ends here.
Adding the Observer
When running this app, you can search and a search is performed and the results are stored in the mapItems NSMutableArray property on CoordinateFinder. This is where the problem happens. If you NSLog coordinateFinder.mapItems you will not see anything. The reason for this is because the block of code (completion handler) hasn't finished at the time of NSLogging the mapItems property. To see that in action now, add the following line to the end of the the search method:
NSLog(@"%i", coordinateFinder.mapItems.count);
This will return a 0 which means the NSMutableArray contains no items.
Lets fix that:
In ViewDidLoad on ViewController.m, add the following lines of code:
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(receivedNotification:)
name:@"Address Found"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(receivedNotification:)
name:@"Not Found"
object:nil];
}
Here we are setting up two observers. The first looks out for the @"Address Found" NSString and the second looks for the @"Not Found" NSString. The @selector (method name) is called receivedNotification for each observer.
We now need to add that method so that the program can compile and work. When a notification is received we have informed the defaultCenter to call the receivedNotification method. This method is implemented below:
- (void)receivedNotification:(NSNotification *) notification {
if ([[notification name] isEqualToString:@"Address Found"]) {
[self addAnnotations];
} else if ([[notification name] isEqualToString:@"Not Found"]) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"No Results Found"
message:nil delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
[alertView show];
}
}
On line 2 we check if the notification name isEqualToString:@"Address Found". If it is, we run a method called addAnnotations (not created yet... will be created next).
If that string is equal to @"Not Found" then an alert view pops up saying "No Results Found".
addAnnotation Method
Now we need to implement the addAnnotations method. At this point, we have received a notification essentially telling us that the mapItems @property of CoordinateFinder now contains data (or does not depending on which notification was provided).
We can now access that information and use it to add annotations to the map. To show quickly how we can see that information is in the NSMutableArray property we can call the same NSLog showing the count as we did earlier in the code:
- (void)addAnnotations {
NSLog(@"%i", coordinateFinder.mapItems.count);
}
Adding this code would now show the count as some other number.
Searching for London would show this in the console:
2013-09-17 11:23:59.129 NSNotificationCenter[42867:c07] 0
2013-09-17 11:23:59.489 NSNotificationCenter[42867:c07] 1
The zero on the first line is there because when the method finished executing, the block was still being run asynchronously... hence zero results. Fractions of a second later that block of code completed and we learn that 1 item was added to the array. Searching for something like "church" shows the following:
2013-09-17 11:25:54.595 NSNotificationCenter[42956:c07] 0
2013-09-17 11:25:54.845 NSNotificationCenter[42956:c07] 10
Again, zero because at the time the method finished, the block/search was not complete. As soon as it was complete, a notification was sent out, the observer picked it up, called the correct method which NSLogged the count again, which is now 10. I believe the results are restricted to 10 results but I might be wrong there.
Lets make the code now use that NSMutableArray and add some annotations:
- (void)addAnnotations {
int i = 0;
do {
MKMapItem *mapItem = [coordinateFinder.mapItems objectAtIndex:i];
MKPointAnnotation *myAnnotation = [[MKPointAnnotation alloc] init];
myAnnotation.coordinate = mapItem.placemark.coordinate;
myAnnotation.title = mapItem.name;
myAnnotation.subtitle = mapItem.placemark.title;
[self.mapView addAnnotation:myAnnotation];
i++;
} while (i < coordinateFinder.mapItems.count);
}
Line 2 we set a counter.
The next set of lines we use a do/while to iterate through the NSMutableArray and we pull each MKMapItem out, create an MKPointAnnotation, specify a title, coordinate and subtitle and then add the annotation to the map. Again, you could use addAnnotations (plural) instead if wanted.
Running the App
You can go ahead and run the app now. What you will find is that a search will either show pins, show a single pin or let you know it couldn't find what you were looking for. You can then tap one of those pins and see details about the location such as the business name, address or the address repeated twice. I haven't optimised what you see on the pins because this is out of the scope of this article. However, if you do want to see how annotations work, I suggest looking my MKPointAnnotation Tutorial which shows how you can customise the pin, callout etc...
Any questions, post them below. Full code is found below:
ViewController.h
#import
#import
@interface ViewController : UIViewController
@property (weak, nonatomic) IBOutlet MKMapView *mapView;
@end
ViewController.m
#import "ViewController.h"
#import "CoordinateFinder.h"
@interface ViewController ()
@end
@implementation ViewController
CoordinateFinder *coordinateFinder;
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(receivedNotification:)
name:@"Address Found"
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(receivedNotification:)
name:@"Not Found"
object:nil];
}
- (void)receivedNotification:(NSNotification *) notification {
if ([[notification name] isEqualToString:@"Address Found"]) {
[self addAnnotations];
} else if ([[notification name] isEqualToString:@"Not Found"]) {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"No Results Found"
message:nil delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil, nil];
[alertView show];
}
}
- (void)addAnnotations {
NSLog(@"%i", coordinateFinder.mapItems.count);
int i = 0;
do {
MKMapItem *mapItem = [coordinateFinder.mapItems objectAtIndex:i];
MKPointAnnotation *myAnnotation = [[MKPointAnnotation alloc] init];
myAnnotation.coordinate = mapItem.placemark.coordinate;
myAnnotation.title = mapItem.name;
myAnnotation.subtitle = mapItem.placemark.title;
[self.mapView addAnnotation:myAnnotation];
i++;
} while (i < coordinateFinder.mapItems.count);
}
- (IBAction)search:(UITextField *)sender {
MKCoordinateRegion currentRegion = MKCoordinateRegionMake(self.mapView.centerCoordinate, MKCoordinateSpanMake(0.58, 0.58));
[self.mapView removeAnnotations:self.mapView.annotations];
coordinateFinder = [[CoordinateFinder alloc] initWithAddressOrPOI:sender.text address:currentRegion];
NSLog(@"%i", coordinateFinder.mapItems.count);
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
CoordinateFinder.h
#import
#import
@interface CoordinateFinder : NSObject
@property (nonatomic, readonly) NSMutableArray *mapItems;
- (id)initWithAddressOrPOI:(NSString *)address address:(MKCoordinateRegion)inRegion;
@end
CoordinateFinder.m
#import "CoordinateFinder.h"
@interface CoordinateFinder ()
@property (readwrite) NSMutableArray *mapItems;
@end
@implementation CoordinateFinder
- (id)initWithAddressOrPOI:(NSString *)address address:(MKCoordinateRegion)inRegion
{
[self forwardGeocodeAddress:address inRegion:inRegion];
self = [super init];
if (self) {
//
}
return self;
}
- (void)forwardGeocodeAddress:(NSString *)address inRegion:(MKCoordinateRegion)inRegion {
MKLocalSearchRequest *searchRequest = [[MKLocalSearchRequest alloc] init];
self.mapItems = [[NSMutableArray alloc] init];
searchRequest.naturalLanguageQuery = address;
searchRequest.region = inRegion;
MKLocalSearch *localSearch = [[MKLocalSearch alloc] initWithRequest:searchRequest];
[localSearch startWithCompletionHandler:^(MKLocalSearchResponse *response, NSError *error) {
if (!error) {
int i = 0;
do {
MKMapItem *mapItem = [response.mapItems objectAtIndex:i];
[self.mapItems addObject:mapItem];
i++;
} while (i < response.mapItems.count);
[[NSNotificationCenter defaultCenter] postNotificationName:@"Address Found" object:self];
} else {
[[NSNotificationCenter defaultCenter] postNotificationName:@"Not Found" object:self];
}
}];
}
@end
Craig Daly says
Great tutorial!! Keep up the good work!!
DaveC says
Hi
Thanks for your tutorials, I am busy implementing ibeacons into my app and have followed your ibeacons tutorial and local notification tutorial.
i would like to know the best way to pass a beacons minor number to a class via notification centre so it can pull down the correct info from Parse?
I am monitoring and ranging my beacons in app delegate and from there i post a local notification. once the app launches from the notification I push a separate view controller and would like that view controller to receive the closest beacon minor number and do a Parse query to fetch the info.
any help would be greatly appreciated.
Thanks
Matthew says
I believe you would store the minor value in a key NSUserDefaults and when the app launches from a local notification you can check to see if that key exists and if so, use performSegueWithIdentifier to load up the correct view and when that view loads (or while it loads), query that key again to get the minor value from it to use in the new view.
I hope this helps. If not, let me know and we can discuss more.
Halah says
Best tutorial I found for NSNotification and Map searching, thank you.
Jun Pitogo says
Where’s the code? I can’t find the download code.
Saurav says
Awesome Tutorial