Using Frameworks Within NSBundles
I wanted to use Connection Kit for my current project, but ran into issues as it requires a few other frameworks to function. Many months ago I worked around this by copying the internal framework’s source files directly into Connection Kit.
Everything went well until I updated Connection Kit. Suddenly it required more internal frameworks, and copying their source became impractical.
Listing Methods at Runtime in Objective C
Before getting into creating my next Rapid Weaver plugin, I needed to do some experimentation to make sure what I wanted to achieve was possible. Wanting to manipulate some aspects of RWPage objects, I first required a list of methods for the RWPage class.
Thanks to Matt Gallagher’s IMP of the current method entry, I was able to generate my method list with the following code:
#import "objc/objc-class.h" // Iterate over the class and all superclasses Class currentClass = [RWPage class]; while (currentClass) { // Iterate over all instance methods for this class unsigned int methodCount; Method *methodList = class_copyMethodList(currentClass, &methodCount); unsigned int i = 0; for (; i < methodCount; i++) { NSLog(@"%@ - %@", [NSString stringWithCString:class_getName(currentClass) encoding:NSUTF8StringEncoding], [NSString stringWithCString:sel_getName(method_getName(methodList[i])) encoding:NSUTF8StringEncoding]); } free(methodList); currentClass = class_getSuperclass(currentClass); } |
Method list acquired, the process of whittling it down until I found the one I wanted began.
Extracting hex value from NSColor
Using NSError to Increase Robustness & Useability
It wasn’t until we (long ago) decided to use the hardcore ‘E_ALL‘ error reporting level in our development environment that I started to realize the value of being overly paranoid when coding. Long ago I recall I did this, but I think PHP made me a little lazy – it’s a very forgiving language, so long as one uses an appropriate error reporting level ;)
Many Cocoa objects implement methods that take an optional error parameter. Years ago, before I was forced to take a long sabbatical from Cocoa programming, I interpreted ‘optional’ as ‘ignore’.
When I had time to get back into Cocoa, I tasked myself with tidying up Slider 2′s code-base. I found myself cringing in a few places, noticing possible situations that could cause Rapid Weaver to … crash and die.
Enter NSError.
To highlight it’s utility, and how it can enhance both useability and the robustness of your Cocoa application / plugin, I provide the following example. Consider a method that should read some data from a file (with a specific format) then unarchive the data. This is precisely what Slider 2 must do when one attempts to load a Slidebrary item from a file. We need to ensure that the file has the correct extension, the data can be read, and that it can be unarchived. We need to do this without causing Rapid Weaver to crash when either of the aforementioned three expectations aren’t realized. The caller also needs to know what went wrong, so the user can be informed.
The method that accomplishes this is reproduced below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | + (SSlidebraryItem *) loadFromPath:(NSString *)path error:(NSError **)error { // 1) check the path - if it's incorrect create an NSError object and populate it. if (![[path pathExtension] isEqualToString:[SSlidebraryItem fileExtension]]) { // Unacceptable extension! // Populate our error variable // I've used a convention outlined here: http://stackoverflow.com/a/3276356/187954 // Notice that userInfo is human-readable. This is the string that we can use to inform our user of the error *error = [NSError errorWithDomain:@"com.codeofinterest.Slider2" code:100 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Passed an invalid file format \"%@\" at \"%@\"", [path pathExtension], path] forKey:NSLocalizedDescriptionKey]]; return nil; // Caller should be checking that the error object is nil, and reacting appropriately } // File extension was acceptable - attempt to read the data from the file // We pass the error object to -initWithContentsOfFile NSData *encodedSettings = [[NSData alloc] initWithContentsOfFile:path options:NSDataReadingUncached error:error]; // If something went wrong, return nil. // The error object will have been populated by -initWithContentsOfFile if (*error != nil) { return nil; } // Have to use a try ... catch block here because +unarchiveObjectWithData raises an // NSInvalidArchiveOperationException if data is not a valid archive, instead of returning nil and // populating an error object @try { SSlidebraryItem *loadedSlidebraryItem = [NSKeyedUnarchiver unarchiveObjectWithData:encodedSettings]; // No data! This shouldn't happen... if (!loadedSlidebraryItem) { // Populate the error object with details of what happened *error = [NSError errorWithDomain:@"com.codeofinterest.Slider2" code:101 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Failed to load slidebrary item from \"%@\"", path] forKey:NSLocalizedDescriptionKey]]; return nil; } // Everything went well, return the loaded, unarchived object! return loadedSlidebraryItem; } @catch (NSException *exception) { // An exception occurred! This calls for another error message. *error = [NSError errorWithDomain:@"com.codeofinterest.Slider2" code:102 userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:@"Failed to unarchive slidebrary item \"%@\"", path] forKey:NSLocalizedDescriptionKey]]; // Also log the exception, so if a user emails you asking why their Slidebrary item won't load, you can // ask them to run Rapid Weaver in a terminal and copy - paste any error output to aid your debugging! // I like to use the logging extension described here: http://pagesofinterest.net/blog/2011/09/more-informative-nslog-alternative/ SLog(@"%@", exception); return nil; } } |
As an example of a possible way to handle functions that provide error information in this manner, the loadSelectedSlidebraryItem method is included below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | - (SSlidebraryItem *) loadSelectedSlidebraryItem { // Prepare the path for the Slidebrary item the user has selected NSDictionary *selectedSlidebraryItem = [[slidersController arrangedObjects] objectAtIndex:[table selectedRow]]; NSString *path = [[SSlidebraryController slidebraryDirectory] stringByAppendingPathComponent:[selectedSlidebraryItem objectForKey:key_si_filename]]; NSError *loadError = nil; // Prepare our error object SSlidebraryItem *settings = [SSlidebraryItem loadFromPath:path error:&loadError]; // There was a problem loading the file if (loadError != nil) { // I like to slide a modal dialog out informing the user of the error as // it makes it impossible to ignore and easy for them to copy-paste or // grab a screenshot & email me the details [SModalSheetDialogs showModalMessage:@"An error occurred" withInformativeText:[loadError localizedDescription] withStyle:NSWarningAlertStyle fromSender:self]; return nil; } // Success - return the loaded object return settings; } |
Summary: using NSError within your application / plugin is more work, but makes for a more robust and user-friendly product. Two facets of a project that should sit high on the priority list of every developer.
Sliding Modal Dialogs in Cocoa Applications & Bundles
Example project available on GitHub – CIModalSheetDialog
One thing I wanted to be able to use during the development of Slider 2 was modal dialogs that slide out from the top of Rapid Weaver. This was troublesome for me as Slider 2 is a bundle that runs within Rapid Weaver – not as a standalone application.
Googling and experimentation allowed me to find a solution for problem that I find acceptable. Below is my implementation of a modal sheet dialog, further down one may find some usage examples.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // // SModalSheetDialogs.h // // Created by Michael Robinson on 7/08/11. // Copyright 2011 Code of Interest. All rights reserved. // #import <Foundation/Foundation.h> @interface SModalSheetDialogs : NSObject + (void) showModalMessage:(NSString *)message withInformativeText:(NSString *)informativeText withStyle:(NSAlertStyle)style fromSender:(id)sender; + (void) showModalRequest:(NSString *)message withInformativeText:(NSString *)informativeText withStyle:(NSAlertStyle)style andButtons:(NSArray *)buttons andContextInfo:(void *)contextInfo fromSender:(id)sender; @end |
And the implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // // SModalSheetDialogs.m //{filelink=1} // Created by Michael Robinson on 7/08/11. // Copyright 2011 Code of Interest. All rights reserved. // #import "SModalSheetDialogs.h" @implementation SModalSheetDialogs + (void) showModalMessage:(NSString *)message withInformativeText:(NSString *)informativeText withStyle:(NSAlertStyle)style fromSender:(id)sender { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; [alert addButtonWithTitle:@"OK"]; [alert setInformativeText:informativeText]; [alert setMessageText:message]; [alert setAlertStyle:style]; [alert beginSheetModalForWindow:[NSApp mainWindow] modalDelegate:sender didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:nil]; } + (void) showModalRequest:(NSString *)message withInformativeText:(NSString *)informativeText withStyle:(NSAlertStyle)style andButtons:(NSArray *)buttons andContextInfo:(void *)contextInfo fromSender:(id)sender { NSAlert *alert = [[[NSAlert alloc] init] autorelease]; NSEnumerator *e = [buttons objectEnumerator]; id object; while (object = [e nextObject]) { [alert addButtonWithTitle:(NSString*)object]; } [alert setInformativeText:informativeText]; [alert setMessageText:message]; [alert setAlertStyle:style]; [alert beginSheetModalForWindow:[NSApp mainWindow] modalDelegate:sender didEndSelector:@selector(alertDidEnd:returnCode:contextInfo:) contextInfo:contextInfo]; } @end |
Showing a dialog is simple, example below.
1 2 3 4 | [CIModalSheetDialog showModalMessage:@"Dialog title" withInformativeText:@"Dialog content" withStyle:NSInformationalAlertStyle fromSender:self]; |
This is fine for cases where we’re just informing the user that something happened – in these cases we don’t care what the user does, as the only option they have is to acknowledge the message. In more complex cases, where the user is presented with a choice, we need to have a way to catch that choice.
As the object handling the user’s choice may differ across use cases, we set the sender (as above). It is expected that this sender implements the following delegate method:
- (void) alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo |
An example, first of showing a more complex dialog, with two buttons:
1 2 3 4 5 6 | [CIModalSheetDialog showModalRequest:@"Dialog title" withInformativeText:@"Dialog content" withStyle:NSInformationalAlertStyle andButtons:[NSArray arrayWithObjects:@"OK", @"Cancel", nil] andContextInfo:@"Some identifying information" fromSender:self]; |
And our implentation of alertDidEnd:
1 2 3 4 5 6 7 8 9 | - (void) alertDidEnd:(NSAlert *)alert returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { if (contextInfo == @"Some identifying information") { if (returnCode == NSAlertSecondButtonReturn) { // OK button was clicked // Handle OK } else { // Handle Cancel } } } |

