Friday, April 16, 2010
My love/hate relationship with the iPhone SDK
My app iTravel goes out and gets Wikitravel pages, and then gets subpages in the background. (eg if you ask for "New York City", it'll also go out and get "Manhattan", "Upper West Side", etc.) It also plots listings (sights, restaurants, etc.) for all these pages on a map. I wanted the background thread that was loading this data to automatically a new page's listings to the map, if the viewer was using one; so they can go to "New York City", go to the map, and then watch Manhattan slowly get barnacled by map annotations, one neighbourhood at a time.
I thought this was going to be difficult. I couldn't have been more wrong. Here's the background-thread code, in its entirety:
-(void) refreshMapIfActive {
UIApplication *app = [UIApplication sharedApplication];
iTravelRightAppDelegate *appDelegate = app.delegate;
UINavigationController *controller = [appDelegate navigationController];
NSArray *viewControllers = [controller viewControllers];
UIViewController *currentController = [viewControllers lastObject];
if ([currentController class] == [MapViewController class])
[currentController performSelectorOnMainThread:@selector(showAnnotations:) withObject:nil waitUntilDone:NO];
}
I'd like to do this in Android, too...but a) their map implementation works a lot slower, b) I don't think there even is a method to get the currently active Activity.
Now, the "showAnnotations:" method within MapViewController is obviously trickier. For one thing, it's synchronized, lest the user try to filter a map just when a background thread is adding listings - not really a UI issue, since this usually takes all of 1-2 seconds:
-(void)doShowAnnotations:(NSArray*)annotationsToShow {
//autorelease pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
@synchronized (self) {
@try {
NSArray *wikiPageMarkers = [WikiPage getPageMarkersWithin:mapView.region];
BOOL added=NO;
double pageLevel = [Settings getMapPageLevel];
BOOL abovePageLevel = mapView.region.span.longitudeDelta > pageLevel || mapView.region.span.latitudeDelta > pageLevel;
if (abovePageLevel && [mapView.annotations count] > [wikiPageMarkers count]) { //clear away low-level annotations
[mapView performSelectorOnMainThread:@selector(removeAnnotations:) withObject:[annotations allObjects] waitUntilDone:YES];
[annotations removeAllObjects];
}
else if (!abovePageLevel)
{
UITabBarItem *selected = [self.tabBar selectedItem];
if (selected!=nil) {
//first, remove all annotations that don't fit the selection
NSMutableArray *toRemove = [NSMutableArray arrayWithCapacity:[mapView.annotations count]];
for (NSObject *annotation in mapView.annotations) {
if ([annotation class] == [Listing class]) {
Listing *listing = (Listing*) annotation;
if (!(selected.tag==MY && [listing isInMyListings] || selected.tag==[listing.category intValue]))
[toRemove addObject:listing];
}
}
[mapView performSelectorOnMainThread:@selector(removeAnnotations:) withObject:toRemove waitUntilDone:YES];
}
if (annotationsToShow==nil) { //if we don't have a specific request, get all the listings within the map's region
NSNumber *category = selected==nil ? nil : [NSNumber numberWithInt:selected.tag];
annotationsToShow = [ListingManager getListingsWithin:mapView.region forCategory:category];
}
//the following is incredibly messy because NSSet and NSMutableSet are unusable for our purposes.
What follows is one of the reasons I hate the SDK. Basically, I want to do some fairly basic set arithmetic to ensure that we remove undesired annotations from the map (but keep them in our local "annotations" set in case we need to add them again) and add new ones that are desired. Because the "-unionSet:" etc. methods on NSMutableSet don't work at all like you'd expect, though, I basically have to do that by hand. I'll skip over that messy part to the good stuff:
NSMutableArray *thereNotRequested = [NSMutableArray arrayWithArray:mapView.annotations];
for (annotation in mapView.annotations) {
if ([requested objectForKey:[annotation title]] != nil)
[thereNotRequested removeObject:annotation];
}
[mapView performSelectorOnMainThread:@selector(removeAnnotations:) withObject:thereNotRequested waitUntilDone:YES];
if ([arrayToAdd count]>0) {
[mapView performSelectorOnMainThread:@selector(addAnnotations:) withObject:arrayToAdd waitUntilDone:YES];
added=YES;
}
if (!added)
[self performSelectorOnMainThread:@selector(activityDone) withObject:nil waitUntilDone:NO];
}
@catch(NSException *exception) {
[Util doLog:[NSString stringWithFormat:@"Warning: adding annotations to map view failed: %@", [exception reason]]];
[self performSelectorOnMainThread:@selector(activityDone) withObject:nil waitUntilDone:NO];
}
@finally {
[pool release];
}
}
Labels: currentController, iPhone, maps, MapView, NSMutableSet, Objective-C, performSelector:, selector, selectors, sets, synchronization, threading, UINavigationController
Sunday, February 7, 2010
Android vs. iPhone: A Developer's Perspective, part I
(See also Part II.)
I've spent the last month or so building both Android and iPhone versions of my app iTravel. (See http://wetravelright.com/ for details and links.) Which gives me a pretty good perspective from which to compare and contrast the Android and iPhone environments and SDKs. Hence I give you the following head-to-head analysis, from a developer's point of view:
Language
Non-programmers often think that one's language of choice is a big deal, but really, once you've learned two or three programming languages, picking up another is generally something you can do in a day or two. That said, there are often substantive differences. And this is definitely true of Java (for Android) and Objective-C (for iPhone.)
Let's start off with the really annoying stuff: Objective-C's memory management. By which I mean, its complete lack of any. Programmers have to manually allocate and release memory when writing for the iPhone SDK, which is positively medieval. If you fail to do so, and there are many pitfalls, then you leak memory which is lost until the device reboots. This is awful. (And it's no longer true of the Mac SDK, incidentally; but the iPhone is behind the times.)
There are other annoyances. You have two files to contend with for every class - a .h and a .c file. Which is inconvenient and complicating and inelegant. And suppose you have a basic, bog-standard instance variable. You generally wind up declaring it in, count 'em, not one, not two, not three, but four different places.
In your .h:
{
NSObject *object
}
@property (nonatomic, retain) NSObject *object
In your .c;
@synthesize object
and later, in dealloc(),
[object release];
Whereas with Java, you have one single .java file, which in general will have
private Object object;
public Object getObject() { return object; }
private void setObject(Object o) { object=o; }
all in one place. I know which one I think is easier.
But. On the other hand. Objective-C is sort of the bastard son of C, which is awful, and Smalltalk, which is awesome. As a result, it has Smalltalk-like features like selectors:
[caller performSelector:@selector(myFunctionName)]
...in short, you can use functions as (more or less, ish) first-class objects. Try that in Java and if memory serves you'll most likely wind up in the irritating labyrinth of reflection. Plus, you get options like "doesNotRecognizeSelector:", which can be easily misused, but is potentially very powerful, and does not exist in Java.
On the other hand, Objective C is really annoyingly logorrheic (meaning wordy) especially when it comes to string handlers. Suppose you have strings A and B, and you wish to combine them into string C. In Java, the syntax is
C=A+B;
whereas in Objective-C, you type
C = [A stringByAppendingString: B]
Or suppose you want the location of the last slash in string A. Java:
n=A.lastIndexOf("/");
Objective-C:
n = [A range ofString:@"/" options:NSBackwardSearch].location;
I much prefer Java's syntax and simplicity. But I do admire Objective-C's flexibility.
Development environment
By this I mean: the "integrated development environment" in which coders work; the documentation for the IDE, the language, and the libraries; the testing and source-code support; and all the stuff that is meant to help you write better code faster.
The Android and iPhone IDEs and documentation really incarnate the attitudes of the two companies in question. Android doesn't exactly have an IDE of its own, although they recommend that you use the Android plug-in for the open-source Eclipse IDE, which is slow, irritating, and buggy in various minor ways. The documentation is written by smart people for smart people, with little handholding. Flashy graphics are minimal to nonexistent. And a lot of important stuff is still handled by tools meant to be run from a shell rather than a GUI. But the search function is excellent.
Apple's IDE is slick, seamless and powerful. It comes with a visual tool to help you lay out the screens of your app. The documentation is full of step-by-step guides (although they are often oddly lacking or confusing) and high-quality graphics and other visualizations.
I have many complaints about both. Eclipse is slow and annoying, and I could only get Android's JUnit test harness to run successfully from a terminal window, rather than the IDE; similarly, I had to use shell tools to sign packages, get a fingerprint for a Maps IDE, install a package on my phone, etc. All of which really calls into question the I in IDE.
On the other hand, at last the external unit-testing actually works; XCode's built-in harness is so bad that Google built and released an entirely separate one, which has the advantage of actually functioning correctly. On the other hand, its Subversion integration is excellent, which is not true of Eclipse.
Both have plenty of official and unofficial online support, as well, at official support sites and places like Stack Overflow. Android's open-source ethos gives it a big advantage in terms of external packages, though; for instance, if you want to build a barcode scanner into an Android app, there's a whole open-source library out there for you, ready to be plugged in. For the iPhone? You'll have to roll your own. Sorry.
Multithreading
In phone apps multithreading is key, because the phone needs to remain as responsive as possible, so you need to do your heavy lifting behind the scenes, outside the main UI thread.
Both the iPhone SDK and Android support multithreading, but the latter's is much more convenient, especially if you want to call back to the main thread once your background thread has done its thing. On the iPhone, you have to do something like:
-(void) doWebView {
NSThread* htmlThread = [[NSThread alloc] initWithTarget:self selector:@selector(loadHtml) object:nil];
[htmlThread start];
[htmlThread release];
}
-(void) loadHtml {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSString *html = doUnroll ? [self getUnrolledData] : root.data;
html=[NSString stringWithFormat: @"%@%@</body></html>", [Util getBaseHtml], html];
[self performSelectorOnMainThread:@selector(setHtml:) withObject:html waitUntilDone:NO];
[pool release];
}
-(void) setHtml:(NSString*)html {
[webView loadHTMLString:html baseURL:nil];
[webView sizeToFit];
[self.tableView setTableHeaderView:webView];
}
Whereas on Android, you can use Java's equally irritating Runnable() framework, but Android provides an extremely convenient (and quite flexible) short form. Just subclass AsyncTask in an inner class:
new LoadHtmlTask().execute();
class LoadHtmlTask extends AsyncTask{
protected String doInBackground(String... strings) {
String headerData = Settings.GetHtmlPrefix() + (unrolled ? getUnrolledData(viewRoot) : viewRoot.getData());
return headerData;
}
protected void onPostExecute(String results) {
mHeaderView.loadDataWithBaseURL("local", headerData, "text/html", "utf-8", "");
}
}
which seems much more encapsulated and intuitive to me.
Persistence
On paper, the iPhone environment has a big advantage here: it features the Core Data object-persistence layer above the SQLite database, whereas Android requires direct DB access.
For me, though, direct DB access was not awful, and given the constraints of my app, arguably simpler than jumping through all of Core Data's hoops - using the separate tool to declare a data model, having to rebuild your custom code every time you add a column, etc. All that without even getting thread safety.
However, I'm totally comfortable writing SQL, not all developers are, and my app had pretty straightforward DB requirements. Core Data is undeniably more elegant and ultimately better. Plus you get nifty little features like shake-to-undo, semi-automatic migration data models in installed apps, etc. And it's not like the Android tools are particularly easy to work with. Check out the method in android.database.sqlite.SQLiteDatabase you use to perform a query:
public Cursor query (boolean distinct, String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit)
As a (Google employee) friend of mine said, "Holy positional parameters, Batman!" Needless to say, one quickly wraps this monstrosity in other methods less prone to grievous error...
Part II of this post compares and contrasts system features, phone features, settings and resources, screen building, internet connectivity, and the app install/release process. (It does not compare and contrast graphics programming, as my apps are data-heavy not graphics-heavy.) Don't touch that dial.
Labels: Android, compare, comparison, contrast, developer, environment, IDE, iPhone, Java, language, multithreading, Objective-C, persistence, theads
Monday, January 18, 2010
Using Core Data in a multithreaded environment.
Managed objects are not thread safe [...] Core Data does not present a situation where reads are "safe" but changes are "dangerous"—every operation is "dangerous" because every operation can trigger faulting.
Sound intimidating? No worries: it's a piece of cake. Just wrap the code where you get your ManagedObjectContext in a method something like this:
@implementation Util
+(NSManagedObjectContext*)getManagedObjectContext {
UIApplication *app = [UIApplication sharedApplication];
MyAppDelegate *appDelegate = app.delegate;
if ([NSThread isMainThread])
return [appDelegate managedObjectContext];
else
return [appDelegate getMOCFor:[NSThread currentThread]];
}
and add the following method to your application delegate:
(where "threadMOCs", obviously, is a properly defined and synthesized ivar.)
- (NSManagedObjectContext*) getMOCFor:(NSThread*) thread {
if (!threadMOCs)
self.threadMOCs=[NSMutableDictionary dictionaryWithCapacity:16];
NSNumber *threadHash = [NSNumber numberWithInt:[thread hash]];
if ([threadMOCs objectForKey:threadHash]==nil) {
NSManagedObjectContext *newMOC = [[NSManagedObjectContext alloc] init];
NSError *error=nil;
NSURL *storeUrl = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"myDBName.sqlite"]];
NSManagedObjectModel *model = [self managedObjectModel];
NSPersistentStoreCoordinator* threadPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: model];
if (![threadPSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:nil error:&error])
{ //handle error here }
[newMOC setPersistentStoreCoordinator: threadPSC];
[threadMOCs setObject:newMOC forKey:threadHash];
}
return [threadMOCs objectForKey:threadHash];
}
Et voila! A new object context and persistent store coordinare for every thread, which gives you full concurrent access to your data. (Though you probably still have to be careful about passing managed objects across threads...)
Labels: concurrency, concurrent, CoreData, iPhone, multithread, NSManagedObjectContext, NSPersistentStoreCoordinator, NSThread, Objective-C, SDK, threading, UIApplicationDelegate
Friday, July 17, 2009
HttpHelper
It's been a week of tweaking, fixing little bugs, spiralling in towards hoped-for perfection. I'm pleased to report that I now have an alpha-test version of my iPhone app up and running on my iPod Touch, and it looks to work a charm. If all goes very well indeed I might submit it to the App Store - where it will ultimately be available for the low low price of $0.00 - by the end of the month.
In the meantime, here's a utility class that people might find handy: my HttpHelper singleton. It's not really a class variable, since it seems Objective-C doesn't support them (Oh, Smalltalk, how I miss your class instance variables) but "static" has the same effect, if you code carefully. Like so:
//
// HttpHelper.m
// iTravelWrite
//
// Singleton class used to send HTTP requests and forward responses to selectors passed in by the caller.
// Created by Jon Evans on 04/07/09.
//
#import "HttpHelper.h"
#import "Util.h"
@implementation HttpHelper
static HttpHelper *singleton=nil;
#pragma mark -
#pragma mark Singleton methods
+(HttpHelper *) getInstance {
if (singleton==nil)
singleton = [[HttpHelper alloc] init];
return singleton;
}
+(id)allocWithZone:(NSZone *)zone {
if (singleton == nil) {
singleton = [super allocWithZone:zone];
return singleton;
}
return nil;
}
-(id)copyWithZone:(NSZone *)zone {
return self;
}
-(id)retain {
return self;
}
-(unsigned)retainCount {
return UINT_MAX;
}
-(void)release {
//pass
}
-(id)autorelease {
return self;
}
- (void)dealloc {
[super dealloc];
}
All the above is housekeeping stuff to ensure that we only ever have one instance of an HttpHelper. Not that a duplicate would be so disastrous in this case, but hey, if you're writing a singleton, write a singleton, right?
Here's the part where it actually does stuff. In particular, it does all the HTTP GETs and HTTP POSTs that your iPhone app will ever need:
#pragma mark -
#pragma mark Business logic
+(NSURLRequest*) buildRequestWithPostKeys:(NSArray *) postKeys postValues:(NSArray *) postValues urlString:(NSString *)urlString {
NSMutableString *params=[[NSMutableString alloc] initWithCapacity:1024];
for (int i=0; i<[postValues count]; i++) {
[params appendString:[postKeys objectAtIndex:i]];
[params appendString:@"="];
[params appendString:[postValues objectAtIndex:i]];
[params appendString:@"&"];
}
NSData * paramData = [params dataUsingEncoding:NSUTF8StringEncoding];
NSURL * url = [NSURL URLWithString:urlString];
NSURLRequestCachePolicy policy = NSURLRequestReloadIgnoringCacheData; // never cache a post response, at least in my app
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:policy timeoutInterval:10.0];
NSString *msgLength = [NSString stringWithFormat:@"%d", [paramData length]];
[request addValue: msgLength forHTTPHeaderField:@"Content-Length"];
[request setHTTPMethod:@"POST"];
[request setHTTPBody: paramData];
return request;
}
+(BOOL) doPost:(NSURLRequest *)request forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {
NSArray *keys = [NSArray arrayWithObjects:@"request", @"caller", @"onSuccess", @"onFailure", nil];
NSArray *values = [NSArray arrayWithObjects:request, caller,
[NSValue valueWithBytes:&onSuccess objCType:@encode(SEL)],
[NSValue valueWithBytes:&onFailure objCType:@encode(SEL)],
nil];
NSDictionary *args = [NSDictionary dictionaryWithObjects:values forKeys: keys];
NSThread* uploadThread = [[NSThread alloc] initWithTarget:[self getInstance] selector:@selector(doHttp:) object:args];
[uploadThread start];
[uploadThread release];
return TRUE;
}
+(NSURLRequestCachePolicy)getCachePolicyFor:(NSString *)urlString {
/*
Hived out to a separate method because we might fine-tune this later.
In theory, this will cause the app to use the cache policies set by
wetravelwrite.appspot.com, which as of this writing means 1 hour
for listing information, and 10 hours for searches.
In the ListingEditController and ListingsViewController we manually wipe
the cache for listings edited by the app.
*/
// return NSURLRequestUseProtocolCachePolicy;
return NSURLRequestReloadIgnoringCacheData;
}
+(NSURLRequest *)getURLRequestFor:(NSString *)urlString {
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequestCachePolicy policy = [self getCachePolicyFor:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:policy timeoutInterval:7.5];
return request;
}
+(BOOL) doGet:(NSString *)urlString forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {
NSURLRequest *request = [self getURLRequestFor:urlString];
NSArray *keys = [NSArray arrayWithObjects:@"request", @"caller", @"onSuccess", @"onFailure", nil];
NSArray *values = [NSArray arrayWithObjects:request, caller,
[NSValue valueWithBytes:&onSuccess objCType:@encode(SEL)],
[NSValue valueWithBytes:&onFailure objCType:@encode(SEL)],
nil];
NSDictionary *args = [NSDictionary dictionaryWithObjects:values forKeys: keys];
NSThread* uploadThread = [[NSThread alloc] initWithTarget:[self getInstance] selector:@selector(doHttp:) object:args];
[uploadThread start];
[uploadThread release];
return TRUE;
}
-(BOOL) doHttp:(NSDictionary *)args
{
@synchronized (self) {
//autorelease pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSURLRequest *request = [args objectForKey:@"request"];
NSObject *caller = [args objectForKey:@"caller"];
SEL onSuccess;
[[args objectForKey:@"onSuccess"] getValue:&onSuccess];
SEL onFailure;
[[args objectForKey:@"onFailure"] getValue:&onFailure];
NSError *error;
NSData *returnData = [NSURLConnection sendSynchronousRequest: request returningResponse: nil error: &error ];
NSString * responseString = [[NSString alloc] initWithData: returnData encoding: NSASCIIStringEncoding];
if (!error) //connection succeeded; but did it work?
error = [Util getErrorFrom:responseString forRequest:request];
if (error) {
NSLog(@"iTravelWrite Error ",error);
[caller performSelectorOnMainThread: onFailure withObject:error waitUntilDone:NO];
}
else
[caller performSelectorOnMainThread: onSuccess withObject:responseString waitUntilDone:NO];
[pool release];
}
return TRUE;
}
@end
Pretty slick, huh?
The three chief interface methods are, for a GET -
+(BOOL) doGet:(NSString *)urlString forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure
and for a POST -
+(NSURLRequest*) buildRequestWithPostKeys:(NSArray *) postKeys postValues:(NSArray *) postValues urlString:(NSString *)urlString
+(BOOL) doPost:(NSURLRequest *)request forCaller:(id)caller onSuccess:(SEL)onSuccess onFailure:(SEL)onFailure {
I'll give you an example of the former first, as it's easier. The call itself is perfectly straightforward:
NSMutableString *urlString = [NSMutableString stringWithCapacity:128];
[urlString appendString:[Util getSearchPageURL]];
[urlString appendString:@"?locale="];
[urlString appendString:[UserSettings getLanguage]];
[urlString appendString:@"&searchTerms="];
[urlString appendString:[searchBar.text stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
[HttpHelper doGet:urlString forCaller:self onSuccess:@selector(parseSearchResults:) onFailure:@selector(searchFailed:)];
Not, however, the two "@selector" arguments. These must be methods on the caller, and they better expect an NSString* and an NSError*, respectively, as arguments. (Or they can take an NSObject* and cast from there, I suppose, but why bother, right?)
Inside HttpHelper we have to wrap the selectors in NSValues to pass them from method to method, which is a bit annoying, but hey, we only have to do it once.
To call a POST, by comparison:
NSArray *postKeys = [NSArray arrayWithObjects:@"title", @"location", @"comments", @"pageUri", @"sectionName", @"sectionNumber", @"listingName", nil];
NSArray *postValues = [NSArray arrayWithObjects:note.title, [note locationString], note.body, note.pageUri, note.sectionName, note.sectionNumber, note.listingName, nil];
NSURLRequest *request = [HttpHelper buildRequestWithPostKeys:postKeys postValues:postValues urlString:[Util getUploadNoteURL]];
[HttpHelper doPost:request forCaller:self onSuccess:@selector(onUploadSuccess) onFailure:@selector(onUploadError:)];
Note that "onUploadSuccess" here doesn't take an argument - I don't care about the web site's response, the fact of success is all that matters. OK, dubious wisdom that, but the example is relevant to show that the selector methods don't actually have to accept arguments.
We launch a new thread every time we call HttpHelper, so we have to be careful lest we run into concurrency problems. So we synchronize the actual HTTP calls in the "doHttp:" method, which is the one place in the app where we actually go out and connect to the big bad scary Internet.
Note also that if we wanted to change from using "sendSynchronousRequest:" to the delegated version of NSURLConnection, the only class that would change is HttpHelper. Ah, encapsulation.
Anyway. Share and enjoy, as the man said. Hope all that's useful to someone out there...
Labels: GET, HTTP, HttpHelper, iPhone, NSRequest, NSThread, NSURL, NSURLConnection, Objective-C, POST, SDK, selector, singleton
Monday, July 6, 2009
Step right up to view the fabulous ListingViewController!
I have had a highly productive day with the iPhone SDK and now I am going to inflict it on you too. Specifically, I am going to show you the complete annotated code for the ListingViewController, the view controller which, despite its less than thrilling name, is kind of the heart of my iPhone application.
Ready? Buckle your seat belts. It's gonna be a bumpy post.
Here goes nothing: here's the header file -
//
// ListingsViewController.h
// iTravelWrite
//
// Created by Jon Evans on 02/07/09.
//
#import
#import
#import "PageSearchResult.h"
#import "ViewEntry.h"
@interface ListingsViewController : UITableViewController{
NSManagedObjectContext *managedObjectContext;
CLLocationManager *locationManager;
PageSearchResult *page;
ViewEntry *rootEntry;
UIBarButtonItem *addNoteButton;
UIBarButtonItem *editListingButton;
UIBarButtonItem *iAmThereButton;
BOOL createdRoot;
}
-(void)addNote;
-(void)editListing;
-(void)iAmThere;
@property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
@property (nonatomic, retain) CLLocationManager *locationManager;
@property (nonatomic, retain) PageSearchResult *page;
@property (nonatomic, retain) ViewEntry *rootEntry;
@property (nonatomic, retain) UIBarButtonItem *addNoteButton;
@property (nonatomic, retain) UIBarButtonItem *editListingButton;
@property (nonatomic, retain) UIBarButtonItem *iAmThereButton;
@end
Lots of stuff there, most of it self-explanatory. A few bits you won't recognize:
"PageSearchResult" comes from the previous view in the hierarchy, in which the user searches WikiTravel (or selects from previously used pages), and contains the name and URL of the page that the user wishes to edit.
"ViewEntry" is a horrifically generic name and I should probably rename it to "WikiTravelEntry" or something like it. See, WikiTravel pages have intrinsic hierarchies. The "Toronto" page, for instance, has sections like "Districts" and "Eat", which in turn have subsections like "Islands" and "Farmer's Markets". Meanwhile, individual listings - like "Royal Ontario Museum" and "Yumi Japanese Restaurant" - can be attached to any section at all.
Too complicated? Let me give you an analogy. You've got your iPod, right? Well, under the "Music" heading, you can have a list of Artists; each Artist can have a list of Albums; and individual Songs can appear under Music, Artist, or Album.
Think of a WikiTravel listing as a Song, and a WikiTravel section as a folder. Well, a ViewEntry can represent either a Section or an individual Listing; and if it's a Section, then it can have children, and they too can be Sections or Listings.
Huh. So WikiTravel is basically laid out in the same way as an iPod's music. Hey, does that mean the iPhone SDK, with its seamless hierarchy-navigation UI tools, is totally perfect for a WikiTravel iPhone app?
Dude. Does it ever. Check it out:
//
// ListingsViewController.m
// iTravelWrite
//
// Created by Jon Evans on 02/07/09.
//
#import "ListingsViewController.h"
#import "NoteEditController.h"
#import "ListingEditController.h"
#import "HttpHelper.h"
#import "Util.h"
#import "Note.h"
@implementation ListingsViewController
@synthesize managedObjectContext, locationManager, page, rootEntry, addNoteButton, editListingButton, iAmThereButton;
- (void)viewDidLoad {
[super viewDidLoad];
//nav bar
//TODO: localize this string, and those below
self.navigationItem.title=@"Loading...";
//location manager
[[self locationManager] startUpdatingLocation];
//toolbar
self.iAmThereButton = [[UIBarButtonItem alloc] initWithTitle:@"I'm There!" style:UIBarButtonItemStyleBordered
target:self action:@selector(iAmThere)];
iAmThereButton.enabled=NO;
self.addNoteButton = [[UIBarButtonItem alloc] initWithTitle:@"Note To Self" style:UIBarButtonItemStyleBordered
target:self action:@selector(addNote)];
addNoteButton.enabled=NO;
self.editListingButton = [[UIBarButtonItem alloc] initWithTitle:@"Edit Listing" style:UIBarButtonItemStyleBordered
target:self action:@selector(editListing)];
editListingButton.enabled=NO;
self.toolbarItems = [NSArray arrayWithObjects:iAmThereButton, addNoteButton, editListingButton, nil];
[self.navigationController setToolbarHidden:NO];
//activity button
UIActivityIndicatorView *loading = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
loading.frame=CGRectMake(0.0, 0.0, 25.0, 25.0);
[loading sizeToFit];
loading.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
UIBarButtonItem *activityButton = [[UIBarButtonItem alloc] initWithCustomView:loading];
self.navigationItem.rightBarButtonItem = activityButton;
[loading release];
[activityButton release];
}
This is pretty basic stuff: assign text and buttons to the navigation bar and toolbar you get for free by subclassing UITableViewController.
Perhaps most interesting is the bit at the end, where I assign a UIActivityIndicatorView - that little swirly "something is happening" animated wheel - to a custom UIBarButton. Such a beast is invisible until you tell it "startAnimating". Which we do next, which is when things get interesting -
//Fill the table
- (void)viewDidAppear:(BOOL)animated
{
UIActivityIndicatorView *loading = (UIActivityIndicatorView*) self.navigationItem.rightBarButtonItem.customView;
if (!rootEntry) { //only load for the top of the hierarchy
[loading startAnimating];
NSThread* loadThread = [[NSThread alloc] initWithTarget:self selector:@selector(loadViewEntries) object:nil];
[loadThread start];
[loadThread release];
createdRoot = YES;
}
else
{
createdRoot = NO;
self.navigationItem.title=rootEntry.sectionName;
[self.tableView reloadData];
}
}
So. First, you may be wondering, "why is this in viewDidAppear rather than viewDidLoad?" The main reason is that it can take a while to load the data, and I don't want the app to freeze up completely for a few seconds.
Now, if "rootEntry" is nil, that means we have to go out to the Internet and get our data. But wait! If we were to go load this view's contents from the Internet in the same thread, the UI would block. If we want to feign responsiveness, and reassure the user that something is indeed happening with the UIActivityIndicatorView swirly, then we have to launch a new background thread. Which we do.
The whole "createdRoot" thing is really kind of ugly, but necessary to avoid deallocation disaster. If it's any kind of consolation, it's only ever used in dealloc.
So. Our new thread goes and calls the "loadViewEntries" selector. Note that we didn't even define "loadViewEntries" in the header file, but selectors work at run-time, so it doesn't matter. It's almost like the Smalltalk "perform:" notation. Which is kinda awesome. Oh, Objective-C, I love you, when I don't hate you.
So our thread reaches out and calls:
-(void)loadViewEntries
{
//autorelease pool
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//load listings
NSString *urlRoot = [[Util getListingsURL] stringByAppendingString:@"?page="];
NSString *urlString = [urlRoot stringByAppendingString:[page.urlSuffix stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:10.0];
NSData *returnData = [NSURLConnection sendSynchronousRequest: request returningResponse: nil error: nil ];
NSString * responseString = [[NSString alloc] initWithData: returnData encoding: NSASCIIStringEncoding];
// [Util doAlert:@"Response" withMessage:responseString];
//parse listings
self.rootEntry = [[ViewEntry alloc] initWithValueString:responseString levelsFromTop:0];
rootEntry.sectionName=page.pageName;
[self performSelectorOnMainThread: @selector(loadFinished) withObject:nil waitUntilDone:NO];
[pool release];
}
Two key things to note here. One is that we create an autorelease pool at the beginning of this method, and release it at the end. You have to do this whenever you create a new thread, or you face major memory leakage. What a hassle, and what a disaster if you forget. Oh, Objective-C, I hate you when I don't love you.
Next - and this is important - you cannot call UIKit, which in this context basically means anything to do with any view, from inside your new spawned thread. If you do, your app will die with "Tried to obtain the web lock with a thread other than the main thread or the web thread. This may be a result of calling to UIKit from a secondary thread." Oh, Objective-C, I hate you etc.
(Also: web thread? Where can I get me a piece of that?)
At least they give you a fairly easy way around it: the "performSelectorOnMainThread: withObject: waitUntilDone:" method on NSObject. Which works just fine, but means you have to create yet another method to do whatever you want done when the thread completes:
-(void)loadFinished
{
UIActivityIndicatorView *loading = (UIActivityIndicatorView*) self.navigationItem.rightBarButtonItem.customView;
self.navigationItem.title=rootEntry.sectionName;
[loading stopAnimating];
[self.tableView reloadData];
}
OK. So we've populated our hierarchy of data (which happens offscreen in ViewEntry's "init" method.) Now what are we going to do with it?
#pragma mark -
#pragma mark TableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[rootEntry getChildren] count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"ViewEntryCell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
// Set up the cell...
ViewEntry *entry = [[rootEntry getChildren] objectAtIndex:indexPath.row];
cell.textLabel.text = [entry getName];
if ([entry hasChildren])
cell.accessoryType=UITableViewCellAccessoryDisclosureIndicator;
else
cell.accessoryType=UITableViewCellAccessoryNone;
return cell;
}
And that's all you need to do to show the data to the user. Pretty slick, eh? Did I mention that this entire view is created programmatically, with no nib file? Did you notice I never actually created or laid out a view? If your app fits into a hierarchy of TableViews, and fortunately mine fits perfectly, the iPhone SDK is really a dream to work with.
So we show the user the current section's children, which can be subsections or listings (think albums or songs). If the former, we show a little chevron which means you can drill down the hierarchy further. And when the user selects...
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
ViewEntry *selectedEntry = [[rootEntry getChildren] objectAtIndex:indexPath.row];
if ([selectedEntry hasChildren]) {
//push this subsection onto the stack
ListingsViewController *lvController = [[ListingsViewController alloc] initWithNibName:nil bundle:nil];
lvController.managedObjectContext = self.managedObjectContext;
lvController.locationManager = self.locationManager;
lvController.page = self.page;
lvController.rootEntry = selectedEntry;
[self.navigationController pushViewController:lvController animated:YES];
[lvController release];
}
else {
addNoteButton.enabled=YES;
editListingButton.enabled=YES;
if ([locationManager location])
iAmThereButton.enabled=YES;
}
}
...we either create a new instance of this very same controller, pass the relevant data to it, and push it onto the view stack; or, if it's a listing (eg "Royal Ontario Museum"), we enable the three do-something-with-a-listing buttons in the toolbar.
Yes, that's right, that's all the code we need to navigate an arbitrarily sized view hierarchy by creating a tree of ListingViewControllers on the fly and stacking them atop one another. The SDK handles navigation and so forth for us. Cool, eh?
What do those buttons do? Well...
#pragma mark -
#pragma mark Business logic
-(void) addNote {
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
ViewEntry *listing = [[rootEntry getChildren] objectAtIndex:indexPath.row];
Note *newNote = (Note *)[NSEntityDescription insertNewObjectForEntityForName:@"Note" inManagedObjectContext:managedObjectContext];
// Configure the new note
[newNote setIsUploaded:[NSNumber numberWithBool:FALSE]];
[newNote setCreationDate:[NSDate date]];
[newNote setUserEmail:[Util getEmail]];
[newNote setSectionName:[listing sectionName]];
[newNote setSectionNumber:[listing sectionNumber]];
[newNote setListingName:[listing listingName]];
[newNote setPageUri:[page urlSuffix]];
CLLocation *location = [locationManager location];
if (location) {
CLLocationCoordinate2D coordinate = [location coordinate];
[newNote setLatitude:[NSNumber numberWithDouble:coordinate.latitude]];
[newNote setLongitude:[NSNumber numberWithDouble:coordinate.longitude]];
}
NoteEditController *noteEditController = [NoteEditController alloc];
noteEditController.note = newNote;
[self.navigationController pushViewController:noteEditController animated:YES];
[noteEditController release];
}
Here you get to see a) part of the Core Data persistence layer in action, b) the LocationManager where-am-I code in action, and c) the creation of a new kind of detail-view controller.
a) We get the ManagedObjectContext from our parent ViewController, who in turn got it from the RootViewController, who in turn got it from the AppDelegate. There are times - such as when you want to save one object, but not another - when you might want more than one ManagedObjectContext, but I haven't bumped into them yet. I think I might be able to get it from the AppDelegate which serves as something of a global, but for now I'm passing them from ViewController to ViewController.
And yeah, "Note" is a horribly generic name. I oughta rename it to "TravelNote" or something. The idea is that, rather than deal with the detailed listing-edit screen, you can jot a few words where you are, and upload that Note to wetravelwrite.com. Then, later,you'll get an email reminder (if you've opted for them) complete with a link, so that when you're at a computer with a real keyboard and screen, you can go online and edit that lasting as per your note and location.
b) We construct the LocationManager in a method further below.
c) OK, so it's not that exciting.
-(void)editListing{
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
ViewEntry *listing = [[rootEntry getChildren] objectAtIndex:indexPath.row];
ListingEditController *listingEditController = [ListingEditController alloc];
listingEditController.listingData = [listing listingData];
NSArray *keys = [NSArray arrayWithObjects:@"oldName", @"sectionToEdit", @"sectionNumber", @"editPageUrl", nil];
NSArray *values = [NSArray arrayWithObjects:[listing listingName], [listing sectionName], [listing sectionNumber], [page urlSuffix], nil];
NSDictionary *listingMetaData = [NSDictionary dictionaryWithObjects:values forKeys: keys];
listingEditController.listingMetadata = listingMetaData;
[self.navigationController pushViewController:listingEditController animated:YES];
[listingEditController release];
}
Pretty much the same thing, only messier. That's a lot of crap passing between ViewControllers, and I'd be tempted to abstract it into an object if this happened anywhere else; fortunately, it doesn't. And the crap is necessary, alas. The detail controller includes seventeen fields. Yes, seventeen. Which is probably OK if you're just tweaking a few details of an existing listing, but is also the reason I added the note-to-future-self functionality. I am going to add an "Add New Listing" button, but I don't expect too many people to use it.
-(void)iAmThere{
NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
ViewEntry *listing = [[rootEntry getChildren] objectAtIndex:indexPath.row];
//construct location string
CLLocation *location = [locationManager location];
CLLocationCoordinate2D coordinate = [location coordinate];
NSString *locationString = [[NSNumber numberWithDouble:coordinate.latitude] stringValue];
locationString = [locationString stringByAppendingString:@","];
locationString = [locationString stringByAppendingString:[[NSNumber numberWithDouble:coordinate.longitude] stringValue]];
//build http request
NSArray *postKeys = [NSArray arrayWithObjects:@"pageUri", @"sectionName", @"sectionNumber", @"listingName", @"location", nil];
NSArray *postValues = [NSArray arrayWithObjects:[page urlSuffix], listing.sectionName, listing.sectionNumber, listing.listingName, locationString, nil];
HttpHelper *http = [[HttpHelper alloc] initWithPostKeys:postKeys postValues:postValues urlString:[Util getLocationUpdateURL]];
NSURLRequest *request = [http request];
//create connection
NSURLConnection *theConnection = [NSURLConnection connectionWithRequest:request delegate: self];
if (!theConnection) {
// inform the user that the download could not be performed
// TODO: real error handling
[Util doAlert:@"Connection creation failure" withMessage:@"Connection failed"];
}
[http release];
}
- (void)connection:(NSURLConnection *)connection
didFailWithError:(NSError *)error
{
// release the connection, and inform the user
[Util handleError:@"Connection Failure" withError: error];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//TODO: add checkbox indicating listing located
[Util doAlert:@"Success" withMessage:@"Connection finished"];
}
Here we have an HTTP connection. The idea being that people can geotag WikiTravel listings with one button; you're passing by the Royal Ontario Museum, you check your iPhone app, you notice that it does not yet have a geotag (I haven't yet, but will, added checkboxes to indicate this), and you push the "I'm there!" button to update its WikiTravel listing with your current location, no typing necessary.
Now, you can perform an HTTP request in a single line, with NSURLConnection's sendSynchronousRequest: method, but this one uses delegation, so we implement the "connectionDidFail..." and "connectionDidFinish" methods.
HttpHelper is my own class, used to give me a central point of control over all HttpRequests, partly so I didn't have to keep copy-pasting code, partly so I can handle caching in one place. (Although I think I may have to add a new "keep or clear cache" to its init.) Util is my own class that holds basic fixed values and utility methods.
#pragma mark -
#pragma mark Location manager
/**
Return a location manager -- create one if necessary.
*/
- (CLLocationManager *)locationManager {
if (locationManager != nil) {
return locationManager;
}
self.locationManager = [[CLLocationManager alloc] init];
[locationManager setDesiredAccuracy:kCLLocationAccuracyNearestTenMeters];
[locationManager setDelegate:self];
return locationManager;
}
/**
Conditionally enable the Add button:
If the location manager is generating updates, then enable the button;
If the location manager is failing, then disable the button.
*/
- (void)locationManager:(CLLocationManager *)manager
didUpdateToLocation:(CLLocation *)newLocation
fromLocation:(CLLocation *)oldLocation {
if ([[self tableView] indexPathForSelectedRow]) {
iAmThereButton.enabled = YES;
}
}
- (void)locationManager:(CLLocationManager *)manager
didFailWithError:(NSError *)error {
iAmThereButton.enabled = NO;
}
And that's how you use the location manager; like much of the iPhone SDK, it's a delegate pattern. You create it and set its delegate; it goes out and tries to work out where you are; when it succeeds or fails, it lets you know. Simple, really.
#pragma mark -
#pragma mark Cleanup
- (void)dealloc {
[addNoteButton release];
[editListingButton release];
[iAmThereButton release];
if (createdRoot) {
[rootEntry release];
[locationManager release];
}
[super dealloc];
}
@end
Note that we only release the rootEntry and locationManager if we're the king ViewController at the top of the hierarchy. Otherwise we're asking for serious EXC_BAD_ACCESS disaster.
Aaaaaand there you go. The whole ListingViewController, in all its glory.
Note, he mentioned with some pride, that I started it writing it only four days ago - and that it's only maybe a quarter of the code I've written and tested since then.
One last caveat. You'll notice there's no copyright claim up above, and you're welcome to use the code as is, but - while it's pretty much running now, there are still tweaks I want to make. Error handling and localization, in particular. And I have hardly any unit tests yet. So if your need isn't dire and urgent, I advise you to wait until I officially release the code at code.google.com, under an open-source license, and post to this blog to that effect.
Labels: CLLocationManager, customView, iPhone, NSThread, NSURLConnection, Objective-C, performSelectorOnMainThread, SDK, selector, threading, UIActivityViewIndicator, UIBarButtonItem
Thursday, July 2, 2009
Don't call me an Objectivist...
Well, sorta. I write to you on my shiny new MacBook, which I purchased in part because you can't write iPhone applications on non-Macs. I guess I don't resent that. Much. It is a nice machine, although I'm still getting used to all of OS X's quirks.
But never mind the OS: writing an iPhone version of my app means mastering the iPhone SDK and a whole new language, Objective-C. Which was kind of frustrating for a week. Imagine, for a moment, that you're a car mechanic. Now imagine that you move to a new garage, where the cars are totally different, not just the engines but even the doors and the dashboard controls are laid out in some bizarre new arrangement, and instead of your old tools, there's a wall full of all these curvy New Age things that look more like vacuum cleaner parts and art objects than hex wrenches and hammers. That's kind of what diving into the iPhone SDK was like.
But. You are a car mechanic. And ultimately, we're still talking about wheeled vehicles powered by internal combustion engines; so after a bewildering week, during which you often don't even know what questions to ask, you begin to realize that in fact these cars are quite elegantly if counterintuitively designed, and that these tools are well suited to them.
Yeah, so the metaphor got out of hand, sue me. Anyway, I didn't know Objective-C, but I've been writing software professionally for nigh on twenty years now, and it's kind of a weird mixture of C and Smalltalk, both of which I have worked with before. It turns out that it is a powerful and flexible language, and the iPhone SDK's tools and libraries do indeed ultimately make sense.
Don't get me wrong. There are still things I hate. Mostly the hangovers from C. Just like C, object you create requires two files: one where all the code goes, and a "header file" that describes the code. It was a bad idea then, and it's a bad idea now, which is why no modern languages require it. The resulting profusion of files is messy, annoying, completely unnecessary, and provides more places for things to go wrong.
Also, you have to allocate memory by hand, and then be extremely careful to de-allocate it, lest you leak memory and consume all your phone's resources. I shake my head with bewildered contempt. Manual memory management is archaic, clumsy, tedious, and perpetually prone to disaster. Making it part of the iPhone SDK is like having a beautifully renovated marble-and-gold bathroom built atop lead plumbing.
(My understanding is that if you're writing for Macs, you can have automatic garbage collection, like the rest of the civilized world, but such is strongly discouraged on the iPhone. Scarce resources, apparently. But I note that Java development for similarly specced Android phones, with garbage collection - and sans useless header files - works just fine...)
I have also learned that there are subtle pitfalls in the Objective-C environment. I'll concede that this can be true of Java as well, though usually that's when you're dealing with classpaths, less of an issue when doing Android development. But, well, see for yourself. Here's what looks like a garden-variety call to a Settings class:
NSMutableString *urlString = [NSMutableString stringWithCapacity:128];
[urlString appendString:[Settings getSearchPageURL]];
which should call the "getSearchPageURL" class method on my "Settings" class, which right now is pretty much just a bundle of constants. Here's a simplified Settings.h:
#import <UIKit/UIKit.h>
@interface Settings {
}
+(NSString *) getSearchPageURL;
and Settings.m:
+(NSString *)getSearchPageURL
{
return @"http://wetravelwrite.appspot.com/mb/searchForPage";
}
Nothing to it, right? Compiled just fine. Did it run? Did it hell. Instead I got
2009-07-02 22:48:28.519 iTravelWrite[17002:20b] *** NSInvocation: warning: object 0xaa40 of class 'Settings' does not implement methodSignatureForSelector: -- trouble ahead
2009-07-02 22:48:28.520 iTravelWrite[17002:20b] *** NSInvocation: warning: object 0xaa40 of class 'Settings' does not implement doesNotRecognizeSelector: -- abort
OK, points for the amusing "trouble ahead" warning, but what the hell? I'm just calling a very simple function that does nothing but return a hardcoded string - so what could possibly have gone wrong?
Turns out that function automatically gets abstracted up at runtime into a selector - and for that to work, Settings must extend NSObject. Which is trivially done, of course, but far from intuitive.
Wacko problem number 2: I changed a variable from an NSArray to an NSDictionary, and reworked the code appropriately - no big deal - and suddenly the app was dying with an EXC_BAD_ACCESS call, meaning that I was trying to release the memory of an object I had already released. Muttering foul imprecations about the whole notion of manual garbage collection, I went carefully through my code and found ... nothing. So I Googled. (Which is how I ultimately solved the previous problem, too; but in both cases I had to go well past the first page of results, which is one reason I'm writing this with copious details. Maybe future sufferers will find their way to this post.)
Here are the relevant parts of the header file:
@interface SearchController : UITableViewController{
NSDictionary *searchResults;
}
@property (nonatomic, retain) NSDictionary *searchResults;
and it turns out that the problematic line in the implementation was this one:
searchResults = [NSDictionary dictionaryWithObjects: urlSuffixes forKeys: pageNames];
Looks harmless enough, doesn't it? Both urlSuffixes and pageNames are perfectly acceptable NSArrays of NSStrings. So what's the problem?
Well, if you're not familiar with Objective-C, you might be a little gobsmacked to learn that the solution was to change the above line to
self.searchResults = [NSDictionary dictionaryWithObjects: urlSuffixes forKeys: pageNames];
Yes, really.
As far as I can tell, the difference is that in the second example, instead of the instance variable being directly modified, the @synthesized auto-generated setter is called, with "retain" as dictated in the @property definition, which prevents the NSDictionary from being released when we exit the method which generates it. Perhaps some people think of this stuff as intuitive. Not me. It's all fixed and working now, but this kind of man-behind-the-curtain stuff makes me a little uneasy.
That said, there's a lot to like. The user-interface stuff, once you figure it out, is (mostly) a joy to work with. You can lay out items visually or programmatically; you get a built-in navigation header and button toolbar, yours for just a few lines of code; it's easy to reach out via HTTP, with a one-line synchronous-call version, a delegated version with fine control, or a compromise somewhere between; and as of the 3.0 SDK, instead of hand-writing SQL code to access SQLite, you can use Apple's Core Data engine to manage all your persistent data. (Which comes with its own problems but overall is definitely more elegant and more powerful.)
Also, while I wouldn't say that I'm fluent in Objective-C yet, I'm now conversant, and I like it a lot when it's reminding me of Smalltalk, and not reminding me of C. You do, however, seem to wind up writing a lot more lines of code than you would in Java to do the same thing. It's like speaking German vs. English.
Overall, though, qualified thumbs up. I still prefer Java to Objective-C, and there are still things that Android can do and the iPhone can't - multitasking is the biggest, and the associated inter-app communication possibilities - while I can't think of any examples of the converse. But I am increasingly a fan of the latter's SDK. What at first looked a bit like a junkyard is now becoming more of a playground.
Labels: Apple, doesNotRecognizeSelector, EXC_BAD_ACCESS, iPhone, Objective-C, SDK
Subscribe to Posts [Atom]