Posts Tagged ‘ source code

MGTwitterEngine and Locations

So Twitter just updated their API to disallow the use of authentication through the REST API. They were nice enough to announce that in an email 2 days after they turned it off so that meant that I had to learn all about OAuth overnight and scramble to find something that could take over the Twitter functionality of the LogYourRun iPhone app.

Luckily most of the leg work in this are has been done by Matt Gemmell and his Twitter engine. Some slight modifications are required in order to get it running on the iPhone and working with OAuth.

With these libraries dropped in the new Twitter authentication system is fairly easy to implement. The main problem is that the MGTwitterEngine does not support the Twitter location API. Since this is an important piece of TweetMyDistance I modified the MGTwitterEngine.m and added a function for tweeting with location. The new method takes latitude and longitude as well as the tweet and if this is a reply to a previous tweet. As you can see from my tweet it works great. The code below should replace the code in MGTwitterEngine and don’t forget to also update your .h file.

- (NSString *)sendUpdate:(NSString *)status
{
    return [self sendUpdate:status inReplyTo:0];
}

- (NSString *)sendUpdate:(NSString *)status inReplyTo:(unsigned long)updateID
{

	return [self sendUpdate: status inReplyTo: updateID withLatitude: 0.0 andLongitude: 0.0];

}

- (NSString *)sendUpdate:(NSString *)status
			   inReplyTo:(unsigned long)updateID
			withLatitude:(double) lat
			andLongitude: (double) lng
{
	if (!status) {
        return nil;
    }

    NSString *path = [NSString stringWithFormat:@"statuses/update.%@", API_FORMAT];

    NSString *trimmedText = status;
    if ([trimmedText length] > MAX_MESSAGE_LENGTH) {
        trimmedText = [trimmedText substringToIndex:MAX_MESSAGE_LENGTH];
    }

    NSMutableDictionary *params = [NSMutableDictionary dictionaryWithCapacity:0];
    [params setObject:trimmedText forKey:@"status"];
    if (updateID > 0) {
        [params setObject:[NSString stringWithFormat:@"%u", updateID] forKey:@"in_reply_to_status_id"];
    }
    if (lat != 0.0 && lng != 0.0) {
		// lat=%1.6f&long=%1.6f&display_coordinates=true
        [params setObject:[NSString stringWithFormat:@"%1.6f", lat] forKey:@"lat"];
        [params setObject:[NSString stringWithFormat:@"%1.6f", lng] forKey:@"long"];
        [params setObject:@"true" forKey:@"display_coordinates"];
    }
    NSString *body = [self _queryStringWithBase:nil parameters:params prefixed:NO];

	DLog(@" twitterbody: %@", body);

    return [self _sendRequestWithMethod:HTTP_POST_METHOD path:path
                        queryParameters:params body:body
                            requestType:MGTwitterUpdateSendRequest
                           responseType:MGTwitterStatus];
}
  • Share/Bookmark

Converting App from iPhone to Universal

With the iPad selling like hot cakes it seems like a good idea to get on the bandwagon and make some apps for the iPad.  The LogYourRun app is not particularly well suited for running on the iPad – I doubt that people will be strapping their iPads to their arms and run around the park.  However, the heart rate app seems like something that people would enjoy using even on iPads.  Since I did not want to have to maintain two separate apps I decided to make the app universal.  This was actually quite an easy process and took only about 10-12 hours (could have been faster if I had known what I was doing – which is why I wanted to share what I learned).

First step is to make sure you copy your application folder so you will be able to go back to the original application if you mess up.  Then with your newly copied application upgrade your target to work on the iPad in addition to the iPhone.  Click on your target and go into the menu – under Project you will find an entry that says “upgrade current target for iPad”.  Select the “One Universal application” option.  This will generate a new group which contains your new iPad version of the MainWindow.xib file – MainWindow-iPad.xib.  You can use this new nib to load all your iPad specific views.

At this point I went through all my existing nibs and created iPad versions (can be done in the Interface Builder) and saved them as XxxViewController-iPad.xib in the Resources-iPad folder.  For iPad nibs you have to make sure that you have versions of the nib that look decent and are usable in both landscape and horizontal orientations.  This can be tricky if you have a lot of UI elements but is very important since iPad apps have to support all orientations.  Make sure you anchor your UI elements to the right edges and that they scale to look nice.

The heart rate app uses a tab bar.  It took me the longest time to figure out how to get this to rotate.  The reason for this was that I had not read Apple’s documentation.  Turns out that all the view controllers that the tab bar links to have to return YES for all shouldAutorotateToInterfaceOrientation: orientations.  This makes sense conceptually.  In addition you need to set your view controllers to automatically resize when rotation occurs.  Once these are set you will be able to see what the rotation looks like in the iPad simulator (hopefully great).

Loading the iPad specific nibs using the view controller is a great way to get these to show up on the iPad only – but there are some cases where you will need to load a view controller programatically (in my case when the user clicks on the help button it brings up the help VC).  To make sure that you load the right nib you can check if the app is running on the iPad.  The following code is what I used:

- (BOOL) isIPad {
#if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 30200)
  if ([[UIDevice currentDevice] respondsToSelector: @selector(userInterfaceIdiom)])
    return ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
#endif

  return NO;
}

This code will check if the iDevice responds to the userInterfaceIdiom selector and if it does it checks if the device is an iPad.  If it is I load the iPad version of the helpVC if not then I load the iPhone version of the helpVC.  Make sure you also put the respondToRotation in these VCs since they will need to respond predictably also.

Finally you will need to create a default image that can be presented on the iPhone.  In your info.plist you can set the “Launch image (iPad)” to the base name of the iPad graphic (e.g. Default-iPad.png).  You can then create two images named Default-iPad-Landscape and Default-iPad-Portrait and add them to your project.  The iPad will pick whichever of the default images that fits with the rotation of the device.

When uploading your app – build the app in OS3.2.  The app will run on both iPhone and iPad from one binary. You will also have to upload screenshots for the iPad version for display in the iPad app store and create two new icons (50×50 and 72×72).  Once you have all that together your app is ready for submission and you can start praying that it will be a speedy path to acceptance.

  • Share/Bookmark

Using audio services to play alert sound

Some times it your application may inadvertently quit with out the user meaning to quit your app.  For example people run with the running/GPS app and the iPhone may be in a pocket / arm band / hand where the square button could be pressed unintentionally.  If the app quits while running it will not collect running data for the rest of the run and the user will be very disappointed.  To avoid this I have setup an alert which will make a sound if the user quits the app while the app is collecting running data.  I was able to find the original apple alert aiff files via Google and I chose to implement the Indigo sound as the alert – the cricket is not really loud enough to be an alert sound.

By implementing the playback of the sound as an alert sound the sound will be played even on the 1st gen iPod Touch which does not have a speaker.  Alert sounds can also be set to finish playback after the app finishes – which is an important feature for this use.  I was able to find how to set this property on Nagano’s blog.  Not sure what most of the page says but the language of Objective C is universal. Remember to add the audiotoolbox to your project.


#include <AudioToolbox/AudioToolbox.h>

I then load the sound in the viewDidLoad init function for the view controller where I want to play the sound – this way you are not trying to load the sound as the app is quitting.  The soundFileObject isa global for this VC.

// SOUNDS
 NSString *path = [[NSBundle mainBundle] pathForResource:@"Indigo" ofType:@"aiff"];
 NSURL *fileURL = [NSURL fileURLWithPath:path];

 OSStatus err;
 err = AudioServicesCreateSystemSoundID((CFURLRef)fileURL, &soundFileObject);
 if(err) {
   DLog(@"AudioServicesCreateSystemSoundID err = %d",err);
   exit(1);
 }

 UInt32 flag = 1;
 err = AudioServicesSetProperty(kAudioServicesPropertyCompletePlaybackIfAppDies,
     sizeof(UInt32),
     &soundFileObject,
     sizeof(UInt32),
     &flag);
 if(err){
   DLog(@"AudioServicesSetProperty err = %d",err);
 }

Then under viewWillDisappear I have the following:

 if (runActivity.isStarted) {
   // play alert sound if exiting while running
   AudioServicesPlayAlertSound (self.soundFileObject);
 }

This way the sound will play if the view is unloading while the user is expecting the app to be collecting data.

  • Share/Bookmark

WebKitErrorDomain error 101

The LogYourRun app uses both UIWebView and NSURLRequest to communicate with the LogYourRun database.  To let users upload and access their data.  These requests often contain fields that the user enter such as running notes, distance, time etc.  I had the WebKitErrorDomain 101 error come up on occasions when users tried to send requests that contained spaces that were not properly encoded.  I was not able to find any documentation on this error so it took a while to figure out what was going on.

WebKitErrorDomain error 101

Once I identified that the issue was that some users had a space in their username it is a very easy fix.  Just use the stringByAddingPercentEscapesUsingEncoding function to have the string properly encoded:

[username stringByAddingPercentEscapesUsingEncoding: NSASCIIStringEncoding];

So if you come along a WebKitErrorDomain 101 error – take a look at the URL you are trying to submit and see if all the parts of it are properly encoded.

  • Share/Bookmark

How to test location and accelerometer functions from the comfort of your home

The LogYourRun iPhone app uses both the GPS and the accelerometer functions of the iPhone.  Neither the GPS nor the accelerometer can be accessed on the iPhone simulator so to circumvent this I used the following methods:

Accelerometer: Otto Chrons has a great Google code project which contains the code for an app that can be run on an iPod touch or iPhone which is on the same subnet as your development computer.  The app will transmit accelerometer data which can be caught by some code that you can insert into your Xcode project.  When you run your app on the iPhone simulator it will thus receive the accelerometer data from the real life device.  This was a great help in troubleshooting the setup of the pedometer functionality.  I was running around in my living room counting steps and comparing to the # of steps on the iPhone simulator.  Be sure to remove the accelerometer simulator code from your application before releasing.

GPS: Everyone who has played with the location manager on the iPhone Simulator knows that you will only get only one location from the location manager on the iPhone Simulator.  Testing the GPS functionality is a critical part of testing the LogYourRun app.  I could run the app on an iPhone and then run/walk/drive around outside – but that would take much too long and the weather in Saint Louis is not conductive to such testing.  Instead I added a button to the pedometer screen of the application which when clicked runs the following code:

- (IBAction) locationButton:(id)sender {
   CLLocation *thisLocation;
   double latitude  = 37.33168900 + (float) ((random() % 100) +1) / 1000000.0;
   double longitude = -122.03073100 + (float) ((random() % 100) +1) / 1000000.0;
   thisLocation = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude];
   [self locationManager:locationManager didUpdateToLocation:thisLocation fromLocation:thisLocation];
   [thisLocation release];
}

This piece of code creates a random location close to the location that the iPhone Simulator will usually get from the core location (One Infinite Loop). The location is sent to the location manager delegate function of the pedometer class and is handled as if it came from the real location manager. Since the location is different from the previous location the getDistanceFrom: still works.  When I am ready to submit the application all I have to do is hide the two buttons (probably should be done programatically – since one of these days I will forget).

Pedometer showing location and step button

The pedometer screen showing location and step button

Locations shown on map

The random locations shown on map

  • Share/Bookmark