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.

Comments (5) | Trackback

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.

Comments (5) | Trackback

Extracting hex value from NSColor

Example project available on GitHub –

NSColor+Hex category available on GitHub –

This category on NSColor allows one to get or set an NSColor’s colour using hex values. Intitialising an NSColor with a hexadecimal colour:
1
2
3
4
5
6
7
8
@try {
    // Both shorthand and full forms of hexadecimal colours are accepted
    [colorWell setColor:[NSColor colorWithHex:@"#F00"]];
    [colorWell setColor:[NSColor colorWithHex:@"#Ff0000"]];
}
@catch (NSException *exception) {
    NSLog(@"%@", [exception description]);
}
It’ll throw an exception if one attempts to use a string that is not 3 or 6 characters in length, excluding the hash. Getting the hexadecimal representation of a given NSColor:
1
NSString *hexColor = [color hexColor]
This being my first Cocoa category, I’m sure it must have rough edges. Nevertheless, I have found it very useful. Below is the header for NSColor+Hex:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
//  NSColor+Hex.h
//  CINSColor+Hex
//
//  Created by Michael Robinson on 4/12/11.
//  Copyright 2011 Code of Interest. All rights reserved.
//
 
#import <AppKit/AppKit.h>
 
@interface NSColor (Hex)
 
+ (NSColor *) colorWithHex:(NSString *)hexColor;
- (NSString *) hexColor;
 
@end
And the implementation, more verbose than I’d like but it does the job. If anyone improves on it, I’ll be happy to update this page & accept your pull request on GitHub:
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
//
//  NSColor+Hex.m
//  CINSColor+Hex
//
//  Created by Michael Robinson on 4/12/11.
//  Copyright 2011 Code of Interest. All rights reserved.
//
 
#import "NSColor+Hex.h"
 
@implementation NSColor (Hex)
 
 
+ (NSColor *) colorWithHex:(NSString *)hexColor {
 
    // Remove the hash if it exists
    hexColor = [hexColor stringByReplacingOccurrencesOfString:@"#" withString:@""];
    int length = (int)[hexColor length];
    bool triple = (length == 3);
 
    NSMutableArray *rgb = [[NSMutableArray alloc] init];
 
    // Make sure the string is three or six characters long
    if (triple || length == 6) {
 
        CFIndex i = 0;
        UniChar character = 0;
        NSString *segment = @"";
        CFStringInlineBuffer buffer;
        CFStringInitInlineBuffer((CFStringRef)hexColor, &buffer, CFRangeMake(0, length));
 
 
        while ((character = CFStringGetCharacterFromInlineBuffer(&buffer, i)) != 0 ) {
            if (triple) segment = [segment stringByAppendingFormat:@"%c%c", character, character];
            else segment = [segment stringByAppendingFormat:@"%c", character];
 
            if ((int)[segment length] == 2) {
                NSScanner *scanner = [[NSScanner alloc] initWithString:segment];
 
                unsigned number;
 
                while([scanner scanHexInt:&number]){
                    [rgb addObject:[NSNumber numberWithFloat:(float)(number / (float)255)]];
                }
                segment = @"";
            }
 
            i++;
        }
 
        // Pad the array out (for cases where we're given invalid input)
        while ([rgb count] != 3) [rgb addObject:[NSNumber numberWithFloat:0.0]];
 
        return [NSColor colorWithCalibratedRed:[[rgb objectAtIndex:0] floatValue] 
                                         green:[[rgb objectAtIndex:1] floatValue] 
                                          blue:[[rgb objectAtIndex:2] floatValue] 
                                         alpha:1];
    }
    else {
        NSException* invalidHexException = [NSException exceptionWithName:@"InvalidHexException"
                                                                   reason:@"Hex color not three or six characters excluding hash"                                    
                                                                 userInfo:nil];
        @throw invalidHexException;
 
    }
 
}
 
- (NSString *) hexColor {
     if ([[self colorSpaceName] isEqualToString:NSCalibratedWhiteColorSpace]) {
        return [NSString stringWithFormat:@"#%0.2X%0.2X%0.2X",
            (int)(r * [self whiteComponent]),
            (int)(g * [self whiteComponent]),
            (int)(b * [self whiteComponent])];
    }
    else if ([[self colorSpaceName] isEqualToString:NSCalibratedBlackColorSpace]) {
        return [NSString stringWithFormat:@"#%0.2X%0.2X%0.2X",
            (int)(r * [self blackComponent]),
            (int)(g * [self blackComponent]),
            (int)(b * [self blackComponent])];
    }
    else if ([[self colorSpaceName] isEqualToString:NSCalibratedRGBColorSpace]
             || [[self colorSpaceName] isEqualToString:NSDeviceRGBColorSpace]) {
        return [NSString stringWithFormat:@"#%0.2X%0.2X%0.2X",
            (int)(r * [self redComponent]),
            (int)(g * [self blueComponent]),
            (int)(b * [self greenComponent])];
    }
    return @"transparent";
}
 
@end
Comments (3) | Trackback

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.

Comments (2) | Trackback

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
        }
    }
}
No comments | Trackback