Showing posts with label Tip. Show all posts
Showing posts with label Tip. Show all posts

Monday, August 2, 2010

Core Data Odds and Ends

I've been doing a fair bit of work with Core Data lately, and have picked up a few tricks to make my life easier, so I thought I'd share.

Mo' Power


Back in the EOF days, there was a great little piece of third-party software called EOGenerator that implemented something called the generation gap design pattern. Essentially, this little program would create your custom subclasses of EOGenericRecord (EOF's equivalent of NSManagedObject) for you. It wouldn't just create that, though, it would actually create two classes for each entity in your EOModel.

One of these two classes was yours to modify as you saw fit. It would never get overwritten even if you ran the EOGenerator script again, over and over. The other class contained all the generated code derived from your EOModel and was designed not to be touched by the programmer. You had your class, EOGenerator had its class, and you could both work without any fear of stepping on each other's toes. You could add attributes and entities to your data model and regenerate the machine-readable classes without any chance of losing your custom validation or methods. It was nice.

Wolf Rentzsch has re-implemented this idea for Core Data, and the result is called MOGenerator. If you spend any time with Core Data and aren't familiar with MOGenerator, go become familiar with it. It will make your life much, much, much better.

SQLite to Check Data


The default persistent store type for Core Data under iOS is a SQLite store, which means that Core Data keeps all of its data inside of a single SQLite database in your application's /Documents folder. Now, the format of the persistent store is undocumented and you should never, ever, ever, ever, never change data in the persistent store or make any assumptions about how Core Data stores the data. That's part of why we use Core Data: It's an abstraction layer that lets us not worry about the underlying storage details.

But, sometimes, it can be really helpful to know whether data is getting into the persistent store or not. I often work on my data model before my user interface, so being able to peek in the data store to make sure all is well is really helpful.

The first thing you might want to consider doing, if you plan to do that, is to create a SQLite config file. This is a file of commands that will execute whenever you launch SQLite. To do that, simply create a file called .sqliterc in your home directory. Here's what mine looks like:
.mode column
.header on
.table
In mine, the first line tells SQLite to print the column names when I issue a SELECT command. The second line tells SQLite to present the data in tabular form. The last line prints out a list of all the tables in the database before it shows me the SQL command prompt. That's handy given that the table names don't usually exactly match your entity or class names.

Once you have a config file in place, you can open up any persistent store using the sqlite3 command in Terminal.app, like so:
sqlite3 MyApplication.sqlite
You can't, of course, execute this directly against database files on your phone, but you can do it in the simulator as well as on files copied from your phone using Xcode's Organizer. If you're using the simulator, you can look for your SQLite databases at the following location:
/Users/[your account name]/Library/Application Support/iPhone Simulator/[SDK version #]/Applications/[application UUID]/Documents/[Application Name].sqlite


When I log into a database with my SQLite config file, this is what I see:
-- Loading resources from /Users/jeff/.sqliterc
ZHERO ZPOWER Z_METADATA Z_PRIMARYKEY
SQLite version 3.6.12
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite>
Notice that it has dumped the names of all my tables. It's pretty easy to figure out which table holds what data once they're right in front of you. The example above is the persistent store from the SuperDB application from More iPhone 3 Development. The Hero entity managed objects are stored in ZHERO and the Power entity managed objects are stored in ZPOWER. To look at the contents of a table, you just need to execute a SQL SELECT command. The simplest is just SELECT * FROM TABLENAME;, which prints the entire contents of the table to the console, like so:
sqlite> select * from ZPOWER;
Z_PK Z_ENT Z_OPT ZHERO ZSOURCE ZNAME
---------- ---------- ---------- ---------- ---------- ----------
1 2 2 1 Mutation Fire
sqlite>
The format of the data is pretty easy to figure out but, again, you really should look and not touch. It'd be very easy to accidentally destroy a persistent store. A seemingly inconsequential change could get the metadata out of whack and prevent your app from using the persistent store any more.

Better Descrption


I noticed a few days ago that Wil Shipley was complaining about the fact that many of Apple's delivered UIKit and Foundation objects have not overridden the -description method. This method is important because it's the one that is used in format strings, NSLog(), and when you type the po command in GDB. Instead of getting a meaningful description of many delivered objects, you instead get the class name and memory address of the instance, which is what NSObject's implementation provides, which isn't very much help while debugging.

Unfortunately, NSManagedObject is one of the many, many classes that doesn't have its own meaningful implementation of -description. That doesn't mean you can't provide one, though.

One thing you can do is to create an abstract subclass of NSManagedObject and put the -description method there. If you then subclass this abstract class, all of your managed objects will inherit the method from the abstract class. If I have other common functionality, I'll often do exactly that. But if I don't (and often you won't) I personally hate to clutter up my object hierarchy to add debugging tools. So I actually will cheat and do something you're not really supposed to do, which is override a method using a category. The reason it's bad to do this is because the Objective-C language does not specify which method should be used, which can lead to unpredictable behavior. But, when a class inherits a method from a superclass, I've found that overriding it in a category always works. I'm not suggesting you should do this in a shipping app, however. I wrap my method in pre-compiler definitions so that it doesn't get compiled into the release version of my application.

My version of -description assumes you're generating properties for your managed objects (I always do) and also assumes you're using custom subclasses (but you are, because you're using MOGenerator, right??). The method iterates over all the properties of the class and all its superclasses up to NSManagedObject, then prints them to the console. The categories I use look like this:
#if DEBUG
@implementation NSObject(MCManagedObjectDebug)
+ (NSMutableArray *)MCproperties
{
NSMutableArray *properties = nil;

if ([self superclass] != [NSManagedObject class])
properties = [[self superclass] MCproperties];
else
properties = [NSMutableArray array];


unsigned int propCount;
objc_property_t * propList = class_copyPropertyList([self class], &propCount);
int i;

for (i=0; i < propCount; i++)
{
objc_property_t oneProp = propList[i];
NSString *propName = [NSString stringWithUTF8String:property_getName(oneProp)];
if (![properties containsObject:propName])
[properties addObject:propName];
}

return properties;
}

@end


@implementation NSManagedObject(MCManagedObjectDebug)
- (NSString *)description
{
NSArray *properties = [[self class] MCproperties];
NSMutableString *ret = [NSMutableString stringWithFormat:@"%@:", [self className]];
NSDictionary *myAttributes = [[self entity] attributesByName];

for (NSString *oneProperty in properties)
{
NSAttributeDescription *oneAttribute = [myAttributes valueForKey:oneProperty];
if (oneAttribute != nil) // If not, it's a relationship or fetched property
{
id value = [self valueForKey:oneProperty];
[ret appendFormat:@"\n\t%@ = %@", oneProperty, value];
}


}

return ret;
}


@end
#endif
The +MCProperties method uses the Objective-C runtime to discover and iterate over properties in the class definition, including @dynamic properties that were created at runtime. Because the property runtime calls do not include properties inherited from parent classes, the method acts recursively, iterating up the object hierarchy until it gets to NSManagedObject. The actual -description method simply prints out each of the attributes and the value currently stored for that attribute for this object instance. Once you do this, if you use NSLog() or po, the result is much richer information about the objects getting printed to the console. This version ignores relationships and fetched properties, but it wouldn't be hard to add those in if you needed them.

Monday, March 22, 2010

Absolute Filenames

Well, I got bit today by something I should've known better than to do. It was a classic newbie mistake so, but it caused me enough grief that I feel it's worth embarrassing myself with a cautionary post. Here's what happened:

When you use Core Data, Apple recommends that except for relatively small chunks of binary data, you store binary data like images, sounds, and movies in the file system and not inside Core Data's persistent store. Now, in my experience, SQLite can handle even really large blobs without too much additional overhead, but when Apple makes a specific recommendation like this, I find it's usually a good idea to follow that recommendation unless I have a really good reason not to. In this case, I didn't.

In an app I'm working on for a client, I'm recording audio using AVAudioRecorder. I don't ask the user to choose a name for the file, I just store the recordings as caf files in the file system using a filename based on the date and time, like so:
NSString *audioPathString = [NSString stringWithFormat:@"%@/%@", DOCUMENTS_FOLDER, [item valueForKey:@"audioPath"]];


Then, later on, after the recording is done, I store audioPathString inside a managed object that represents audio recordings:

audioItem.audioPath = audioPathString;


Then, when I want to play the sound, I just pull the pathname from Core Data and load the data from the file system, sort of like this:

NSError *error;

NSURL *url = [NSURL fileURLWithPath:item.audioPath];
AVAudioPlayer *player =[[AVAudioPlayer alloc] initWithContentsOfURL:url error:&error];


And it worked great. For a while. I could quit and start back up and it would keep working. And then, at some point, it wouldn't work any more. It would report back OSStatus codes of -43 or -50 when I tried to create an instance of AVAudioPlayer, even though the audio file was still in the /Documents directory. I'm sure a few of you are chuckling to yourselves about my dumb error, but for those who don't see it, I'll explain.

Tip: If you find yourself in a situation where an NSError gives you nothing more than an OSStatus or OSErr error code, you can find out what that error code means by dropping to the terminal and typing macerror followed by the error code you're interested in, including the - (these old-style error codes are negative numbers).


The full pathname to an item in the Documents directory of an application's sandbox includes not the name of the application, but a UUID (unique identifier) instead. When you build and run your application, a new UUID is calculated if, and only if, something has changed since your last build. If you haven't made any changes, it keeps running with the previous UUID. Once you make some changes to code or a resource and re-compile, the UUID changes, and the stored absolute path no longer points to the right location. The portion of the URL that represents the Application's UUID has now changed. Xcode is smart enough to move the application's /Documents directory to the new location so you don't lose all of your data while developing, but all that data is now at a new location in the file system.

So, how do you handle this situation? The easiest way is to just store the filename and then re-build the absolute path at runtime by assuming the file is in your application's /Documents directory. So, instead of storing the path from recorderFilePath as I did in the earlier code sample, you would store just the last path component using NSString's lastPathComponent method, like so:

newItem.audioPath = [recorderFilePath lastPathComponent];


Then, when it's time to load the file, you just re-build the full path to the file based on the current UUID of the application, like so:

NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString *audioPathString = [NSString stringWithFormat:@"%@/%@", documentsDirectory, [item valueForKey:@"audioPath"]];


Using this approach, your code will continue to function even between development builds or application releases, and you won't waste an hour trying to figure out why your perfectly valid files won't load.

Tuesday, November 4, 2008

Changing Application Name

If you need to change the name of the Application as it appears on the iPhone's home screen, you need to do it in the Target configuration, not the project configuration. Expand the Targets group in Xcode, then single-click the item under that. It should share the name of your project, which is also the default name of the application that gets generated.

Press command-I to bring up the Info window, then navigate to the Build tag. Set the Configuration drop-down to read All Configurations, then look for a setting called Product Name under the Packaging heading. Change that value to the name you want for your compiled applications and, in the immortal words of Bugs Bunny: Viola! Do a clean then build and your application will take on the new name.

Xcode File Templates and a Mystery

One of the things that confuses many newcomers to Xcode is how to set it up so that your company name gets automatically filled in when you create a new file using one of the file templates. The company name value is based on a parameter that is not exposed through the Xcode's preferences or through the System Preferences GUI. The only way that I know of to set this is to execute a single line in the Terminal:

defaults write com.apple.Xcode PBXCustomTemplateMacroDefinitions '{ORGANIZATIONNAME = "My Company Name";}'


The mystery here, for me, is that my files are getting created with an organization name, though I've never actually set the organization name on this machine. I do work for several clients, and just never bothered to set it. Only, I noticed today that the value is set. It didn't used to be - I have many source code files to attest to that. Not only that, but it is set to the Organization value that I used on my ADC membership. I find that very odd and have no idea what's going on.

Customizing File Templates


Anyway, since I'm on the topic, did you know that you can also change the file templates to contain whatever information you want? You probably did, but in case you don't know how to go about it, I'll shed a little light on the matter. There are many tutorials floating around the Intertubes on this subject, but you might find it hard to follow some of them. The file template locations have changed a couple of times recently. Most tutorials you find will direct you to templates located here:
/Library/Application Support/Apple/Developer Tools/File Templates

Only, they're not there any more, at least on my install of Leopard. It's possible that they might be there on your system if you didn't do a clean install of Leopard (don't know either way, just saying it's possible), but the default location now is:
/Developer/Library/Xcode/File Templates

But, if you look there, you'll notice a conspicuous absence, which is that there are no iPhone file templates anywhere to be found. The iPhone-specific file templates are located inside the iPhone platform folder:
/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/File Templates

Now, generally, it's a good idea not to mess with the original files. So, if you want to change a file template, you should copy it to ANOTHER location. But it has to be in a location where Xcode will find it. The base location for user templates, which will override any of the templates in the System, Library, or Developer folder, is:
~/Library/Application Support/Apple/Developer Tools/File Templates/

Most of you know that ~ indicates your home folder, but I'm mentioning it on the off chance that somebody doesn't. Anyway, if you want to customize an existing file template, your file must match the directory hierarchy of the file you are replacing. So, for example, if you want to customize the UIView Subclass template, you would copy the folder:
/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/File Templates/Cocoa Touch Classes/UIView subclass.pbfiletemplate/

to the location:
/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/File Templates/Cocoa Touch Classes/UIView subclass.pbfiletemplate/

Once you've done this, you can then then open the template files contained in the UIView subclass.pbfiletemplate folder in a text editor. You can add a copyright statement or license or whatever you need written on every source file you create. Any changes you make will roll into any new files you create using the selected file template, though it won't affect existing files.

The following substitution variables will get be replaced with context- or date- specific information when you create a file using the template:

«DATE»
«DIRECTORY»
«ORGANIZATIONNAME»
«PROJECTNAME»
«PROJECTNAMEASIDENTIFIER»
«TIME»
«USERNAME»
«FULLUSERNAME»
«YEAR»

Those characters you see surrounding the substitution variables are not two greater-than or less-than symbols, those are unicode characters called guillemets (which are used similarly to quotation marks in several languages). If you change the file encoding when you save, or if you change the guillemets to greater-than or less-than symbols, the substitution won't work. Copy and paste is your friend here, but if you need to type them type "«" as Option-Backslash and "»" as Option-Shift-Backslash (Backslash is the one above the return key on most keyboards, not the one that shares a key with the question mark).

Note: Project templates work similarly, but are more complex.

Friday, October 24, 2008

Captain Obvious Strikes Again

In the Xcode editor pane (the part of the window where your source code is displayed), you can option-double-click any term to search for it in the Documentation Browser. You can also command-double-click any term to open up its definition in the public header files.

That is all.

Wednesday, October 22, 2008

You Shouldn't, but You Will...

Class-Dump is an essential tool in the Cocoa Programmer's arsenal. It will read a compiled Objective-C binary and print out the header files for all classes used in that file. Depending on the settings used to compile, it may not have the correct variable names, but it will have the correct method signatures, even those that are not declared in the public headers.

Now, for the iPhone, we can't use Class-Dump, because it doesn't recognize binaries compiled for the ARM6 processor. Additionally, we shouldn't use it, because it means we're poking around in private stuff that could get our Application banned from the App Store.

But, for the curious, I thought I'd point out that you can actually use Class-Dump on iPhone frameworks; you just have to run it on the framework that's in the SDK for the simulator rather than the device. The iPhone simulator is NOT an emulator.. it's not running ARM6 byte codes, it's running X86 byte codes (except for those naughty people who installed the SDK on a PPC Mac).

So, want to see the headers from the UIKit in all their glory?


cd /Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator2.1.sdk/System/Library/Frameworks/UIKit.framework
class-dump UIKit > ~/Desktop/UIKit.txt


But, shhhh.... You didn't learn it from me.

Monday, October 20, 2008

Another Couple of Tips by Captain Obvious

The iPhone SDK is so large, it's easy to miss things that other people think are obvious. Because of that, I'm going to post another tip that most people probably know already, but that some people might now.

If you create a PNG file of size 320x480 and call it "Default.png" (spelled exactly that way, making sure you have the first letter capitalized), you can then add that image to your Xcode project's Resources group and it will act sort of like a splash screen for our application - it will show that image until your application is fully loaded.

Although I'm not a big fan of splash screens, it's a good idea if you've got a slow-loading application to let the user know everything is proceeding okay. If you've got a slow-loading application, and you don't know why, it may be because you have a lot of resources in your project. The iPhone seems to do some kind of check on application launch, and the more individual files you have in your application bundle, the longer it takes. The actual size of the files doesn't seem to matter, but the number of files definitely does. If you're in this situation, consider reducing the number of files in your bundle somehow. A fairly easy way to do that is to store all those files as blobs inside a single SQLite database. You can use a read-only database from your bundle without copying it to the Documents folder, but you must make sure not to write to it, because if you change anything in your application bundle, code signing for the application will stop working and the user will not be able to launch it again. You can use a separate SQLite database in your application's Documents folder for user data.

Friday, October 3, 2008

Did you know you could take screenshots without Xcode?

I discovered this accidentally today. It may be documented somewhere, but I was not aware of it. You can take screenshots of your iPhone and store them in your iPhone's photo library, even if your phone is not hooked up to your Mac. I assume most people know that they can take screenshots using the Organizer in Xcode, but you can do it with just the iPhone and nothing else. I wish I would have known this when I was taking screenshots for the book.

Here's how you do it. Hold down the power button - the one that's on the top edge of the phone. While the power button is being held down, press and then quickly release the Home button (the round button with a rounded square on the front of the phone). You'll hear the click noise and see a flash, and when you go to the Photos application, you'll see your screenshot.