Talk Funnel

Ramin Firoozye's Public Whisperings

Archive for September, 2009

Problems with Push

without comments

pushsad.png

I agree with most everything Karl Adam says about the limitations of the Apple Push Notification Service, especially the problem with its failure to stack notifications so they’re not missed.

I posted a bug report a while back (rdar://7054632) offering a simple solution to get around this particular problem: save each incoming push payload into Messages.app as a separate entry. That way if I get a push and don’t have time to get to it I can ignore it and come back to Messages later on and retrieve it and all received Push messages are kept until I choose to get rid of them.

The entry could be in the form of a special URL link that shows the alert message, but when clicked generates the same JSON payload format as a regular push event and invokes the app in the same manner so no extra coding would be needed (OK, maybe just a little bit of code on the server to check against processing duplicate requests). It would take care of a lot of problems with push usability.

An even more pressing issue I have with Push is if you are on a WiFi network behind a bunch of firewalls and more than one NAT server. This happens often in corporations or in homes with multiple routers acting as range-extenders. In these cases pushes fail to reach you — until you get back to a 3G network.

For some people Push is doubling as a remote event timer (since Apple won’t let us access the phone’s alarm database or submit local cron tasks). This makes it really hard to issue reliable time-based alerts.

If Apple would just open up true background tasks and/or timed alerts and let the user decide whether they trust an app to let it access those services (much like location-based or push services) a lot of these hassles would go away.

Also, a European friend brought up that whereas SMS is included in most phone plans, push incurs data usage charges. Could be a hassle if you’re traveling and continue getting pushes.

Over all, I’d say push on the iPhone is a work in progress. As much as I’m intrigued and excited by its potential, I’m frustrated by its current implementation and limitations.

Written by ramin

September 30th, 2009 at 1:51 am

Posted in Apple,iphone

Tagged with , ,

Semi-Modal (Transparent) Dialogs on the iPhone

with 23 comments

Popping up a modal dialog on the iPhone is a fairly straightforward process:

modalDialogViewController *modalController = [[modalDialogViewController alloc]          initWithNibName:@"modalDialogView" bundle:nil];  [self presentModalViewController:modalController animated:YES]; [modalController release];

Dismissing it then is a simple matter of the modalController invoking:

[self dismissModalViewControllerAnimated:YES];

But what if you want to show only half a page’s worth or maybe you need the underlying view to continue being available for user viewing or interaction. Or maybe you want to show a pop-up toolbar where users are asked to choose something before continuing. You might think “Aha! I’ll just have my modal dialog view be half as tall and make the background transparent.”

Go ahead, give it a try. We’ll wait… (* … the girl from Ipanema goes walking … *)

So now you know that the standard modal dialog can only be full-screen and maintains a solid black background. What’s more you can’t interact with what’s behind it because, you know it’s modal — and modal means users shouldn’t be able to do anything else until they’re done with the front-most task (unless you’re the search box in the Contacts app in which case apparently it’s OK to be kindasortamodal).

So what we’re going to do is have a view that can be modal but takes only part of the top view, the space above it remaining visible. What’s more, you can choose to have it so tapping on the background view hides the modal view, or even go full-bore and let the background remain responsive to user input. This technically makes the view semi-modal so let’s ignore the sirens and the UI Police banging on the door and go with that.

The first thing you need is a view that has something interactive on it. The easiest way to build one is in interface builder, so go ahead and make yourself one. For the sake of expedience make it only a fraction of the screen. Here’s an example of a half-height view along with some user controls. The background is set to fully transparent. The view is connected to a UIViewController that reacts to user input:

modalib.png

In this case, the view is the same height as the whole screen because we want the upper portion to be see-through but not react to user input. If we wanted it to be truly interactive, we could make the view height be as tall as the actual content (i.e. half-screen) but that would make it a bit strange for the user because it would be hard to tell apart the actual content from the modal view. But hey, it’s your app. You can do what you want. Another option is to set the background of this view black and partially transparent. That would look cool and show a nice smoky cover while we’re in modal mode… unless you’re mucking with color (like we are in this example) in which case it’s best to leave it fully transparent. Next throw the following code in the parent UIViewController. Load up the UIViewcontroller/UIView you just created and pass the view to this routine instead of calling the standard presentModalViewController method (substitute your application delegate for MyAppDelegate):

// Use this to show the modal view (pops-up from the bottom)
- (void) showModal:(UIView*) modalView {   UIWindow* mainWindow = (((MyAppDelegate*) [UIApplication sharedApplication].delegate).window);   CGPoint middleCenter = modalView.center;   CGSize offSize = [UIScreen mainScreen].bounds.size;   CGPoint offScreenCenter = CGPointMake(offSize.width / 2.0, offSize.height * 1.5);   modalView.center = offScreenCenter; // we start off-screen   [mainWindow addSubview:modalView];    // Show it with a transition effect     [UIView beginAnimations:nil context:nil];   [UIView setAnimationDuration:0.7]; // animation duration in seconds   modalView.center = middleCenter;
  [UIView commitAnimations]; }

What this does is add your view as a top-level above the main window, effectively rendering it modal. It also uses Core Animation to move the window from offscreen bottom up until it’s fully shown. You should adjust the timing to suit your view’s actual height. I’ve found that the taller the semi-modal view, the more time you should give it to become fully visible. Now let’s go through the hiding action. Note that we use the animation completion handler to do the actual removing of the item from the parent view and cleaning up. We also use the context parameter of the animation call (which was thoughtfully provided for exactly this sort of thing) to keep track of what view to clean up afterward:

// Use this to slide the semi-modal view back down. - (void) hideModal:(UIView*) modalView {   CGSize offSize = [UIScreen mainScreen].bounds.size;   CGPoint offScreenCenter = CGPointMake(offSize.width / 2.0, offSize.height * 1.5);    [UIView beginAnimations:nil context:modalView];   [UIView setAnimationDuration:0.7];   [UIView setAnimationDelegate:self];   [UIView setAnimationDidStopSelector:@selector(hideModalEnded:finished:context:)];   modalView.center = offScreenCenter;   [UIView commitAnimations]; }  - (void) hideModalEnded:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {   UIView* modalView = (UIView *)context; [modalView removeFromSuperview]; } 

[ Update: As noted in the comments, there was an extra release on modalView in hideModalEnded. This code was excerpted from a larger code-base and the release was left in inadvertently. The code listing here has been updated. Thanks for catching it, folks. ]

What I’m not showing you here is the way to trigger the show/hide action. That’s entirely up to you. In standard iPhone modal dialogs this is often a button in the toolbar or navigation bar. In the case of the semi-modal dialog, however, you have even more flexibility. Basically it comes down to the show/hide elements being:

  • Explicit: Provide an Accept or Cancel button on your view.
  • Implicit: You can simulate an action toolbar that shows and hides this way. Put a row of buttons on the view and wire it so tapping each one invokes hideModal before going on to the actual action.
  • Other: Tapping anywhere else on the screen dismisses the dialog. You can do this by placing a full-screen sized view (or custom transparent button) behind your modal dialog and wiring it so it a tap-down action dismisses the dialog . For best results, try making this full-screen view black and semi-transparent (e.g. opacity=0.2). This way the user’s main view darkens so they get a sense your modal dialog is in focus but they still get to see what’s behind.

Here’s a movie of the above semi-modal view in action. It lets the user select a color then confirm or cancel the action. The modal view in this case also has interactive controls on it. As the user changes color the background image changes in real-time so they can visualize what the end-result will be like. Once they’re done they can tap the checkbox or X/cancel buttons to make the modal go away.

modalmovie.png

Play movie

The semi-modal dialog is a handy UI interaction component but it’s important to think about how to dismiss the dialog and what to allow in the rest of the visible region on the main window to avoid confusing the user. Also note that there are no restrictions on the shape or size of the overlay view as long as the background color is set to [UIColor clearColor]. You can use the same method for irregularly shaped pop-ups.

Go nuts and have fun.

Written by ramin

September 29th, 2009 at 7:50 pm

Posted in iphone

Tagged with ,

Push Notification and Python (Django)

with 4 comments

I’ve been trying to get the iPhone Push notification services introduced in OS 3.0 working with Python (specifically, Django). It took a while but I figured I’d post my notes so it’ll spare someone the suffering I had to go through.

Other attempts at this (apns-python-wrapper (aka APNSWrapper) and Lee Packham’s code) both use the standard ssl libraries which I couldn’t get to work under Snow Leopard and Python 2.6.

I eventually got it going with Django and pyOpenSSL libraries. The easiest way is to get the prebuilt pyOpenSSL binaries from eGenix. If the test server is running on Snow Leopard the Python 2.6 UCS2 version seems to work best.

The thing about pyOpenSSL is that it expects the certificate and private key to be in separate .PEM files so you have to export each one from KeyChain Access in .P12 format then convert them to .PEM from the command line. For the development key it’s easier to specify the password during export then strip it out on the command line. In production you may not want to do that.

   % openssl pkcs12 -clcerts -nokeys -out devcert.pem -in devcert.p12
   % openssl pkcs12 -nocerts -out devkey.pem -in devkeypw.p12
   % openssl rsa -in devkeypw.pem -out devkey.pem

Now copy devcert.pem and devkey.pem (with no password) into the server directory.

The following bit of code is in a file called PushSender.py and does the actual talking to the push server (I heavily edited out project-specific bits and standard exception and error-handling stuff. Hopefully didn’t bork it too badly ;-)

Also, in this example the actual communication runs in a thread. In some cases it might make more sense to run it as a main routine:

import os, sys
import struct, binascii, ssl, datetime
import threading
import simplejson as json
from socket import socket
from OpenSSL import SSL

class PushSender(threading.Thread):
def __init__(self, sandbox, token, message, badge, sound):
	super(PushSender, self).__init__()
    		self.token = token
    		self.sandbox = sandbox
    		self.message = message
    		self.badge = badge
    		self.sound = sound
    		self.ctx = SSL.Context(SSL.SSLv3_METHOD)
    		if sandbox:
    			self.apnHost = "gateway.sandbox.push.apple.com"
    			self.ctx.use_certificate_file(os.path.join(PROJECT_ROOT, "devcert.pem"))
    			self.ctx.use_privatekey_file(os.path.join(PROJECT_ROOT, "devkey.pem"))
    		else:
    			self.apnHost = "gateway.push.apple.com"
    			self.ctx.use_certificate_file(os.path.join(PROJECT_ROOT, "prodcert.pem"))
    			self.ctx.use_privatekey_file(os.path.join(PROJECT_ROOT, "prodcert.pem"))

    	def run(self):
    		payload = {}
    		aps = {}
    		if (self.message):
    			aps["alert"] = str(self.message)
    		if (self.badge):
    			aps["badge"] = self.badge
    		if (self.sound):
    			aps["sound"] = str(self.sound)

    		payload["aps"] = aps

    		token = binascii.unhexlify(self.token)
    		payloadstr = json.dumps(payload, separators=(',',':'))
    		payloadLen = len(payloadstr)
    		fmt = "!cH32sH%ds" % payloadLen
    		command = '\x00'
    		msg = struct.pack(fmt, command, 32, token, payloadLen, payloadstr)
    		sock = socket()
    		s = SSL.Connection(self.ctx, sock)
    		s.connect((self.apnHost, 2195))
    		s.send(msg)
    		s.shutdown()
    		s.close()

[ PushSender source ]

To invoke it in a thread (the first param is True for sandbox and False for production server) from the main routine:

	from PushSender import PushSender
	...
	pushsender = PushSender(True, token, pushmessage, pushbadge, pushsound)
	pushsender.start()

On the phone the token returned by the didRegisterForRemoteNotificationsWithDeviceToken method is in the form <xxxxxxxx xxxxxxxx ...>. To pass it along to the server you have to strip out the <> and spaces. The unhexlify method then converts this string into a 32-byte hex binary value. The easiest way to strip out the extraneous stuff is to just do it on the client-side:

    NSString *deviceToken = [[[[tokenString description]
			stringByReplacingOccurrencesOfString:@"< " withString:@""]
			stringByReplacingOccurrencesOfString:@">" withString:@""]
			stringByReplacingOccurrencesOfString: @" " withString: @""];

One last thing: at least with the sandbox if the phone is running on WiFi sometimes push notices don’t come through. This may be due to router NAT or firewall configuration issues. One way to check is to go into XCode Organizer while the phone is tethered and run your app, then under the Organizer Console you can check for errors. To get around this you’ll want to turn off the WiFi on the phone and go with 3G. This usually makes push notices arrive as expected (and strangely enough the WiFi method goes back to working for a few minutes).

There’s more that needs to be done to make it production-ready but at least it’s good to know it’s doable:

cp18.png

 

Written by ramin

September 9th, 2009 at 4:36 am

Posted in Tech

Tagged with , , ,