TweetFollow Us on Twitter

Doing Objects Right

Volume Number: 14 (1998)
Issue Number: 3
Column Tag: Rhapsody

Doing Objects Right

by Andrew C. Stone

Using modular objects with multiple nib files to make evolving your projects easier

One of the most compelling features of writing software is that there are many ways to accomplish the same task. This gives you a large latitude for creativity, but also "the power to run off into the weeds." (I overheard an Apple Engineer using this phrase.) In this article I present some guidelines for creating usable and reusable objects, and provide source for a search and replace panel.

Our Rhapsody-based object draw and web authoring application, Create(tm), has 550 classes, and about 100 user-interface nib (NeXT InterfaceBuilder) files. This highly modular structure makes changing one component trivial and speedy. Because the nib files are loaded only when needed, it also speeds application launching.

Figure 1.

There is always a temptation to add objects directly to your main nib file because its easy to make object connections. But this bloats the main nib and causes the app to take longer to launch. Moreover, it makes multiple documents almost impossible because sometimes you need more than one instance. You also may want to take advantage of loading the nib files only when needed. This article will show you how to write an object with its own independent interface file, and how to write the glue needed to have a menu item bring up that interface. Code is included for a universal text find and replace object, "TextFinder", which can be added to the simple Word Processor from the November 1997 issue of MacTech.

sWord
The entire source of sWord, our simple rich text & graphics word processor.

#import <AppKit/AppKit.h>
@interface WordDelegate : NSObject
{
  id theText;
}
- (void)newText:(id)sender;
- (void)openText:(id)sender;
- (void)saveText:(id)sender;
@end

#import "WordDelegate.h"
@implementation WordDelegate
- (void)newText:(id)sender
{
  [theText setString:@""];
}
- (void)openText:(id)sender
{
  NSOpenPanel *openPanel = [NSOpenPanel openPanel];
  if ([openPanel runModalForTypes:[NSArray arrayWithObjects:@"rtf",@"rtfd",NULL]]) {
      [theText readRTFDFromFile:[openPanel filename]];
  }
}
- (void)saveText:(id)sender
{
  NSSavePanel *savePanel = [NSSavePanel savePanel];
  [savePanel setRequiredFileType:@"rtfd"];
  if ([savePanel runModal]) {
    [theText writeRTFDToFile:[savePanel filename] atomically:NO];
  }
}
@end

Tips and Techniques

This article won't go into style issues -- that's a topic for holy wars! However, here are some basic guidelines for developing stand-alone objects that are truly reusable:

1. Every nib file should have an owner object to which you say "+ new:"

This means that a client need know only the object's class name presenting a simple calling interface. By separating the details of the class (such as the nib name) from its use, you obtain a cushion from changes to the object. Then your client code looks like this:

id aCoolObject = [CoolObject new:(NSZone *)zone];

Note that the client determines the memory allocation zone, the NSZone, in which to create the new object by passing it as an argument. You can always pass in "NSDefaultMallocZone()", a function which returns the default memory allocation zone, or "[self zone]", which returns the zone of the calling object.

In our CoolObject's + new: method, we have

+ new:(NSZone *)zone
{
  self = [[CoolObject allocWithZone:zone] init];
  return self;  /* don't ever forget this! */
}

In its -init method, we load the user interface file:

- init
{
  [super init];
  [NSBundle loadNibNamed:@"CoolObject.nib" owner:self];
  /* place initialization code here:*/

  return self;  /* don't ever forget this! */
}

Many objects require only one instance per class. For example, Create uses just one TextFinder object, which brings up the same panel each time. For objects like these, it is more appropriate to create a class method named + sharedInstance, which might look like this:

+ (id)sharedInstance {
  // subclasses need their own instance if both classes are needed:
  static id sharedFindObject = nil;
  // get the real McCoy the first time through:
  if (!sharedFindObject) {
    sharedFindObject = [[self allocWithZone:
            [[NSApplication sharedApplication] zone]] init];
  }
  return sharedFindObject;
}

2. Name your nib file the same as the owner's class name

For each object which has a visual representation, your project directory will have three associated files: the .h, .m, and .nib. (If the nib file is localized, it will reside in English.proj, German.proj, French.proj, etc.)

If the owner's class name coincides with the nib file name, the following generic code will load a nib file based on that class name, using the NSStringFromClass() function:

#import <AppKit/AppKit.h>   /* Everything you need */

- init
{
  // Continue the designated initializer chain:
  [super init];
  // here's a fuller invocation of "loadNibNamed:" which shows the loading of the
  // dictionary with the key-value pair NSOwner, which has a value of "self".
  [NSBundle loadNibFile:[[NSBundle mainBundle] 
    pathForResource:NSStringFromClass([self class]) 
    ofType:@"nib"]
    externalNameTable:[NSDictionary 
      dictionaryWithObjectsAndKeys:self, @"NSOwner", nil]
    withZone:[self zone]];

  // place other initialization code here
  return self;
}

By making our object a subclass of an object which uses this code to load a nib, we never even have to even write a new line of code -- the nib with the name of our subclass will be loaded automatically.

Apt class naming is one of the most important aspects of creating comprehensible, not reprehensible, code. The name should clearly and concisely describe the object's function. When my custom class is a subclass of an NSObject, I like to include the superclass name in my class name. For example, SliderDualActing descends from NSSlider. Usually, nib owners will descend from NSObject, so they can have more succinct descriptive names, such as AlignPanel, TextFinder, or OpenAccessory.

3. Use the power of Objective C

We would like any text object to be able to use our TextFinder's search and replace functionality, not just our own custom subclasses. Objective C allows us use categories to add methods to existing classes. We can extend NSTextView with a category TextFinderMethods, which contains the search and replace methods. Then any NSTextView in our application will be able to respond to methods like findNext: or findPrevious:.

One note of caution about categories: if you add multiple categories to a class and define a method in more than one category, which method will be used at runtime is undeterminable. Be sure to use categories carefully. Someday categories may be thought of as the Object Oriented GOTO, but they reveal the power of a dynamic runtime system. The full set of methods that we extend the NSTextView class are defined in "NSTextViewTextFinder.m".

Objective C also provides subclassing, which allows us to reuse classes by modifying their functionality to fit specific applications. For example, in specific text objects, we might want to provide the capability to use regular expressions in our search strings. We could subclass TextFinder and modify a few of its methods without having to rework the whole object.

@implementation NSTextView(TextFinderMethods)

- (void)orderFrontFindPanel:(id)sender {
  // no variable is used - instead, we grab the sharedInstance:
  [[TextFinder sharedInstance] orderFrontFindPanel:sender];
}

4. Use the power of the AppKit

Your interface depends on being able to cause various controls (buttons, menu items) to trigger actions in your code. This is easy with the TextFinder object and the TextFinder nib file. We easily can create the necessary connections in the Interface Builder, but now that you've followed my advice to use modular design and have created many individual nib files, how do you connect the menu items defined in the main nib file to targets in other nib files? How do you connect menu items for finding text to the methods defined in the TextFinderMethods category?

The solution is the use of the AppKit's "First Responder" hierarchy.

Figure 2.

In AppKit programs, if a menu item is connected to the "First Responder" stand-in object, then when the menu item is clicked, it sends its message up a hierarchy until it reaches an object which responds to that method. If no object in the hierarchy responds to that message, the menu item automatically will be disabled. Each NSWindow in your application keeps track of which object in its view hierarchy has first responder status. This object gets the first chance to handle messages sent to First Responder. From there, the message is passed to the first responder's superview, through the view hierarchy to the window and then to the window's delegate. If the message has not yet been handled, it then goes to the NSApplication and finally to the NSApplication's delegate.

So, all you have to do is add the method's name (also called "action") to your main nib's First Responder, and connect the menu item to that action. The rest is done automatically by the AppKit objects and the runtime system. Full instructions on adding the TextFinder to an application are the next section of this article.

5. Document the object

Document what your object does and how it should be called. If you commented as you went, the documentation is mostly written. Make your API understandable by clearly explaining instance variables and methods.

6. Don't Panic

I guess this belongs in every list of guidelines! Happy Hacking.

Adding the TextFinder to Your Application

Adding the TextFinder to an existing project, like the simple word processor we built in November 1997, is as easy as adding the TextFinder.subproj to your project, adding the more complete Edit menu available in Interface Builder's Menu palette, and then connecting these menu items with the method names that we have added to the NSTextView. Here's a step-by-step guide:

  1. Download or type in the TextFinder.subproj files.
  2. Open the pWord PB.project file in ProjectBuilder.
  3. Double-Click "Subprojects" which brings up a Open Panel.
  4. Select the "TextFinder.subproj" to add this subproject.

    Figure 3.

  5. Double-Click Interfaces->sWord.nib to launch InterfaceBuilder and load the main nib file.
  6. Select the "Edit" menu item, and choose Delete.

    Figure 4.

  7. Choose the "Menu" section of IB's Palette.
  8. Drag over the "Edit" menu item onto the main menu.

    Figure 5.

    Figure 6.

  9. Now, we must add the new methods that the NSTextView understands to the First Responder stand in object. These are the methods we defined in NSTextViewTextFinder.m such as orderFrontFindPanel:, findNext:, findPrevious:, jumpToSelection:, and scrollToSelection:.
    1. Double-click the First Responder icon to load the Classes subpanel.

      Figure 7.

    2. Click the crossed "Action" icon to reveal the list of actions understood by the First Responder.

      Figure 8.

    3. Choose Classes->New Action, or Return to open a new, untitled action.

      Figure 9.

    4. Rename "myAction:" to, e.g., "orderFrontFindPanel:"
    5. Repeat a-d for each of the other actions.

      Figure 10.

    6. Save your interface file to update the First Responder completely.
  10. Connect the menu items to their corresponding First Responder action by control-dragging from the menu item to the First Responder icon, and then selecting the correct action in IB's.

    Figure 11.

  11. Recompile, and you are done! Type in some words and try out the find/replace.

The Code

The TextFinder.subproj contains TextFinder.h in Headers, TextFinder.m in Classes, NSTextViewTextFinder.m in Other Sources, and TextFinder.nib in Interfaces.

NSTextViewTextFinder.m
This is the glue which makes every text object able to do search and replace. 
These methods extend the original functionality of the NSTextView in order to 
talk to our TextFinder object. Now, you can add the complete "Edit" menu in 
InterfaceBuilder which contains the Find submenu, and it will just work....

#import <AppKit/AppKit.h>
#import "TextFinder.h"

@implementation NSTextView(TextFinderMethods)

- (void)orderFrontFindPanel:(id)sender {
  [[TextFinder sharedInstance] orderFrontFindPanel:sender];
}

- (void)findNext:(id)sender {
  [[TextFinder sharedInstance] findNext:sender];
}
- (void)findPrevious:(id)sender {
  [[TextFinder sharedInstance] findPrevious:sender];
}

- (void)enterSelection:(id)sender {
  NSRange range = [self selectedRange];
  if (range.length) {
    [[TextFinder sharedInstance] setFindString:[[self string] substringWithRange:range]];
  } else {
    NSBeep();
  }
}

- (void)jumpToSelection:(id)sender {
  [self scrollRangeToVisible:[self selectedRange]];
}

- (void)doFindSelection:sender
{
  [self enterSelection:self];
}

@end

TextFinder.h
#import <AppKit/AppKit.h>

#define Forward YES
#define Backward NO

@interface TextFinder : NSObject {
  NSString *findString;
  id findTextField;
  id replaceTextField;
  id ignoreCaseButton;
  id findNextButton;
  id replaceAllScopeMatrix;
  id statusField;
  BOOL findStringChangedSinceLastPasteboardUpdate;
  BOOL lastFindWasSuccessful;
}

/* Common way to get a text finder. One instance of TextFinder per app is 
good enough. */
+ (id)sharedInstance;

/*  Main method for external users; does a find in the first responder. 
  Selects found range or beeps. */
- (BOOL)find:(BOOL)direction;

/* Loads UI lazily */
- (NSPanel *)findPanel;
/* Gets the first responder and returns it if it's an NSTextView */
- (NSTextView *)textObjectToSearchIn;
/* Get/set the current find string. Will update UI if UI is loaded */
- (NSString *)findString;
- (void)setFindString:(NSString *)string;
/* Misc internal methods */
- (void)appDidActivate:(NSNotification *)notification;
- (void)addWillDeactivate:(NSNotification *)notification;
- (void)loadFindStringFromPasteboard;
- (void)loadFindStringToPasteboard;

/* Methods sent from the find panel UI */
- (void)findNext:(id)sender;
- (void)findPrevious:(id)sender;
- (void)findNextAndOrderFindPanelOut:(id)sender;
- (void)replace:(id)sender;
- (void)replaceAndFind:(id)sender;
- (void)replaceAll:(id)sender;
- (void)orderFrontFindPanel:(id)sender;

@end

@interface NSString (NSStringTextFinding)

- (NSRange)findString:(NSString *)string 
    selectedRange:(NSRange)selectedRange 
    options:(unsigned)mask wrap:(BOOL)wrapFlag;

@end
    

TextFinder.m
Generic Find/Replace functionality for text. Uses new text API.You may freely copy, 
distribute and reuse the code in this example. NeXT disclaims any warranty of 
any kind, expressed or implied, as to its fitness for any particular use.

#import <AppKit/AppKit.h>
#import "TextFinder.h"

@implementation TextFinder

- (id)init {
  // if there are memory allocation problems, we bail and return nil: 
  if (!(self = [super init])) return nil;

  // in order share find strings among applications, 
  // we'll register for notifications when the app activates or deactivates: 
  [[NSNotificationCenter defaultCenter] 
    addObserver:self selector:@selector(appDidActivate:) 
    name:NSApplicationDidBecomeActiveNotification 
    object:[NSApplication sharedApplication]];
  [[NSNotificationCenter defaultCenter] addObserver:self 
    selector:@selector(addWillDeactivate:) 
    name:NSApplicationWillResignActiveNotification 
    object:[NSApplication sharedApplication]];

  // initialize ourselves to the empty string: 
  [self setFindString:@""];
  
  // here we grab the last used findstring from other apps: 
  [self loadFindStringFromPasteboard];
  return self;
}

// these are the methods called whenever we get an activate or deactivate 
// notification: 

- (void)appDidActivate:(NSNotification *)notification {
  [self loadFindStringFromPasteboard];
}

- (void)addWillDeactivate:(NSNotification *)notification {
  [self loadFindStringToPasteboard];
}

// and here is the workhorse code for sharing the findstrings among apps: 

- (void)loadFindStringFromPasteboard {
  NSPasteboard *pasteboard = [NSPasteboard 
                    pasteboardWithName:NSFindPboard];
  if ([[pasteboard types] containsObject:NSStringPboardType]) 
  {
    NSString *string = [pasteboard 
      stringForType:NSStringPboardType];
    if (string && [string length]) {
      [self setFindString:string];
      findStringChangedSinceLastPasteboardUpdate = NO;
    }
  }
}

- (void)loadFindStringToPasteboard {
  NSPasteboard *pasteboard = [NSPasteboard 
                    pasteboardWithName:NSFindPboard];
  if (findStringChangedSinceLastPasteboardUpdate) {
    [pasteboard declareTypes:[NSArray 
      arrayWithObject:NSStringPboardType] owner:nil];
    [pasteboard setString:[self findString] 
      forType:NSStringPboardType];
    findStringChangedSinceLastPasteboardUpdate = NO;
  }
}

// Only one of the TextFinder objects is ever required: 
static id sharedFindObject = nil;

+ (id)sharedInstance {
  if (!sharedFindObject) {
    sharedFindObject = [[self allocWithZone:[[NSApplication 
                          sharedApplication] zone]] init];
  }
  return sharedFindObject;
}

- (void)loadUI {
  // we check to see if the findTextField ivar is nil, if so, we load the nib: 
  if (!findTextField) {
    if (![NSBundle loadNibNamed:@"TextFinder" owner:self]) {
      NSLog(@"Failed to load TextFinder.nib");
      NSBeep();
    }
    // here we automatically remember the user's last location of the find panel: 
    if (self == sharedFindObject) 
      [[findTextField window] setFrameAutosaveName:@"Find"];
  }
  // now update the search string: 
  [findTextField setStringValue:[self findString]];
}

- (void)dealloc {
  // don't litter 
  if (self != sharedFindObject) {
    [findString release];
    [super dealloc];
  }
}

- (NSString *)findString {
  return findString;
}

- (void)setFindString:(NSString *)string {
  // only change if different: 
  if ([string isEqualToString:findString]) return;
  // careful memory management is what makes a good programmer! 
  [findString autorelease];
  // keep a copy around: 
  findString = [string copyWithZone:[self zone]];
  if (findTextField) {
    [findTextField setStringValue:string];
    [findTextField selectText:nil];
  }
  // here we note that we haven't set the global pasteboard string yet: 
  findStringChangedSinceLastPasteboardUpdate = YES;
}

// this method tries to find the NSText object that is active
// it will return nil if none is active: 

- (NSTextView *)textObjectToSearchIn {
  id obj = [[NSApp mainWindow] firstResponder];
  return (obj && [obj isKindOfClass:[NSText class]]) 
            ? obj : nil;
}
- (NSPanel *)findPanel {
  if (!findTextField) [self loadUI];
  return (NSPanel *)[findTextField window];
}

/* The primitive for finding; this ends up setting the status field (and beeping if 
necessary)... */
- (BOOL)find:(BOOL)direction {
  NSTextView *text = [self textObjectToSearchIn];

  lastFindWasSuccessful = NO;
  if (text) {
    NSString *textContents = [text string];
    unsigned textLength;
    if (textContents && (textLength = [textContents length])) {
      NSRange range;
      unsigned options = 0;
_  if (direction == Backward) options |= NSBackwardsSearch;
      if ([ignoreCaseButton state]) 
        options |= NSCaseInsensitiveSearch;
      range = [textContents findString:[self findString] 
                selectedRange:[text selectedRange] 
                options:options wrap:YES];
      if (range.length) {
        [text setSelectedRange:range];
        [text scrollRangeToVisible:range];
        lastFindWasSuccessful = YES;
      }
    }
  }
  if (!lastFindWasSuccessful) {
    NSBeep();
    [statusField setStringValue:NSLocalizedStringFromTable(
      @"Not found", @"FindPanel", 
      @"Status displayed in find panel when the find string \
      is not found.")];
  } else {
    [statusField setStringValue:@""];
  }
  return lastFindWasSuccessful;
}

- (void)orderFrontFindPanel:(id)sender {
  NSPanel *panel = [self findPanel];
  [findTextField selectText:nil];
  [panel makeKeyAndOrderFront:nil];
}

/** * ** * Action methods for gadgets in the find panel; these should all end up 
setting or clearing the status field ** * ** * /

- (void)findNextAndOrderFindPanelOut:(id)sender {
  [findNextButton performClick:nil];
  if (lastFindWasSuccessful) {
    [[self findPanel] orderOut:sender];
  } else {
    [findTextField selectText:nil];
  }
}

- (void)findNext:(id)sender {
  if (findTextField) 
    /* findTextField should be set */
    [self setFindString:[findTextField stringValue]];
  (void)[self find:Forward];
}

- (void)findPrevious:(id)sender {
  if (findTextField) 
    /* findTextField should be set */
    [self setFindString:[findTextField stringValue]];
  (void)[self find:Backward];
}

- (void)replace:(id)sender {
  NSTextView *text = [self textObjectToSearchIn];
  if (!text) {
    NSBeep();
  } else {
    [[text textStorage] 
      replaceCharactersInRange:[text selectedRange] 
      withString:[replaceTextField stringValue]];
    [text didChangeText];
  }
  [statusField setStringValue:@""];
}

- (void)replaceAndFind:(id)sender {
  [self replace:sender];
  [self findNext:sender];
}
#define ReplaceAllScopeEntireFile 42
#define ReplaceAllScopeSelection 43

- (void)replaceAll:(id)sender {

  NSTextView *text = [self textObjectToSearchIn];

  if (!text) {
    NSBeep();
  } else {
    NSString *textContents = [text string];
    BOOL entireFile = replaceAllScopeMatrix 
          ? ([replaceAllScopeMatrix selectedTag] 
            == ReplaceAllScopeEntireFile) : YES;
    NSRange replaceRange = entireFile 
          ? NSMakeRange(0, [[text textStorage] length]) 
          : [text selectedRange];
    unsigned options = NSBackwardsSearch 
          | ([ignoreCaseButton state] 
          ? NSCaseInsensitiveSearch : 0);
    unsigned replaced = 0;
    if (findTextField) 
      [self setFindString:[findTextField stringValue]];

    [[text textStorage] beginEditing];
    while (1) {
      NSRange foundRange = [textContents 
              rangeOfString:[self findString] 
              options:options range:replaceRange];
      if (foundRange.length == 0) break;
      replaced++;
      [[text textStorage] replaceCharactersInRange:foundRange 
        withString:[replaceTextField stringValue]];
      replaceRange.length 
        = foundRange.location - replaceRange.location;
    }
    [[text textStorage] endEditing];
    [text didChangeText];
    if (replaced == 0) {
      NSBeep();
      [statusField setStringValue:NSLocalizedStringFromTable(
        @"Not found", @"FindPanel", 
        @"Status displayed in find panel when the find \
        string is not found.")];
    } else {
      [statusField setStringValue:[NSString   
        localizedStringWithFormat: 
        NSLocalizedStringFromTable(@"%d replaced", 
          @"FindPanel", @"Status displayed in find panel \
          when indicated number of matches are replaced."), 
          replaced]];
    }
  }
}

@end

@interface NSString (StringTextFinding)
- (NSRange)findString:(NSString *)string 
    selectedRange:(NSRange)selectedRange 
    options:(unsigned)options wrap:(BOOL)wrap;

@end

@implementation NSString (StringTextFinding)

- (NSRange)findString:(NSString *)string 
    selectedRange:(NSRange)selectedRange 
    options:(unsigned)options wrap:(BOOL)wrap {
  BOOL forwards = (options & NSBackwardsSearch) == 0;
  unsigned length = [self length];
  NSRange searchRange, range;

  if (forwards) {
    searchRange.location = NSMaxRange(selectedRange);
    searchRange.length = length - searchRange.location;
    range = [self rangeOfString:string options:options 
              range:searchRange];
    if ((range.length == 0) && wrap) {
      /* If not found look at the first part of the string */
      searchRange.location = 0;
      searchRange.length = selectedRange.location;
      range = [self rangeOfString:string options:options 
                range:searchRange];
    }
  } else {
    searchRange.location = 0;
    searchRange.length = selectedRange.location;
    range = [self rangeOfString:string options:options 
              range:searchRange];
    if ((range.length == 0) && wrap) {
      searchRange.location = NSMaxRange(selectedRange);
      searchRange.length = length - searchRange.location;
      range = [self rangeOfString:string options:options 
                range:searchRange];
    }
  }
  return range;
}

@end

Andrew Stone, an early HyperTalk developer and coauthor of "Tricks of the HyperTalk Masters" emigrated to the NEXT community in 1989, going on to write such NeXT classics as TextArt, Create, DataPhile and 3Dreality.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Spotify 1.0.44.100. - Stream music, crea...
Spotify is a streaming music service that gives you on-demand access to millions of songs. Whether you like driving rock, silky R&B, or grandiose classical music, Spotify's massive catalogue puts... Read more
Microsoft OneNote 15.29 - Free digital n...
OneNote is your very own digital notebook. With OneNote, you can capture that flash of genius, that moment of inspiration, or that list of errands that's too important to forget. Whether you're at... Read more
WALTR 2 2.0.8 - $39.95
WALTR 2 helps you wirelessly drag-and-drop any music, ringtones, videos, PDF, and ePub files onto your iPhone, iPad, or iPod without iTunes. It is the second major version of Softorino's critically-... Read more
Dropbox 16.3.27 - Cloud backup and synch...
Dropbox is an application that creates a special Finder folder that automatically syncs online and between your computers. It allows you to both backup files and keep them up-to-date between systems... Read more
EtreCheck 3.1.5 - For troubleshooting yo...
EtreCheck is an app that displays the important details of your system configuration and allow you to copy that information to the Clipboard. It is meant to be used with Apple Support Communities to... Read more
Carbon Copy Cloner 4.1.12 - Easy-to-use...
Carbon Copy Cloner backups are better than ordinary backups. Suppose the unthinkable happens while you're under deadline to finish a project: your Mac is unresponsive and all you hear is an ominous,... Read more
VueScan 9.5.62 - Scanner software with a...
VueScan is a scanning program that works with most high-quality flatbed and film scanners to produce scans that have excellent color fidelity and color balance. VueScan is easy to use, and has... Read more
SpamSieve 2.9.27 - Robust spam filter fo...
SpamSieve is a robust spam filter for major email clients that uses powerful Bayesian spam filtering. SpamSieve understands what your spam looks like in order to block it all, but also learns what... Read more
Fantastical 2.3.2 - Create calendar even...
Fantastical 2 is the Mac calendar you'll actually enjoy using. Creating an event with Fantastical is quick, easy, and fun: Open Fantastical with a single click or keystroke Type in your event... Read more
PCalc 4.4.4 - Full-featured scientific c...
PCalc is a full-featured, scriptable scientific calculator with support for hexadecimal, octal, and binary calculations, as well as an RPN mode, programmable functions, and an extensive set of unit... Read more

Latest Forum Discussions

See All

Track Santa with these three festive app...
Christmas is fast approaching and that means it's time to prepare for Santa's yearly pilgrimage around the globe. Christmas Eve is an exciting time as parents help their kids get ready to welcome Santa. You've got the cookies and milk all planned... | Read more »
Galaxy on Fire 3 and four other fantasti...
Galaxy on Fire 3 - Manticore brings the series back for another round of daring space battles. It's familiar territory for folks who are familiar with the franchise. If you've beaten the game and are looking to broaden your horizons, might we... | Read more »
The best apps for your holiday gift exch...
What's that, you say? You still haven't started your holiday shopping? Don't beat yourself up over it -- a lot of people have been putting it off, too. It's become easier and easier to procrastinate gift shopping thanks to a number of apps that... | Read more »
Toca Hair Salon 3 (Education)
Toca Hair Salon 3 1.0 Device: iOS Universal Category: Education Price: $2.99, Version: 1.0 (iTunes) Description: | Read more »
Winter comes to Darkwood as Seekers Note...
MyTona, based in the chilly Siberian city of Yakutsk, has brought a little festive fun to its hidden object game Seekers Notes: Hidden Mystery. The Christmas update introduces some new inhabitants to players, and with them a chance to win plenty of... | Read more »
Bully: Anniversary Edition (Games)
Bully: Anniversary Edition 1.03.1 Device: iOS Universal Category: Games Price: $6.99, Version: 1.03.1 (iTunes) Description: *** PLEASE NOTE: This game is officially supported on the following devices: iPhone 5 and newer, iPod Touch... | Read more »
PINE GROVE (Games)
PINE GROVE 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: A pine grove where there are no footsteps of people due to continuous missing cases. The case is still unsolved and nothing has... | Read more »
Niantic teases new Pokémon announcement...
After rumors started swirling yesterday, it turns out there is an official Pokémon GO update on its way. We’ll find out what’s in store for us and our growing Pokémon collections tomorrow during the Starbucks event, but Niantic will be revealing... | Read more »
3 reasons why Nicki Minaj: The Empire is...
Nicki Minaj is as business-savvy as she is musically talented and she’s proved that by launching her own game. Designed by Glu, purveyors of other fine celebrity games like cult favorite Kim Kardashian: Hollywood, Nicki Minaj: The Empire launched... | Read more »
Clash of Clans is getting its own animat...
Riding on its unending wave of fame and success, Clash of Clans is getting an animated web series based on its Clash-A-Rama animated shorts.As opposed to the current shorts' 60 second run time, the new and improved Clash-A-Rama will be comprised of... | Read more »

Price Scanner via MacPrices.net

Never Settle for Low Performing Wifi With iOS...
AppYogi Software has announced the release of WiFi Signal Strength Status App 1.0, the company’s new utility developed exclusively for macOS. WiFi Signal Strength Status App features a unique, single... Read more
New 2016 13-inch Touch Bar MacBook Pros in st...
B&H Photo has stock of new 2016 Apple 13″ Touch Bar MacBook Pro models, each including free shipping plus NY sales tax only: - 13″ 2.9GHz/512GB Touch Bar MacBook Pro Space Gray: $1999 - 13″ 2.... Read more
New 2016 15″ Touch Bar MacBook Pros in stock...
B&H Photo has new 2016 Apple 15″ Touch Bar MacBook Pro models in stock today including free shipping plus NY sales tax only: - 15″ 2.7GHz Touch Bar MacBook Pro Space Gray: $2799 - 15″ 2.7GHz... Read more
DietSensor App Targeting Diabetes and Obesity...
DietSensor, Inc., a developer of smart food and nutrition applications designed to fight diabetes and obesity and help improve overall fitness, has announced the launch of its DietSensor app for... Read more
Holiday 2016 13-inch 2.0GHz MacBook Pro sales...
B&H has the non-Touch Bar 13″ MacBook Pros in stock today for $50-$100 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 13″ 2.0GHz MacBook Pro Space Gray (MLL42LL/A): $1449 $... Read more
Holiday sale: Apple TVs for $51-$40 off MSRP,...
Best Buy has dropped their price on the 64GB Apple TV to $159.99 including free shipping. That’s $40 off MSRP. 32GB Apple TVs are on sale right now for $98 on Sams Club’s online store. That’s $51 off... Read more
12-inch Retina MacBooks, Apple refurbished, n...
Apple has restocked a full line of Certified Refurbished 2016 12″ Retina MacBooks, now available for $200-$260 off MSRP. Refurbished 2015 models are available starting at $929. Apple will include a... Read more
Holiday sale: 12-inch Retina MacBook for $100...
B&H has 12″ Retina MacBooks on sale for $100 off MSRP as part of their Holiday sale. Shipping is free, and B&H charges NY sales tax only: - 12″ 1.1GHz Space Gray Retina MacBook: $1199 $100... Read more
Apple refurbished 13-inch MacBook Airs availa...
Apple has Certified Refurbished 13″ MacBook Airs available starting at $849. An Apple one-year warranty is included with each MacBook, and shipping is free: - 13″ 1.6GHz/8GB/128GB MacBook Air: $849 $... Read more
Apple refurbished iMacs available for up to $...
Apple has Certified Refurbished 2015 21″ & 27″ iMacs available for up to $350 off MSRP. Apple’s one-year warranty is standard, and shipping is free. The following models are available: - 21″ 3.... Read more

Jobs Board

*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
US- *Apple* Store Leader Program - Apple (Un...
…Summary Learn and grow as you explore the art of leadership at the Apple Store. You'll master our retail business inside and out through training, hands-on Read more
Automotive Detailer - *Apple* Used Autos -...
We are currently conductinginterviews and will be accepting applications for a part-time detailer. Apple Used Autos is a great place to work andstart a career. We Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
*Apple* Retail - Multiple Positions- Trumbul...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.