First, you need to be able to become first responder, and be able to perform the actions of the menu items you want displayed. The menu controller sifts through its default set, and actions your provide and sees if any can be performed. If not, they get dropped on the floor. This assumes that the menu being shown only has "Duplicate".
- (BOOL) canPerformAction: (SEL) action withSender: (id) sender { return action == @selector(duplicate:); } // canPerform - (BOOL) canBecomeFirstResponder { return YES; } // canBecomeFirstResponerThen acquire an array of UIMenuItems (title / selector pairs), become first responder (that's important), and then figure out where you want the menu item to point to. Then show it:
// Permanent set of items. You might want to do something saner static NSArray *s_menuItems; if (s_menuItems == nil) { UIMenuItem *item = [[UIMenuItem alloc] initWithTitle: @"Duplicate" action: @selector(duplicate:)]; s_menuItems = [ @[ item ] retain]; [item release]; } [self becomeFirstResponder]; // important! // point the menu point somewhere CGPoint point = CGPointMake (160.0, 5.0); CGRect rect; rect.origin = point; rect.size = CGSizeMake (1.0, 1.0); UIMenuController *menu = [UIMenuController sharedMenuController]; menu.menuItems = s_menuItems; [menu setTargetRect: rect inView: self]; [menu setMenuVisible: YES animated: YES];If you have work to do when the menu disappears (such as deselecting a tableview cell), you can listen for
UIMenuControllerWillHideMenuNotification
or one of its kin.
NSOpenPanel *panel = [NSOpenPanel openPanel]; [panel setPrompt: @"Stuff for the Choose Button"]; [panel beginSheetForDirectory: nil file: nil types: [NSArray arrayWithObject: @"buf"] // or other file types modalForWindow: window modalDelegate: self didEndSelector: @selector(openPanelDidEnd:returnCode:contextInfo:) contextInfo: nil];and the didEndSelector implementation looks kinda like this:
- (void) openPanelDidEnd: (NSOpenPanel *) sheet returnCode: (int) returnCode contextInfo: (void *) context { if (returnCode == NSOKButton) { NSArray *fileNames = [sheet filenames]; NSLog (@"wooOOooot! %@", [fileNames objectAtIndex: 0]); } } // openPanelDidEnd
NSViewAnimation
lets you resize a window and cross-fade some views in one operation. (note I've had problems with the window size getting a couple of pixels off using this. Hopefully either I'm doing something dumb, or Apple fixes a bug). This code resizes the window, and changes the view that lives inside of a box. This is like how the Pages inspector works (except Pages doesn't do the gratuitous animation effect)
NSRect newWindowFrame = ... the new window size; NSDictionary *windowResize; windowResize = [NSDictionary dictionaryWithObjectsAndKeys: window, NSViewAnimationTargetKey, [NSValue valueWithRect: newWindowFrame], NSViewAnimationEndFrameKey, nil]; NSDictionary *oldFadeOut = nil; if (oldView != nil) { oldFadeOut = [NSDictionary dictionaryWithObjectsAndKeys: oldView, NSViewAnimationTargetKey, NSViewAnimationFadeOutEffect, NSViewAnimationEffectKey, nil]; } NSDictionary *newFadeIn; newFadeIn = [NSDictionary dictionaryWithObjectsAndKeys: newView, NSViewAnimationTargetKey, NSViewAnimationFadeInEffect, NSViewAnimationEffectKey, nil]; NSArray *animations; animations = [NSArray arrayWithObjects: windowResize, newFadeIn, oldFadeOut, nil]; NSViewAnimation *animation; animation = [[NSViewAnimation alloc] initWithViewAnimations: animations]; [animation setAnimationBlockingMode: NSAnimationBlocking]; [animation setDuration: 0.5]; // or however long you want it for [animation startAnimation]; // because it's blocking, once it returns, we're done [animation release];
NSView
's viewWithTag:
to find it.
Growl.framework
into your application bundle.
Pick a class to be the contact point with Growl. Your AppController
class is a good place. Import <Growl/Growl.h>
Set the delegate to the GrowlApplicationBridge
:
[GrowlApplicationBridge setGrowlDelegate: self];Doing this will eventually have the
registrationDictionaryForGrowl
delegate message called. Return a dictionary with two arrays (Which can be the same). These are the names of the alerts you will be posting. These are human-readable, so you'll want to use a localized string (which I've already set in the global variable here:
- (NSDictionary *) registrationDictionaryForGrowl { NSArray *notifications; notifications = [NSArray arrayWithObject: g_timesUpString]; NSDictionary *dict; dict = [NSDictionary dictionaryWithObjectsAndKeys: notifications, GROWL_NOTIFICATIONS_ALL, notifications, GROWL_NOTIFICATIONS_DEFAULT, nil]; return (dict); } // registrationDictionaryForGrowlAnd use this to post a notification:
[GrowlApplicationBridge notifyWithTitle: @"Woop! Time has expired!" description: @"You have been waiting for 37 minutes" notificationName: g_timesUpString iconData: nil priority: 0 isSticky: NO clickContext: nil];Consult the SDK documentation for more explanations of the features, but they are pretty self-explanitory.
NSSearchPathForDirectoriesInDomains
is how you find the location of things like Library directories, or User directories, document directory, and the like (this is the NSSearchPathDirectory
). The NSSearchPathDomainMask
is what domains to find things in. For instance for a NSLibraryDirectory
, a NSUserDomainMask
will give you the path to ~/Library
, NSSystemDomainMask
will give you the path to /System/Library
, and so on.
The directories inside of Library, like "Preferences" and "Application Support" are in English in the file system, and the Finder presents localized versions to the user. If you need ~/Library/Application Support/Borkware
, you can construct it like
NSMutableString *path; path = [[NSMutableString alloc] init]; // find /User/user-name/Library NSArray *directories; directories = NSSearchPathForDirectoriesInDomains (NSLibraryDirectory, NSUserDomainMask, YES); // if you had more than one user domain, you would walk directories and // work with each path [path appendString: [directories objectAtIndex: 0]]; [path appendString: @"/Application Support"]; [path appendString: @"/Borkware"];
- (void) drawGradientInRect: (CGRect) rect colorSpace: (CGColorSpaceRef) colorSpcae context: (CGContextRef) context { CGFloat startComponents[4] = { 254.0 / 255.0, 254.0 / 255.0, 254.0 / 255.0, 1.0 }; CGFloat endComponents[4] = { 206.0 / 255.0, 206.0 / 255.0, 206.0 / 255.0, 1.0 }; CGColorRef startColor = CGColorCreate (colorSpace, startComponents); CGColorRef endColor = CGColorCreate (colorSpace, endComponents); NSArray *array = [NSArray arrayWithObjects: (id)startColor, (id)endColor, nil]; CGGradientRef gradient = CGGradientCreateWithColors (colorSpace, (CFArrayRef)array, NULL); CGPoint endPoint = rect.origin; endPoint.y += rect.size.height; // Don't let the gradient bleed all over everything CGContextSaveGState (context); { CGContextClipToRect (context, rect); CGContextDrawLinearGradient (context, gradient, rect.origin, endPoint, 0); } CGContextRestoreGState (context); CGGradientRelease (gradient); CGColorRelease (startColor); CGColorRelease (endColor); } // drawGradientInRect
- (BOOL) tableView: (NSTableView *) tableView writeRowsWithIndexes: (NSIndexSet *) rowIndexes toPasteboard: (NSPasteboard *) pboard;in your table view data source (and don't forget to hook up the datasource if you use bindings to populate the tableview)
(Thanks to Rob Rix for updating this with more modern API)
NSArray *chunks = ... get an array, say by splitting it; string = [chunks componentsJoinedByString: @" :-) "];would produce something like
oop :-) ack :-) bork :-) greeble :-) ponies
NSString *string = @"oop:ack:bork:greeble:ponies"; NSArray *chunks = [string componentsSeparatedByString: @":"];
@implementation NSString (PasteboardGoodies) - (void) sendToPasteboard { [[NSPasteboard generalPasteboard] declareTypes: [NSArray arrayWithObject: NSStringPboardType] owner:nil]; [[NSPasteboard generalPasteboard] setString: self forType: NSStringPboardType]; } // sendToPasteboard @end // PasteboardGoodiesThanks to Dan Jalkut for this tip.
/usr/share/dict/words
or from a script, and you want to process them. In this case turning a bunch of strings into floats:
NSMutableArray *altitudes = [NSMutableArray array]; NSString *altitudeString = [self altitudeStringFromGoogle: coords]; [altitudeString enumerateLinesUsingBlock: ^(NSString *line, BOOL *stop) { float value = [line floatValue]; [altitudes addObject: [NSNumber numberWithFloat: value]]; }];
NSURL *mediaDirectory = ...; NSArray *contents = [fm contentsOfDirectoryAtURL: mediaDirectory includingPropertiesForKeys: @[] options: 0 error: &error]; if (contents == nil) { NSLog (@"could not contents of %@ - %@", mediaDirectory, error); return nil; }There's also
enumeratorAtURL:includingPropertiesForKeys:...
that gives you back a directory enumerator.
NSDictionary
, NSArray
,NSNumber
, NSString
, NSData
) as XML like this:
NSData *data; data = [NSPropertyListSerialization dataFromPropertyList: notes format: NSPropertyListXMLFormat_v1_0 errorDescription: nil];Then write out the data using
NSFileManager
, or whatever other mechanism you wish.
NSTask *task; task = [[NSTask alloc] init]; [task setLaunchPath: @"/bin/ls"]; NSArray *arguments; arguments = [NSArray arrayWithObjects: @"-l", @"-a", @"-t", nil]; [task setArguments: arguments]; NSPipe *pipe; pipe = [NSPipe pipe]; [task setStandardOutput: pipe]; NSFileHandle *file; file = [pipe fileHandleForReading]; [task launch]; NSData *data; data = [file readDataToEndOfFile]; NSString *string; string = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding]; NSLog (@"woop! got\n%@", string);Of course, you can use different
NSFileHandle
methods for different styles of reading, and you can make a pipe for standard input so you can feed data to the task.
NSTasks
and a bunch of NSPipes
and hook them together, or you can use the "sh -c
" trick to feed a shell a command, and let it parse it and set up all the IPC. This pipeline cats /usr/share/dict/words, finds all the words with 'ham' in them, reverses them, and shows you the last 5.
NSTask *task; task = [[NSTask alloc] init]; [task setLaunchPath: @"/bin/sh"]; NSArray *arguments; arguments = [NSArray arrayWithObjects: @"-c", @"cat /usr/share/dict/words | grep -i ham | rev | tail -5", nil]; [task setArguments: arguments]; // and then do all the other jazz for running an NSTask.One important note:, don't feed in arbitrary user data using this trick, since they could sneak in extra commands you probably don't want to execute.
@property (strong, nonatomic) IBOutletCollection(XXLaneView) NSArray *laneViews;
In the header file
#import <Cocoa/Cocoa.h> @interface BWSearchArrayController : NSArrayController { NSString *searchString; } - (IBAction) search: (id) sender; @end // BWSearchArrayControllerand then in the implementation:
// returns an array containing the content of the objects arranged // with the user's critera entered into the search box thingie - (NSArray *) arrangeObjects: (NSArray *) objects { // result of the filtering NSArray *returnObjects = objects; // if there is a search string, use it to compare with the // search field string if (searchString != nil) { // where to store the filtered NSMutableArray *filteredObjects; filteredObjects = [NSMutableArray arrayWithCapacity: [objects count]]; // walk the enumerator NSEnumerator *enumerator = [objects objectEnumerator]; id item; // actully BWFileEntries while (item = [enumerator nextObject]) { // get the filename from the entry NSString *filename; filename = [item valueForKeyPath: @"fileName"]; // see if the file name matches the search string NSRange range; range = [filename rangeOfString: searchString options: NSCaseInsensitiveSearch]; // found the search string in the file name, add it to // the result set if (range.location != NSNotFound) { [filteredObjects addObject: item]; } } returnObjects = filteredObjects; } // have the superclass arrange them too, to pick up NSTableView sorting return ([super arrangeObjects:returnObjects]); } // arrangeObjectsand then to set the search string:
- (void) setSearchString: (NSString *) string { [searchString release]; if ([string length] == 0) { searchString = nil; } else { searchString = [string copy]; } } // setSearchString - (void) search: (id) sender { [self setSearchString: [sender stringValue]]; [self rearrangeObjects]; } // search
observeValueForKeyPath:ofObject:change:context:
, (or you do override it but don't handle your callbacks) you'll generally get an exception.
This trains you to do it right and never call super for the callbacks you're handling, or you'll get an exception.
Except
if you've added KVO to a subclass of NSArrayController
(and possibly all controller types), and don't call super's observeValueForKeyPath:ofObject:change:context:
, bindings won't work at all, with no warning/notice/nothing. (Courtesy of Duncan Wilcox)
- (NSArray *) layers { return (layers); } // layers - (void) insertObject: (id) obj inLayersAtIndex: (unsigned) index { [layers insertObject: obj atIndex: index]; } // insertObjectInLayers - (void) removeObjectFromLayersAtIndex: (unsigned) index { [layers removeObjectAtIndex: index]; } // removeObjectFromLayersAtIndex
In Interface Builder, drag over the NSSearchField, and bind the predicate
like this:
what contains[c] $value
(assuming the attribute you're filtering is called what
)
First, in the +initialize for the class that's going to be observed:
+ (void) initialize { NSArray *keys; keys = [NSArray arrayWithObjects: @"showMajorLines", @"minorWeight", @"minorColor", @"minorWeightIndex", @"minorColorIndex", @"majorWeightIndex", @"majorColorIndex", nil]; [BWGridAttributes setKeys: keys triggerChangeNotificationsForDependentKey: @"gridAttributeChange"]; } // initializeSo now when "showMajorLines" changes, "gridAttributeChange" will also be observed. KVO requires there actually must exist a gridAttributeChange method (or ivar I presume) before it'll do the notification to observing objects, so there needs to be a do-nothing method:
- (BOOL) gridAttributeChange { return (YES); } // gridAttributeChangeSo now the view can do this:
- (void) setGridAttributes: (BWGridAttributes *) a { [attributes removeObserver: self forKeyPath: @"gridAttributeChange"]; [a retain]; [attributes release]; attributes = a; [a addObserver: self forKeyPath: @"gridAttributeChange" options: NSKeyValueObservingOptionNew context: NULL]; } // setGridAttributesAnd will get updated whenever an individual attribute changes.
NSSortDescriptor *sorter; sorter = [[NSSortDescriptor alloc] initWithKey: @"when" ascending: YES]; [fetchRequest setSortDescriptors: [NSArray arrayWithObject: sortDescriptor]];
- (void) setWhenSortDescriptors: (NSArray *) descriptors { } // setWhenSortDescriptors - (NSArray *) whenSortDescriptors { NSSortDescriptor *sorter; sorter = [[[NSSortDescriptor alloc] initWithKey: @"when" ascending: NO] autorelease]; return ([NSArray arrayWithObject: sorter]); } // whenSortDescriptorsThis is for a 'permanent' sorting, not allowing the user to change the sorting. Also, new/changed objects added to the collection don't appear to be placed in sorted order.
NSMutableArray *sortedArray = [NSMutableArray arrayWithObjects: @"Alice", @"Beth", @"Carol",@"Ellen",nil]; //Where is "Beth"? unsigned index = (unsigned)CFArrayBSearchValues((CFArrayRef)sortedArray, CFRangeMake(0, CFArrayGetCount((CFArrayRef)sortedArray)), (CFStringRef)@"Beth", (CFComparatorFunction)CFStringCompare, NULL); if (index < [sortedArray count] && [@"Beth" isEqualToString:[sortedArray[index]]) { NSLog(@"Beth was found at index %u", index); } else { NSLog(@"Beth was not found (index is beyond the bounds of sortedArray)"); } //Where should we insert "Debra"? unsigned insertIndex = (unsigned)CFArrayBSearchValues((CFArrayRef)sortedArray, CFRangeMake(0, CFArrayGetCount((CFArrayRef)sortedArray)), (CFStringRef)@"Debra", (CFComparatorFunction)CFStringCompare, NULL); [sortedArray insertObject:@"Debra" atIndex:insertIndex]; NSLog([sortedArray description]); //note: NSArray indices and counts were typed as unsigned. With the move to 64-bit, they are NSUInteger. //CFArray indices and counts are CFIndex, which was SInt32 but also will move to 64-bit? //Why was it ever signed and will it remain so?Muchos Thankos to James Hober for this one.
Gus Mueller chimed in saying that if you use CFArrayBSearchValues
, be sure to sort with CFArraySortValues
rather than using the Cocoa sorting routines (or at least the comparison) - they treat diacritic marks differently, leading to strange errors. From Gus, a quick objc addition to NSMutableArray:
- (void) cfStringSort { CFArraySortValues((CFMutableArrayRef)self, CFRangeMake(0, [self count]), (CFComparatorFunction)CFStringCompare, NULL); }
Thanks to Preston Jackson for finding a bug in the original implementation. In the case of "not found", it returns the insertion point to put this item, so if you're interested in "is this thing there", you need to compare to what was found.
NSPredicate
and -filteredArrayUsingPredicate:
to selectively remove stuff from an array.
NSPredicate *hasZone = [NSPredicate predicateWithBlock: ^BOOL (id obj, NSDictionary *bindings) { GRProfileCueTime *cueTime = obj; return cueTime.cue.targetHeartZone != kZoneNone; }]; NSArray *justZones = [cues filteredArrayUsingPredicate: hasZone];This removes all of the elements of the array that do not have a target heart zone.
If this kind of stuff is useful for you, you might want to check out Mike Ash's collection utilities.
UIButton *button = [UIButton buttonWithType: UIButtonTypeInfoLight]; [button addTarget: self action: @selector(about:) forControlEvents: UIControlEventTouchUpInside]; UIBarButtonItem *infoItem = [[UIBarButtonItem alloc] initWithCustomView: button]; ... make any other button items you want NSArray *rightButtonItems = @[ spacer, infoItem, widerSpace, someOtherItem ]; _viewController.navigationItem.rightBarButtonItems = rightButtonItems;
And then override these. In this case, I split up an array into an array of arrays, each sub-array being the contents of a section that corresponds to a string to display in the index. _tableIndex
is an array of strings for display of the index.
- (NSArray *) sectionIndexTitlesForTableView: (UITableView *) tableView { return _tableIndex; } // sectionIndexTitles - (NSInteger) tableView: (UITableView *) tableView sectionForSectionIndexTitle: (NSString *) title atIndex: (NSInteger) index { return index; } // sectionForSectionIndexTitle
- (void) tableView: (UITableView *) tableView commitEditingStyle: (UITableViewCellEditingStyle) editingStyle forRowAtIndexPath: (NSIndexPath *) indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { [_cues removeObjectAtIndex: indexPath.row]; // manipulate your data structure. [tableView deleteRowsAtIndexPaths: [NSArray arrayWithObject: indexPath] withRowAnimation: UITableViewRowAnimationFade]; [self updateUI]; // Do whatever other UI updating you need to do. } } // commitEditingStyle
-reloadData
, but don't. That's an awfully big hammer. Instead just reload a row
NSIndexPath *indexPath = [NSIndexPath indexPathForRow: row inSection: 0]; NSArray *array = [NSArray arrayWithObject: indexPath]; [_playlistTableView reloadRowsAtIndexPaths: array withRowAnimation: UITableViewRowAnimationNone];
- (NSString *) tableView: (UITableView *) tableview titleForHeaderInSection: (NSInteger) section { NSArray *sections = [BWTermStorage sections]; return [sections objectAtIndex: section]; } // titleForHeaderInSectionYou can return a title for a footer.
You can also return a view:
- (UIView *) tableView: (UITableView *) tableView viewForHeaderInSection: (NSInteger) section;
#import "CJSONDeserializer.h" NSData *jsonData = [NSData dataWithContentsOfFile: path]; // or from network, or whatever NSError *error; NSArray *playlists = [[CJSONDeserializer deserializer] deserializeAsArray: jsonData error: &error];
NSFileManager *fm = [NSFileManager defaultManager]; NSURL *ubiquitousFolder = [fm URLForUbiquityContainerIdentifier:nil]; NSLog (@"Location of ubiquitous folder: %@", ubiquitousFolder); // Then use standard ways of iterating, given a URL, such as NSArray *contents = [fm contentsOfDirectoryAtURL: ubiquitousFolder includingPropertiesForKeys: @[] options: 0 error: &error]; if (contents == nil) { NSLog (@"could not contents of %@ - %@", mediaDirectory, error); }