Apple Push Notification Services Tutorial: Part 2/2

来源:互联网 发布:饮食男女专业影评知乎 编辑:程序博客网 时间:2024/05/16 09:56

From: 点击打开链接

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced freelance iOS developer available for hire!

This is the second part of a 2-part tutorial series on integrating Apple Push Notification Services (APNS) into an iPhone app.

In the first part of the tutorial series, you learned how to enable your iPhone app to receive push notifications, and how to send a test push notification using a PHP script.

In this second and final part of the tutorial series, you’ll learn how to make a simple app using APNS, and a simple PHP web service to power it!

Note: This tutorial is on the long side, so make sure you set aside a nice chunk of time (and some munchies!) to go through it. But it’s well worth it – once you finish you’ll have a functional app and web service using push notifications!

Introducing PushChat

In this tutorial, we’re going to make a simple direct messaging app named PushChat that uses push notifications to deliver the messages. This is what it will look like:

Push Chat - Login Screenshot

The first screen that a user sees is the Login screen. Here the user enters their nickname and a “secret code”. The idea is that the user shares this secret code with her friends.

Everyone using the same secret code will see each other’s messages. So in effect, the secret code functions as the name of a chat room. Of course, if you can guess someone’s code you can see their messages, that’s why it’s supposed to be asecret code. ;-)

When the user presses the Start! button, the app sends a message to our server to register this user for that chat room. The Login screen then makes way for the Chat screen:

Push Chat - Chat Screenshot

The secret code is shown in the navigation bar. Messages that are sent by this user are displayed on the right-hand side of the screen, messages that we received are shown on the left. So in this example, both the user and someone named SteveJ (I wonder who that is?) signed up with secret code “TopSecretRoom123”.

The third and final screen of the app is the Compose screen:

Push Chat - Compose Screenshot

Nothing special going on here. It’s just a text view with a keyboard. Messages are limited to 190 bytes and the top of the screen shows how many are left.

I put in that limitation because the payload of a push notification has a maximum size of 256 bytes, and that includes the JSON overhead (see “Anatomy of a push notification” above).

When the user presses the Save button, the message is sent to our server and pushed to all other users who signed up with the same secret code.

The Server API

In the above explanation of how PushChat works, I mentioned “our server” several times. We need to use a server because we have to coordinate stuff between different devices. It’s not much of a chat when you’re just talking to yourself!

I wrote a simple web service that uses PHP and MySQL. The iPhone app will send the following commands to this server:

JOIN. When a user signs up in the Login screen, we send his nickname, his secret code, and his device token to the server. The server adds a record for this user to the list of active users. From then on, any messages sent by members of that same chat room will also be pushed to this new user.

LEAVE. This is the opposite of JOIN. The user can press the Exit button in the Chat screen to leave the chat. We send a LEAVE command to the server which removes the user from the list of active users. From then on, no more notifications will be pushed to this user.

MESSAGE. When a user presses the Save button on the Compose screen, we will send the new text message to our server. The server will then convert these messages into push notifications, one for each member of that chat room, and passes them on to APNS. A few seconds later, APNS pushes these messages to those users’ devices.

UPDATE. This lets the server know that a user has received a new device token. The token may change from time to time and if that happens we need to let the server know. More about this later.

In a picture it looks like this:

Push Chat Server API

At various times the app sends data to the server to let it know that the user has joined or left a chat, or that she is sending a new message. The server in turn creates push notifications that it sends to APNS.

The Apple servers are responsible for delivering those notifications to the app. When the app receives a new push notification, it adds a speech bubble to the Chat screen with the contents of that message.

Setting up the server

If you’re new to web services, you may want to check out Ray’s tutorial on PHP and MySQL first.

We are going to use MAMP to set up a server and database on our own Mac. This is an excellent solution for when your app is still in the development stage. MAMP is very easy to set up and you don’t need to pay for a separate server.

The only requirement is that your Mac and your iPhone share the same local network, otherwise the app will not be able to communicate with the server. Most people will have WiFi at home, so this shouldn’t be too much of a problem.

Of course, once your app is ready for submission to the App Store, you should set up a real server that is connected to the internet. ;-)

You can download MAMP for free. (There is also a paid PRO version, but the free one is good enough for our purposes.)

MAMP contains the Apache web server, the PHP scripting language, and the MySQL database. We will be using all three of these. (If you want to use the server code from this tutorial on a PHP installation other than MAMP, you will have to make sure the following extensions are installed and enabled: PDO, pdo_mysql, mbstring, OpenSSL.)

Installing MAMP is easy. Unzip the download and open the DMG file. Accept the license agreement and drag the MAMP folder into your Applications folder. Done!

To start MAMP, go to Applications/MAMP and click on the MAMP icon (the one with the elephant). This should open the MAMP window:

MAMP Main Window

Click the Open start page button. Your favorite web browser should open and you should see a welcome page:

MAMP Start Page

Great! Now download the PushChatServer code and unzip it. I’m going to assume you unzipped it on your Desktop. The reason I say this is because we need to put the path to these files into the configuration for the Apache web server.

Open Applications/MAMP/conf/apache/httpd.conf and add the following lines:

Listen 44447 <VirtualHost *:44447>DocumentRoot "/Users/matthijs/Desktop/PushChatServer/api"ServerName 192.168.2.244:44447ServerAlias pushchat.localCustomLog "/Users/matthijs/Desktop/PushChatServer/log/apache_access.log" combinedErrorLog "/Users/matthijs/Desktop/PushChatServer/log/apache_error.log" SetEnv APPLICATION_ENV developmentphp_flag magic_quotes_gpc off <Directory "/Users/matthijs/Desktop/PushChatServer/api">Options Indexes MultiViews FollowSymLinksAllowOverride AllOrder allow,denyAllow from all</Directory></VirtualHost>

There are a few lines you will need to change. The path to where I installed the PushChatServer files is “/Users/matthijs/Desktop”. Change those to where you unzipped the files.

Change the IP address in the “ServerName” line to the IP address of your Mac. If you don’t know how to find the IP address, open System Preferences and go to the Network panel.

Getting your IP Address on Mac OSX

I used the IP address of my MacBook’s WiFi but the Ethernet port’s IP address would have been fine too. Note that you should still keep the port number 44447 in ServerName:

ServerName <your IP address>:44447

Our web service runs on port 44447. This is an arbitrary number. Web sites usually run on port 80 and the default MAMP web page runs on port 8888. I just picked something that wouldn’t conflict with either of these.

I also defined “pushchat.local” as an alias for the server name. This is like a domain name except that it only works on the local network. We have to bind that name to an IP address. The easiest way to do this is to edit the file “/etc/hosts”. Add the following line to the bottom of this file and save it:

127.0.0.1       pushchat.local

In the MAMP window, click Stop Servers. When the lights have turned red, click Start Servers. If your changes to httpd.conf contain no errors, then both server lights should turn green again.

Now point your favorite web browser to http://pushchat.local:44447. You should see the message:

If you can see this, it works!

Awesome. This means Apache and the PHP code for the server API were installed properly. Now we have to configure the database.

Setting up the database

Return to the MAMP start page (you can do this by clicking Open start page in the MAMP window) and click the phpMyAdmin button at the top. The screen should now look like this:

phpMyAdmin - Create Database

Type “pushchat” in the Create Database field and choose “utf8_general_ci” for Collation. Then clickCreate to create the database. You should see a message that the database was successfully created.

Click the Privileges tab at the top of the screen, followed by Add a new user.

phpMyAdmin - Add User

I filled in the fields as follows:

  • User name: pushchat
  • Host: localhost
  • Password: d]682\#%yI1nb3
  • Privileges: Check “Grant all privileges on database “pushchat””

Click the Go button to add the user. It’s OK if you choose another password but then you will have to remember to update the password in the various PHP scripts as well.

That’s the database and the user. Now we need to add the tables to the database. Click theSQL tab at the top of the screen and paste the following into the text box:

USE pushchat; SET NAMES utf8; DROP TABLE IF EXISTS active_users; CREATE TABLE active_users(udid varchar(40) NOT NULL PRIMARY KEY,device_token varchar(64) NOT NULL,nickname varchar(255) NOT NULL,secret_code varchar(255) NOT NULL,ip_address varchar(32) NOT NULL)ENGINE=InnoDB DEFAULT CHARSET=utf8;

(You can also find these statements in the file PushChatServer/database/api.sql.)

Click the Go button to execute these statements. This adds a single table, active_users, to the database. When the server API receives a JOIN command, we will add a new record for that user to this table. Conversely, when a LEAVE command is issued, we will remove the user’s record from this table.

There is another table we need to add. Repeat the above procedure with these statements:

USE pushchat; SET NAMES utf8; DROP TABLE IF EXISTS push_queue; CREATE TABLE push_queue(message_id integer NOT NULL AUTO_INCREMENT,device_token varchar(64) NOT NULL,payload varchar(256) NOT NULL,time_queued datetime NOT NULL,time_sent datetime,PRIMARY KEY (message_id))ENGINE=InnoDB DEFAULT CHARSET=latin1;

(You can find these statements in the file PushChatServer/database/push.sql.)

Whenever the server receives a MESSAGE command, it adds the push notifications to this table.

Configuring the server API

I don’t have much room in this tutorial to describe how the server API works but I have extensively commented the api.php source, so even if you know only little PHP you should be able to follow what is going on. So please take a peek!

Of particular interest to us at this point is the file api_config.php. This file contains the configuration options for the server API.

Currently there are two sets of configuration options, one for development mode and one for production more. I find it useful to have two sets of settings that you can easily switch between.

But how do we tell the API whether it runs in development mode or production mode? The answer is in the Apache VirtualHost configuration. Recall that we added a <VirtualHost> block to httpd.conf:

<VirtualHost *:44447>… SetEnv APPLICATION_ENV development ...</VirtualHost>

The directive “SetEnv APPLICATION_ENV development” creates an environment variable that determines in which mode the script runs. If you want to switch to the production configuration, either remove this line or change the word “development” to “production”.

If you’ve used the same value for the database name, username and password that I have, then you shouldn’t have to change anything in api_config.php. However, if you chose different values then you’ll have to enter them here, otherwise the API won’t be able to connect to the database.

Surf with your favorite browser to: http://pushchat.local:44447/test/database.php. You should see the following message:

Database connection successful!

Tip: If you get unexplained errors, then check the PHP error log in Applications/MAMP/logs/php_error.log and the Apache log in PushChatServer/log/apache_error.log. These log files may provide useful hints as to what is wrong.

That should complete the setup for the server API.

Let’s do some programming already!

Download the PushChatStarter code and unzip it. This is the PushChat app without the networking and push code. I will quickly explain how the app works and then we’ll add in the code that lets it talk to the server and receive push notifications. After all, that’s what you came here for. ;-)

You can run PushChat in the simulator for now because it doesn’t have any push functionality yet.

First, open the project in Xcode and go to the Target Settings. You have to change the bundle ID from “com.hollance.PushChat” to your own bundle ID that you chose all the way back when we made the App ID in the iOS Provisioning Portal (com.yoursite.PushChat). This is important because the app needs to be signed with your provisioning profile.

PushChat - Target Settings

Build & Run the app and play with it for a bit. Because this new app uses the same bundle ID as the very basic test app that we made earlier, it’s a good idea to uninstall that app before you Build & Run this one, just to make sure your iPhone doesn’t get confused.

Open up MainWindow.xib to take a peek at how the app is structured:

Push Chat - MainWindow.nib

The main window contains a navigation controller that has the ChatViewController at its root. That view controller shows the sent and received messages. As you can see in the nib, it’s a regular UITableView controller.

The speech bubbles are drawn by MessageTableViewCell, which is a subclass of UITableViewCell, and SpeechBubbleView, which is a subclass of UIView that does some custom drawing. This is plain old table view programming and you’ve no doubt seen it before, so I won’t waste any time on explaining how this works.

The other two screens of the app, the Login screen and the Compose screen, are modal view controllers that are displayed on top of the ChatViewController. You can probably guess, but the Login screen is implemented in LoginViewController and the Compose screen in ComposeViewController. Both have their own nib. These are very simple screens; check out the source code for more details.

That leaves me the two data model classes, DataModel and Message. Message describes the content of a single message that was either sent by this user or received from another user. Each Message has a sender name, a date, and the actual message text.

The DataModel class is responsible for the list of these Message objects. When a new message is sent or received, DataModel adds it to the list and then saves this list to a file in the app’s Documents directory. When the app starts up, DataModel loads the messages from this file.

That concludes our very brief overview of this app. I suggest you play a little with the source code before we continue. It has plenty of comments that should explain how everything works.

Talking With the Server

We will now go through the various screens in the app and add the code that lets it communicate with the server. Our server expects the app to send HTTP POST requests, so I am going to use ASIFormDataRequest to send data to the server. All the required classes have already been added to the PushChat starter code, so there is no need to install more stuff.

If you’re new to using web services from iOS apps, I suggest you take a small detour and readRay’s previous tutorial on this topic.

Add the following line to defs.h:

#define ServerApiURL @"http://192.168.2.244:44447/api.php"

This is the URL that we will be sending our HTTP POST requests to. You should change the IP address to point to your server. Also make sure that MAMP is currently active, so that the server is actually available, and that your iPhone is on the same network as the server.

The Login screen seems like a good place to start. When the user presses the Start! Button, we should send a JOIN command to the server. This lets the server know that there is a new user. The server will then add this user to its active_users table.

Add the following lines to the top of LoginViewController:

#import "ASIFormDataRequest.h"#import "MBProgressHUD.h"

Add the following chunk of code between the userDidJoin and loginAction methods:

- (void)postJoinRequest{MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];hud.labelText = NSLocalizedString(@"Connecting", nil)NSURL* url = [NSURL URLWithString:ServerApiURL];__block ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setDelegate:self][request setPostValue:@"join" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"];[request setPostValue:[dataModel deviceToken] forKey:@"token"];[request setPostValue:[dataModel nickname] forKey:@"name"];[request setPostValue:[dataModel secretCode] forKey:@"code"][request setCompletionBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES]if ([request responseStatusCode] != 200){ShowErrorAlert(NSLocalizedString(@"There was an error communicating with the server", nil));}else{[self userDidJoin];}}}][request setFailedBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];ShowErrorAlert([[request error] localizedDescription]);}}][request startAsynchronous];}

Yikes, what is all of this? Let’s go through it line by line.

MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];hud.labelText = NSLocalizedString(@"Connecting", nil);

This shows an activity spinner that blocks the whole screen. You can read more about MBProgressHUD in Ray’s article.

NSURL* url = [NSURL URLWithString:ServerApiURL];__block ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setDelegate:self];

This creates an ASIFormDataRequest object for our server’s URL. ASIFormDataRequest is a great class for sending POST requests to servers. You tell it the URL, you give it the POST values, and off it goes. I’ll explain about the __block thingy in a minute.

[request setPostValue:@"join" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"];[request setPostValue:[dataModel deviceToken] forKey:@"token"];[request setPostValue:[dataModel nickname] forKey:@"name"];[request setPostValue:[dataModel secretCode] forKey:@"code"];

Here we add the POST values to the request. I designed the server API to look for a field named “cmd” in the POST data. The value of this field determines which command the API will execute. In this case, it is the “join” command.

Join takes four parameters. The user’s nickname and the secret code are obvious. These are what the user typed into the input fields on the Login screen. But what are “udid” and “token”? As you might guess, token is the push device token. This is where we let the server know what address to put on the push notifications that it wants to send to this user.

The UDID is the device’s unique identifier. I chose to identify users in the server’s database by the ID of their device. I could have used the nickname instead but then no two persons could have the same nickname and we would need some way to let the app know that a nickname was already taken.

I could also have used the device token to uniquely identify a user, but the device token may change from time to time. The UDID never changes. I’m not saying that this approach should be used for all web services, but for our purposes it is sufficient.

OK, now it becomes interesting:

[request setCompletionBlock:^{...}];

We are using blocks! There are two ways to be notified of the results of an ASIFormDataRequest. The old school way is to use the delegate methods requestFinished and requestedFailed – this is what Ray did in his article on using web services.

This approach works fine but in OS 4.0, blocks were introduced as an alternative for delegates. Just for the fun of it, I decided to use blocks in this tutorial. Here we set the completion block. The code in between the ^{ } brackets will not be executed right now, but when the request was successfully completed.

Remember the __block specifier that I used when I created the ASIFormDataRequest object? That prevents the block from keeping a reference to the request. Because the request already keeps a reference to the block, this would lead to a retain cycle and a memory leak. If that blows your mind, then don’t worry about it for now. Just remember to put __block in front of ASIFormDataRequest when you’re using blocks instead of the delegate methods.

Inside the block we do:

if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];

First, we check if the view is still loaded. Because the request will be performed asynchronously in the background, it might take a while to complete. Theoretically it is possible that our view is unloaded in the mean time, for example because of a low memory warning. In that case, we will simply ignore the fact that this request ever happened.

This is probably a little overkill for an app as simple as this one, but it’s a bit of defensive programming on my part. It’s always a good idea to keep these kinds of strange situations in mind, especially when you’re doing things in the background.

The rest of the completion block looks like this:

if ([request responseStatusCode] != 200){ShowErrorAlert(NSLocalizedString(@"There was an error communicating with the server", nil));}else{[self userDidJoin];}

We check the status code of the response. This is the HTTP status code that the server API sends back to the app. If everything went right, as it normally does, this status code is “200 OK”. However, computers and the internet are fickle and sometimes things go wrong. For example if the MySQL database goes offline, the server API will return “500 Server Error”. We shown an alert view in such cases.

The ShowErrorAlert() function is a helper function that creates a UIAlertView and shows it. Also note that I use NSLocalizedString() whenever I create a string for showing on the screen. That is a habit I got into, which makes it a lot easier later to localize your apps. See also Sean Berry’s tutorial on localization.

If there were no errors, we call the userDidJoin method. This method tells the DataModel that the user has successfully joined the chat and then it closes the modal view controller, which returns the user to the main screen.

Because who knows what might happen on the internet, we also set a block that is executed when the request fails for some reason. Often this happens because the server could not be reached, or the request took too long (time out).

[request setFailedBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];ShowErrorAlert([[request error] localizedDescription]);}}];

Here we simply hide the spinner and show an error alert view.

[request startAsynchronous];

Finally, we tell the request to do its thing. You should always use asynchronous requests when talking to a server. The request may take many seconds to complete. If you used a synchronous request, your app would become totally unresponsive for the entire duration of the request. And if it is unresponsive for too long, the OS will terminate your app.

Still in LoginViewController.m, make the following change to loginAction:

- (IBAction)loginAction{... // REPLACE THIS LINE:[self userDidJoin]// WITH:[self postJoinRequest];}

Previously, we faked the login by calling userDidJoin directly when the Start! button was pressed. That will no longer fly, and we will only call userDidJoin when the request to the server was successfully completed.

If you were paying close attention, you might have noticed that we called two methods on DataModel that aren’t defined yet: udid and deviceToken. Let’s add these and get rid of those compiler warnings.

In DataModel.h, add:

- (NSString*)udid;- (NSString*)deviceToken;- (void)setDeviceToken:(NSString*)token;

In DataModel.m, add the following code inside the @implementation block:

- (NSString*)udid{UIDevice* device = [UIDevice currentDevice];return [device.uniqueIdentifier stringByReplacingOccurrencesOfString:@"-" withString:@""];}

This obtains the device’s UDID. By default the UDID has dashes in it but we just want to have it as a string of 40 characters, so we strip out the dashes. (FYI: On the simulator the device ID is only 32 characters.)

Still in DataModel.m, add the following line to the top of the file, above the @implementation block:

static NSString* const DeviceTokenKey = @"DeviceToken";

And the following function instead the @implementation:

- (NSString*)deviceToken{return [[NSUserDefaults standardUserDefaults] stringForKey:DeviceTokenKey];} - (void)setDeviceToken:(NSString*)token{[[NSUserDefaults standardUserDefaults] setObject:token forKey:DeviceTokenKey];}

There is one more small change to make to the initialize method:

+ (void)initialize{if (self == [DataModel class]){[[NSUserDefaults standardUserDefaults] registerDefaults:[NSDictionary dictionaryWithObjectsAndKeys:@"", NicknameKey,@"", SecretCodeKey,[NSNumber numberWithInt:0], JoinedChatKey, // ADD THIS LINE:@"0", DeviceTokenKey, nil]];}}

What did we do here? In setDeviceToken, we store the token in the NSUserDefaults dictionary. In deviceToken we read it from NSUserDefaults again. If you’re not familiar with NSUserDefaults, it’s a simple but very useful class that lets you store settings for your app.

In the initialize method, which is called by the Objective-C runtime when the DataModel class is used for the very first time, we set default values for all the settings we store in NSUserDefaults.

The line we added sets the default value for the device token to the string @”0″. I will explain later why that is necessary. For now, it means that when we send the JOIN command to our server, it passes along a device token that is @”0″ instead of the real token.

Phew, that was a lot to take in. Let’s Build & Run the sucker and see if it actually works.

PushChat - Connecting with MBProgressHUD

Enter a nickname and a secret code and press Start! I used “MisterX” and “TopSecret”. The “Connecting” spinner should appear briefly and if the communication with the server went well, the Login screen disappears and the main screen shows up.

If instead you got an error message, then here are some troubleshooting tips: Make sure MAMP is running. There should be green lights in front of both Apache Server and MySQL Server. Make sure you can access the server’s IP address using your web browser. Also verify you put the proper server IP address in defs.h. Finally, your iPhone should be able to reach your server, which requires them to be part of the same local network.

It would be nice if we had a way to verify that the server actually received and processed the message. Well, it turns out we do. After receiving a JOIN request, the server API adds a record for this user to the active_users database table. We can use phpMyAdmin to look at the contents of this table and verify that this really took place.

Click on MAMP’s Open start page button and click the phpMyAdmin tab at the top of the screen. Navigate to the pushchat database, pick the active_users table, and click the Browse tab. You should see something like this:

phpMyAdmin - After Join Request

The table now contains one row for MisterX, with secret code TopSecret, and device token “0”. The server API also logs the IP address of the device that made the request.

I kept it simple for this tutorial but for a real web service I would log as much data as possible, such as the version of the app that made the request, the OS version and type of the user’s device, the time the request was made, and so on. This kind of analytics data is invaluable to track down problems, to prevent abuse, and to get insight into who your users are and how they use your app.

Finishing up the server communication

If you understood what we did to LoginViewController, then you should be able to follow along with the rest of the changes without a problem, because they are basically the same.

Add the following lines to the top of ChatViewController.m:

#import "ASIFormDataRequest.h"#import "MBProgressHUD.h"

Between userDidLeave and exitAction, add:

- (void)postLeaveRequest{MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.navigationController.view animated:YES];hud.labelText = NSLocalizedString(@"Signing Out", nil)NSURL* url = [NSURL URLWithString:ServerApiURL];__block ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setDelegate:self][request setPostValue:@"leave" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"][request setCompletionBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.navigationController.view animated:YES]if ([request responseStatusCode] != 200){ShowErrorAlert(NSLocalizedString(@"There was an error communicating with the server", nil));}else{[self userDidLeave];}}}][request setFailedBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.navigationController.view animated:YES];ShowErrorAlert([[request error] localizedDescription]);}}][request startAsynchronous];}

This should look familiar. We create an ASIFormDataRequest with a “cmd” value of “leave”. The only other parameter is the UDID, which is how we identify which user wants to sign out. The rest of the code is nearly identical to what we did before, except now we call userDidLeave when the request successfully completes.

Replace exitAction with:

- (IBAction)exitAction{[self postLeaveRequest];}

Build & Run and tap the Exit button on the main screen. Now go into phpMyAdmin and refresh the active_users table. It should be empty. When the server API received the LEAVE command, it removed the user’s record from the table.

Our final changes are to ComposeViewController.m. Add to the top of the file:

#import "ASIFormDataRequest.h"#import "MBProgressHUD.h"

Between userDidCompose and cancelAction, add:

- (void)postMessageRequest{[messageTextView resignFirstResponder]; MBProgressHUD* hud = [MBProgressHUD showHUDAddedTo:self.view animated:YES];hud.labelText = NSLocalizedString(@"Sending", nil)NSString* text = self.messageTextView.text; NSURL* url = [NSURL URLWithString:ServerApiURL];__block ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setDelegate:self][request setPostValue:@"message" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"];[request setPostValue:text forKey:@"text"][request setCompletionBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES]if ([request responseStatusCode] != 200){ShowErrorAlert(NSLocalizedString(@"Could not send the message to the server", nil));}else{[self userDidCompose:text];}}}][request setFailedBlock:^{if ([self isViewLoaded]){[MBProgressHUD hideHUDForView:self.view animated:YES];ShowErrorAlert([[request error] localizedDescription]);}}][request startAsynchronous];}

More of the same. The only difference is that we hide the keyboard first; for some reason the spinner shows up partially behind it and that is ugly. Then we send the “message” command to the API, along with the user’s UDID and the message text. If all goes well, we call userDidCompose, which adds a new Message object to the DataModel and shows it on the main screen.

Replace saveAction with:

- (IBAction)saveAction{[self postMessageRequest];}

Build & Run once more. Login and tap the compose button at the top-right of the screen. Write some excellent prose and tap Save. After a few seconds of network activity, your message should show up in a new speech bubble.

Preparing for push

It took a while, but we’re finally ready to add push notifications to the app. We’ve already seen how to register for push notifications and how to obtain the device token. Let’s add that again to AppDelegate.m.

In application:didFinishLaunchingWithOptions:, add the following just before the return statement:

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions{... [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)]return YES;}

We will register for sounds and alert messages. Our app doesn’t put a badge on its icon, so there’s no point in registering for that kind of notification.

At the bottom of the AppDelegate.m, just before @end, add:

- (void)application:(UIApplication*)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken{NSString* oldToken = [dataModel deviceToken]NSString* newToken = [deviceToken description];newToken = [newToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];newToken = [newToken stringByReplacingOccurrencesOfString:@" " withString:@""]; NSLog(@"My token is: %@", newToken)[dataModel setDeviceToken:newToken]if ([dataModel joinedChat] && ![newToken isEqualToString:oldToken]){[self postUpdateRequest];}} - (void)application:(UIApplication*)application didFailToRegisterForRemoteNotificationsWithError:(NSError*)error{NSLog(@"Failed to get token, error: %@", error);}

You’ve seen these methods before; this is how we obtain the device token. Let’s look at didRegisterForRemoteNotificationsWithDeviceToken in a little more detail:

NSString* newToken = [deviceToken description];newToken = [newToken stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"<>"]];newToken = [newToken stringByReplacingOccurrencesOfString:@" " withString:@""];

You’ve seen before that a device token looks like this:

<0f744707 bebcf74f 9b7c25d4 8e335894 5f6aa01d a5ddb387 462c7eaf 61bbad78>

But it’s more convenient to deal with in this format:

0f744707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bbad78

The code snippet above converts the former into the latter.

The following bit of code requires some explanation:

if ([dataModel joinedChat] && ![newToken isEqualToString:oldToken]){[self postUpdateRequest];}

The didRegisterForRemoteNotificationsWithDeviceToken method is usually called right away after you’ve registered for push notifications. Usually, but not always. Obtaining the device token happens asynchronously and it could take up to a few seconds, especially if your app hasn’t tried to obtain a device token before (i.e. the very first time it is run).

It is theoretically possible that the user has already clicked the Start! button on the Login screen before we have received a valid device token. In that case, the app will send the default value for the device token, @”0″, to the server. That is obviously not a valid token and we cannot send push notifications to it.

If we receive the device token after the user has already joined a chat, then we have to let the server know about this as soon as we get it. That is what the “update” API command is for. It ensures that the server API always has the most up-to-date token for this user’s device. (We only send the UPDATE command when the device token has actually changed, not every time the app starts.)

Add the following code above didRegisterForRemoteNotificationsWithDeviceToken:

- (void)postUpdateRequest{NSURL* url = [NSURL URLWithString:ServerApiURL];ASIFormDataRequest* request = [ASIFormDataRequest requestWithURL:url];[request setPostValue:@"update" forKey:@"cmd"];[request setPostValue:[dataModel udid] forKey:@"udid"];[request setPostValue:[dataModel deviceToken] forKey:@"token"];[request setDelegate:self];[request startAsynchronous];}

Don’t forget to import the proper headers:

#import "ASIFormDataRequest.h"

We don’t care about what happens to this ASIFormDataRequest. It is silently performed in the background. So if it fails, we don’t show an error message.

This completes all the communication that the app sends to the server API. Now let’s take a look at what the server does when it receives a message and how it turns that message into a push notification.

Push on the server

In the PushChatServer directory there is a push folder that contains the PHP scripts you need to send out push requests. You should put these files in a directory on the server that is not accessible from the web, in other words outside of your DocumentRoot. This is important because you don’t want visitors to your website to download your private key! (In our MAMP setup, this is already taken care of.)

The most important script in the push folder is push.php. This script should be run as a background process on your server. Every few seconds it checks if there are new push notifications to be sent out. If so, it sends them to the Apple Push Notification Service.

First, we need to edit the file push_config.php, which contains the configuration options for push.php. You may need to change the passphrase for the private key and possibly the database password.

As with the server API, the push script can run in either development mode or production mode. In development mode, it talks to the APNS sandbox server and it uses your development SSL certificate. You should use development mode in combination with Debug builds of your app. Production mode should be used for Ad Hoc and App Store builds of your app.

There is a file named ck_development.pem in the push folder. Replace it with your own PEM file that we created way back in the beginning of this tutorial.

Now open a new Terminal window and execute the following command:

$ /Applications/MAMP/bin/php5.2/bin/php push.php development

This starts the push.php script in development mode. Note that we are now using MAMP’s version of PHP, not the PHP that comes pre-installed with your Mac. This is important because otherwise the script cannot connect to the MySQL database.

The push.php script should not exit. If it does, then something went wrong. Take a peek at the push_development.log file in the log folder. Mine looks like this:

2011-05-06T16:32:19+02:00 Push script started (development mode)2011-05-06T16:32:19+02:00 Connecting to gateway.sandbox.push.apple.com:21952011-05-06T16:32:21+02:00 Connection OK

Because the push.php script should be run as a background process, it cannot write its output to the console. Instead, it writes to a log file. Every time it sends out a push notification, a line is added to the log.

Note that currently we’re not actually running push.php as a background process. I don’t like to do that for development purposes. (If you want to stop push.php, press Ctrl+C.) However, on your production server you should start the script as follows:

$ /Applications/MAMP/bin/php5.2/bin/php push.php production &

The “&” will detach the script from the shell and put it in the background.

So what does push.php do? First, it opens a secure connection to the APNS server. It keeps that connection open all the time. Many of the examples for push that I’ve seen create a new connection to APNS for each push message they deliver. That approach is discouraged by Apple. Setting up a new secure connection over and over is very costly in terms of processing power and network resources. It’s better to just keep the connection open.

Once the connection is established, the script goes into an endless loop. Each time through the loop it checks the push_queue database table for new entries. It can recognize new entries by their “time_sent” field. If the value of time_sent is NULL, then this notification hasn’t been sent yet. The script creates a binary package that contains the device token and the JSON payload and sends it to the APNS server.

If you’re interested in learning about this binary format, I suggest you take a look at the The Binary Interface and Notification Formats chapter in theLocal and Push Notification Programming Guide. It’s a good idea to read that guide anyway!

Once push.php has sent out the new notifications, it fills in their time_sent fields with the current time. Then the script goes to sleep for a few seconds, and the loop repeats.

This means that to send a push notification all you have to do is place a new record in the push_queue table. That is exactly what the server API script does when it receives a MESSAGE request from the iPhone app. A few seconds later, push.php wakes up, sees the new record and sends out the notification.

Can we finally send some push notifications!?

If you have two iPhones, you could install the app on both devices and have both users join with the same secret code. When one user sends a message to the server, the API should send a push notification to the other user.

But what if you don’t have two devices to test with? Well, in that case we just have to pretend we have another user to chat with. Point your browser to:http://pushchat.local:44447/test/api_join.html

This shows a very basic HTML form:

Web page to test JOIN message in web service

We can use this form to send a POST request to the server API, just like our app does. The server can’t really tell the difference, so this is a useful tool to fake (and test) the API commands.

Fill in a 40-character UDID, a 64-character device token, and a nickname. The secret code should be the same as the one the app is signed into. It doesn’t really matter what the UDID and device token are, as long as they are different from the ones the app uses. Otherwise the server API obviously won’t recognize this as a different user.

Press the Submit button. You can verify that a new user signed in by browsing the active_users table with phpMyAdmin.

Now point your browser to: http://pushchat.local:44447/test/api_message.html

Make sure the UDID is the one from the fake user, type a message text, and press Submit. Tadaa… within a few seconds your iPhone should beep and display the push notification. Congrats!

If you don’t see anything, close the app first and try again. We haven’t actually added any code in the app to handle incoming push notifications, so the notification will only show up when the app isn’t currently running.

If you get no notification at all, then make sure push.php is still running and check the push_development.log. It should say something like:

2011-05-06T23:57:29+02:00 Sending message 1 to '0f744707bebcf74f9b7c25d48e3358945f6aa01da5ddb387462c7eaf61bbad78',payload: '{"aps":{"alert":"SteveJ: Hello, world!","sound":"default"}}'2011-05-06T23:57:29+02:00 Message successfully delivered

You can also verify with phpMyAdmin that a new record was added to the push_queue database table.

It may take a while for push notifications to be delivered, and as I mentioned before, they sometimes may not be delivered at all. Keep trying it a few more times and see if you get lucky.

Receiving push messages in the app

So what happens when your iPhone receives a push notification? There are three possible situations:

  • The app is currently running in the foreground. Nothing happens on the screen and no sound is played, but your app delegate gets a notification. It is up to the app to do something in response to the notification.
  • The app is closed, either the iPhone’s home screen is active or some other app is running. An alert view pops up with the message text and a sound is played. The user can press Close to dismiss the notification or View to open your app. If the user presses Close, then your app will never be told about this notification.
  • The iPhone is locked. Now also an alert view pops up and a sound is played but the alert does not have Close/View buttons. Instead, unlocking the phone opens the app.

The final modifications to our app all take place in AppDelegate.m, as the application delegate is the one who receives the push notifications. There are two places for this:

  1. application:didFinishLaunchingWithOptions:. If your app wasn’t running when the notification came in, it is launched and the notification is passed as part of the launchOptions dictionary.
  2. application:didReceiveRemoteNotification:. This method is invoked if your app is active when a notification comes in. On iOS 4.0 or later, if your app was suspended in the background it is woken up and this method is also called. You can use UIApplication’s applicationState property to find out whether your app was suspended or not.

Both methods receive a dictionary that contains the JSON payload. We don’t have to parse the JSON data ourselves, that is already done for us by the OS.

Add the following lines to didFinishLaunchingWithOptions:, just before the return statement:

if (launchOptions != nil){NSDictionary* dictionary = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];if (dictionary != nil){NSLog(@"Launched from push notification: %@", dictionary);[self addMessageFromRemoteNotification:dictionary updateUI:NO];}}

This checks to see if there are launch options and whether they contain a push notification. If so, we call the addMessageFromRemoteNotification method to handle the notification.

Add the following method to AppDelegate.m:

- (void)application:(UIApplication*)application didReceiveRemoteNotification:(NSDictionary*)userInfo{NSLog(@"Received notification: %@", userInfo);[self addMessageFromRemoteNotification:userInfo updateUI:YES];}

Pretty simple. This method also depends on addMessageFromRemoteNotification to do all the work.

Copy-paste the following method above didFinishLaunchingWithOptions:

- (void)addMessageFromRemoteNotification:(NSDictionary*)userInfo updateUI:(BOOL)updateUI{Message* message = [[Message alloc] init];message.date = [NSDate date]NSString* alertValue = [[userInfo valueForKey:@"aps"] valueForKey:@"alert"]NSMutableArray* parts = [NSMutableArray arrayWithArray:[alertValue componentsSeparatedByString:@": "]];message.senderName = [parts objectAtIndex:0];[parts removeObjectAtIndex:0];message.text = [parts componentsJoinedByString:@": "]int index = [dataModel addMessage:message]if (updateUI)[self.chatViewController didSaveMessage:message atIndex:index][message release];}

This is the last bit of code, I promise! Let’s go through it bit by bit.

Message* message = [[Message alloc] init];message.date = [NSDate date];

First we create a new Message object. We will fill it up with the contents of the push notification and add it to the DataModel momentarily.

NSString* alertValue = [[userInfo valueForKey:@"aps"] valueForKey:@"alert"];

This obtains the alert text from the push notification. The JSON payload of our push notifications looks like this:

{"aps":{"alert": "SENDER_NAME: MESSAGE_TEXT","sound": "default"},}

The server has put the message text and the sender name into the “alert” field. We’re not really interested in any of the other fields from this dictionary.

NSMutableArray* parts = [NSMutableArray arrayWithArray:[alertValue componentsSeparatedByString:@": "]];message.senderName = [parts objectAtIndex:0];[parts removeObjectAtIndex:0];message.text = [parts componentsJoinedByString:@": "];

This code extracts the sender name and the message text and puts them into the Message object. The sender name is the text until the first colon followed by a space.

int index = [dataModel addMessage:message];

Now that we’ve set the properties of the Message object, we can add it to the DataModel.

if (updateUI)[self.chatViewController didSaveMessage:message atIndex:index];

Finally, we tell the ChatViewController to insert a new row in its table view. However, we shouldn’t do this when the notification is received in didFinishLaunchingWithOptions. At that point, the table view isn’t loaded yet. If we try to insert a row it will get confused and the application will crash.

That’s it. Build & Run once more. Use the test_message.html form to send a push notification and now a speech bubble should be added on the left-hand side of the screen whenever the app receives a new message. Woot!

Customizing the notifications

You may recall from the earlier discussion on the anatomy of a push notification that you can do more than send text. It is also possible to change the sound that the phone makes when it receives a new notification. I have added a short sound file, beep.caf, to the app’s resources.

Go to api.php and change the following line in the makePayload() function from this:

$payload = '{"aps":{"alert":"' . $nameJson . ': ' . $textJson . '","sound":"default"}}';

Into this:

$payload = '{"aps":{"alert":"' . $nameJson . ': ' . $textJson . '","sound":"beep.caf"}}';

You don’t have to change anything in the app or recompile it. Do close the app on the device, though. When notifications are received while the app is running no sound is played, which will sort of spoil this demonstration. Now use test_message.html to send another push notification. When the alert pops up on your phone, the sound should be different.

Play with the other parameters too! Try to localize the buttons, or set a badge on the app icon. (Note: if you want to play with badges, don’t forget to make the app also register for badge notifications. Currently it only listens for sounds and alerts.)

Have fun!

The feedback server

APNS is happy to give you feedback!

APNS is happy to give you feedback!

You’re probably ready to fall asleep by now (feel free to take a short break!), but there is one more piece of code that a proper push server needs.

Imagine the following scenario: Your server has a database table with the device tokens of many users. At some point, some of these users will remove your app from their devices. A sad, but unavoidable fact of life.

However, your server will not know about this as no one tells it about these uninstalls. It will happily continue to send push notifications that can no longer be delivered as your app is not present to receive them.

This is clearly an undesirable situation and that is where the Feedback Service comes in. You are supposed to connect to this service periodically and download a list of device tokens that are no longer valid. You should stop sending push notifications to these device tokens.

In the PushChatServer/push folder you will find the files feedback.php, which is the script that connects to the Feedback Service and downloads the tokens, and feedback_config.php, which contains the configuration options. This script also uses your PEM file with the SSL certificate and private key to authenticate with the Apple servers.

Unlike push.php, the feedback script should not be run continuously as a background process. Instead, you should set up a cron job on the server that launches it every hour or so.

The script sets up a connection to the Feedback Service, downloads the list of expired device tokens, and closes the connection. Then it simply deletes the records from active_users that correspond to these tokens.

Improvements and limitations

PushChat uses push notifications as the sole mechanism of delivering data to the app. That works OK for this tutorial, but it has a big problem. The delivery of push notifications is not guaranteed, so the user may miss a piece of data if one of the notifications goes missing. In addition, if the user presses the Close button on the push alert, the app will never see the notification. In the case of PushChat, no speech bubble will be added for such messages.

A better approach is to keep the data on the server. Our API could put each message into a “messages” database table. When the app is opened, either by the user directly or in response to a push notification, it should send a “give me all new messages” request to the server and then download these messages. This way the app doesn’t depend on the push notifications alone to receive the data.

The push.php script works reasonably well but I wouldn’t use it if your app has lots of users. PHP isn’t the tool of choice for heavy-duty networking software. If you have ambitious plans it may be worth switching to a more robust implementation in C or C++. It’s worth checking out the following code for an alternative PHP implementation:http://code.google.com/p/php-apns/.

Where To Go From Here?

Here is a sample project with all of the code we’ve developed in the above tutorial.

Even though this tutorial is already the length of a short novel, there is still much more to be said about push notifications. If you’re serious about adding push to your app, I suggest you study the following resources for the do’s and don’ts:

  • Apple’s Local and Push Notification Programming Guide
  • Section 5 of the App Store Approval Guidelines
  • Technical Note TN2265
  • Session 129 of the WWDC 2010 videos. Most of this talk is about local notifications but there’s some discussion on push in the beginning.

If you have any questions, comments, or suggestions, please join in the forum discussion below!

Matthijs Hollemans

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced freelance iOS developer available for hire!

Tweet

原创粉丝点击