TweetFollow Us on Twitter

Courteous Apps
Volume Number:9
Issue Number:12
Column Tag:C Workshop

Running in the Background

Writing courteous applications

By Geoff Clements, Chelmsford, Massachusetts

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

About the author

Geoff Clements is a radar engineer with a Master’s degree in Electrical Engineering who seems to find himself spending more time programming Macintoshes than directing electromagnetic energy. Geoff lives in Chelmsford, MA with his wife Michele, a dog named Tara, a bird named Disney, and a baby in development.

Introduction

In a past article (March ’93 issue, Voxels), I described drawing a sphere that appeared 3-dimensional on the Macintosh. The application rendered a three dimensional sphere with two light sources during startup. Rendering images is a time consuming process, so the Macintosh appears to lock up while the sphere drawn. The sphere is rendered into an off screen graphics port so the application need only do it once. The sphere is copied from the off screen port when it is finished. When the screen needs updating the image is just copied from the off screen port. The window containing the drawing can be resized and when the user chooses quit, the application goes politely away. It’s a reasonably well behaved application, except during startup.

Now, I’d like to show how this same application can be modified to render the sphere in the background while the user interacts with another application in the foreground, (in fact the program is running in the background while I’m writing this). Remember that there is a cost to running an application in the background. Both the foreground application and background application will run slower. On the other hand, the cost is small in comparison with having to stare at an unresponsive screen while a time consuming task is being computed.

While we are making modifications we can add some extra code to update the screen with what’s been calculated when the application is brought to the foreground. This way, the user can see what the image looks like before it is done. If the user isn’t happy with the way the image is coming out they can stop it and try some new parameters rather than having to wait for the entire image to be drawn.

Events

In order to understand how an application runs in the background, we need to understand how events are processed. For those of you who already understand events, skip to the source code section.

An event is generated whenever some kind of action happens to, or within, a Macintosh. For example, pressing the mouse button or pressing a key on the keyboard. Events are placed into an event queue, which is a list of events the operating system keeps track of. Even though there are several different event queues in the Macintosh, the Toolbox Event Manager procedures cause them to appear as a single queue which we will refer to as the “event queue.” When an application is ready, it will repeatedly access this queue and process the events the operating system passes back.

Events themselves are defined by the following structure:

struct EventRecord {
 short  what;
 long   message;
 long   when;
 Point  where;
 short  modifiers;
};
typedef struct EventRecord EventRecord;

The only field of this structure that we will concern ourselves with is the “what” field which specifies the type of event.

The different types of events an application receives can be sorted into three categories: low-level, operating system, and high-level. The low level events, include “mouse down” and “key pressed” events. Examples of operating system events include “suspend” and “resume” events. The high level events, are the new “apple events” included in System 7. I’ll refer to the high level events as apple events from now on. An application running in the foreground can receive any of these categories of events. On the other hand an application running in the background only receives a small subset. Background applications receive only null events (a low-level event), update events (also a low-level event), and apple events.

Apple events are a mechanism that applications can use to send messages to one another. Apple has specified that an application must handle four required apple events for an application to be System 7 savvy: “open application”, “open document”, “print document”, “quit application”. The System 7 finder sends one of the first three of these events to an application when the application first starts up depending on how the user started the application. For example, if the user drops a document onto the icon for the application the finder sends an open document event. The open document event includes a file specification for the file to be opened. In response the application should initialize itself, then open the document as if the user had chosen ‘Open’ from the file menu. The quit application apple event is a polite way of telling an application to shut itself down and clean up after itself.

The operating system will only send apple events to an application that has the ‘high level event aware’ flag set in the ‘SIZE’ resource. The SIZE resource contains information that the operating system uses to start up an application such as: whether or not to send apple events to the application and if the application is 32 bit clean.

Update events just notify the application that it needs to redraw part or all of one of its windows. Null events are for the most part ignored. Figure 1 shows a block diagram of how events are sent from the operating system to an application.

Figure 1

At this point, we need to make a small digression to clarify how events are added to the event queue. Events have a priority associated with them. With the exception of the Null event, the low-level events have the highest priority. Next come the operating system events, then the apple events. Null events have the lowest priority.

Events are inserted into the event queue in FIFO (first in first out) order. Events in the queue are sorted by priority so that low level events are always inserted before apple and operating system events; and operating system events are always inserted in front of apple events. Null events are added at the very end of the event queue.

Applications receive events by calling the Toolbox procedure WaitNextEvent. The next available event can also be retrieved using the procedure GetNextEvent; but GetNextEvent does not provide the support for multi-tasking that WaitNextEvent does. Apple recommends using WaitNextEvent. Following Apple’s recommendations is usually a good idea. An application can peek at the next event by calling EventAvail.

The prototype for WaitNextEvent is:

pascal Boolean WaitNextEvent(short eventMask,
    EventRecord *theEvent, unsigned long sleep,
    RgnHandle mouseRgn);

eventMask is a set of events that the application wants to be notified about. In our case we will use everyEvent. theEvent is a pointer to an EventRecord that the operating system will fill in. The sleep parameter is the number of ticks that an application is willing to wait in the WaitNextEvent call if there are no events in the event queue, (a tick is 1/60th of a second). mouseRgn is a handle to a region that defines an area in which mouse movement does not generate mouse moved events. We will pass NULL in mouseRgn.

When a foreground application calls WaitNextEvent several things can happen. If there is an event in the event queue the operating system immediately returns it. If there are no events in the event queue and the application has been waiting for longer than sleep ticks then the operating system returns a null event. If there are no events in the event queue and the application has been waiting less than sleep ticks, null events are sent to applications running in the background that are waiting for their call to WaitNextEvent to return. WaitNextEvent will not return until sleep ticks worth of time has expired; or until a non-null event is pending.

This is the feature we’ll use to render the sphere in the background. During start up, the application will calculate a pixel’s color for each event received no matter what kind of event it is. Almost all of the pixels will be calculated while processing null events.

Source Code

Since everyone already has the code for the sphere application, I’ll just describe the changes that need to be made in order to render the sphere in the background during start up. (You did save that copy of MacTech, right? Just kidding, all the source is included.) We’ll start with the main procedure.

We first change how the application calls the initialization procedure Init. If there is a problem during initialization, the application quietly exits as before; but, rather than using doneFlag to signal that the application is quitting, Init will return a false boolean value. In fact, I’ve eliminated the need for doneFlag entirely. If Init returns successfully the application calls InitCursor and then enters the main event loop.

Next, we change the DoEvent procedure. When a user clicks in the content portion of the window, the application copies what’s been calculated so far from the off screen port into the content region of the window. This allows the user to view how much of the sphere has been drawn. If an activate event is received the application redraws the menu bar and sets the cursor to the arrow. (Users usually like having the proper cursor showing when an application is activated. It’s rude to have to click on something with a watch cursor.)

While we are changing DoEvent we can add support for the suspend and resume operating system events by adding cases to the DoEvent switch statement.

The only change to CleanUp is to replace setting doneFlag equal to one with a call to ExitToShell. It is important to note that ExitToShell should only be called from your application. This sounds silly. How could ExitToShell be called otherwise? It can if, for instance, you are supporting the new apple events and your handler for the quit application apple event calls ExitToShell. In this case the operating system is calling ExitToShell, not your application. During start up, an application registers the apple event handlers it understands with the operating system. When the application receives an apple event, it asks the operating system to take care of calling the proper handler depending on the event received. The operating system calls the apple event handler and the event handler calls ExitToShell and then bad things happen. This can happen with any code called indirectly through the operating system.

We have been working our way up from the bottom of the source listing and we’ve come to Init. As you might expect, the biggest changes are made to Init. Init now returns a boolean. If Color Quickdraw is not available or if the palette didn’t load, return 0 rather than setting doneFlag equal to one. If the call to NewGWorld fails, return 0 rather than setting doneFlag equal to one. Since the application will be updating the screen before the entire sphere has been rendered, erase the off screen port with a call to EraseRect.

Those were the simple changes, now come the big changes. The loop to calculate the pixel color is modified from this:

for(i=0;i<volSize;i++)
 for (j=0;j<volSize;j++) {
 k = 0;
 do {
 DoColor(i, j, k, &pixColor);
 k++;
 } while ((pixColor.red == 0) & (k < volSize));
 SetCPixel (i, j, &pixColor);
 }

to this:

/* 1*/

for(i=0;i<volSize;i++)
 for (j=0;j<volSize;j++) {
 k = 0;
 do {
 DoColor(i, j, k, &pixColor);
 k++;
 } while ((pixColor.red == 0) & (k < volSize));
 SetCPixel (i, j, &pixColor);
 UnlockPixels (wallyWorld->portPixMap);
 SetGWorld (savedPort, savedDevice);
 if (WaitNextEvent(everyEvent,&event,0,NULL))
 DoEvent ();
 SetGWorld (wallyWorld, NULL);
 LockPixels (wallyWorld->portPixMap);
 }

Once the application has calculated a single pixel color we unlock the pixels, set the graphics world to the on screen port, and call WaitNextEvent with sleep = 0. Using a value of zero for sleep tells the operating system that the application is willing to wait if another application has an event pending, otherwise it wants the processor back.

The reason we change the graphics port is that DoEvent can process any category of event. This includes update events which cause drawing in the on screen port. Being cautious of the current graphics world allows us to use a generic DoEvent procedure to process all events rather than a generic DoEvent procedure in the main event loop and a specialized one in our pixel color calculation loop.

When WaitNextEvent returns and DoEvent finishes, we can set the graphics world to the off screen port and calculate the color of another pixel. As you might expect, swapping graphics worlds and processing events takes time. The application running in the background, generating a 128x128 image, takes about a minute 45 seconds. The original application, generating the same image, takes about a minute and 10 seconds. Although 35 seconds over the course of calculating 16384 pixels is significant, staring at an unresponsive screen for a minute 10 seconds is less appealing. Your performance may differ depending on how fast you type in a foreground application while the sphere is rendered in the background.

The last change to Init is drawing the completed sphere. Even if the application is running in the background, the completed sphere is drawn and the user can see that the application has finished.

There are only two other changes that need to be made to the sphere rendering application. We need to set the “can Background” and “accept suspend/resume events” flags in the ThinkC project and add prototypes for all the functions. Adding prototypes may seem unnecessary, but if the compiler is willing to check to make sure reasonable variable types are passed to procedures I’ll let it. This is a personal preference. As a final illustration of the utility of running applications in the background, I’d like to describe faceless background applications (FBAs).

Faceless Background Applications

Recently, in an Apple publication, I read about applications called faceless background applications. These applications do all of their processing in the background. Most have no user interface. They trundle along in the background taking care of tasks unnoticed and unappreciated. Like emptying the trash can every half hour or beeping every fifteen minutes to let you know you should rest your eyes. FBAs are full fledged applications that serve a useful purpose without interaction with a user at all.

Now that you have seen how to make an application do something useful between key presses, try adding this capability to your own applications. Not every application can benefit by processing null events in the background. But any application that needs time to process a request can benefit by occasionally making a call to WaitNextEvent and letting another application have a crack at the processor.

Acknowledgements

Thanks to Howard Clements my brother, Doug Cuthbertson my friend, and my wife Michele, all of whom read this article and made it readable.

References

Apple Computer, Inc., Inside the Macintosh, Volume VI, Addison Wesley, 1992

Clements, Geoffrey, “What are Voxels?,” MacTech Magazine, March 1993

Huan C. K., “Be Our Guest: Background-Only Applications in System 7,” Develop, Issue 10

Listing: Voxel.c

#include <Palettes.h>
#include <SANE.h>
#include <QDOffscreen.h>

/* size of the voxel data */
#define volSize  128
/* half the size of voxel data */
#define halfVolSize64
/* the radius of the sphere */
#define sphere_r 60
/* sphere_r*sphere_r */
#define volumeMag3600.0
/* sqrt(3.0)*sphere_r */
#define sqrt3r   103.9230
/* sphere_r*sqrt(6+2*sqrt(3)) */
#define rsqrt6   184.5827

/* resource numbers for the window, palette and menus */
#define windowRscr 128
#define paletteRscr128

#define appleID  128
#define appleM   0
#define appleAbout 1

#define fileID   129
#define fileM    1
#define fileQuit 1

#define editID   130
#define editM    2
#define editUndo 1
#define editCut  3
#define editCopy 4
#define editPaste  5
#define editClear  6

#define sleepTicks 30

#define aboutDialog  128

/* these constants define the Phong shading */
/* ambient reflection coefficient */
#define ambientReflCoef 0.1
/* depth cueing coefficient */
#define depthCueCoef  1.0
/* diffuse reflection coefficient */
#define diffReflCoef 5.0
/* specular reflection coefficient */
#define specReflCoef 5.0
/* first light source intensity */
#define light    1.0
/* coefficient to approx highlight */
#define highlightCoef 30

char aChar;
WindowPtr currentWindow;
MenuHandle myMenus[editM+1];
Rect dragRect, growRect;
long newSize;
EventRecord event;
WindowPtr whichWindow;
RGBColor pixColor;
short i, j, k;
PaletteHandle palH;
DialogPtr dPtr;
short doneDlg;
OSErr err;
SysEnvRec envRec;

Rect copyRect;
GWorldPtr wallyWorld;
GDHandle savedDevice;
CGrafPtr savedPort;
double PowerOfN (double x, short r);
short CalcVolumeData (short i,short j,short k);
void DoColor (short i, short j, short k,
 RGBColor *RGBVal);
void OpenWindow (void);
Boolean Init (void);
void DoAboutBox (void);
void CleanUp (void);
void DoCommand (long menuResult);
void DoEvent (void);

double PowerOfN (double x, short r) {
 double ans;
 
 ans = 1.0;
 while (r-- > 0) ans *= x;
 return ans;
}

double fx, fy, fz;

short CalcVolumeData(short i,short j,short k) {
 long x, y, z;
 
 fx = -(double)(i - halfVolSize);
 fy = -(double)(j - halfVolSize);
 fz = -(double)(k - halfVolSize);
 if((fx*fx+fy*fy+fz*fz) <= volumeMag) return 1;
 else return 0;
}

void DoColor (short i, short j, short k,
 RGBColor *RGBVal) {
 double n_dot_h, n_dot_l, n_dot_h2, n_dot_l2;
 double shade;
 unsigned short color;
 
 if (CalcVolumeData (i, j, k)) {
 n_dot_l = (fx + fy + fz)/sqrt3r;
 n_dot_h = (fx + fy + 2.7321*fz)/rsqrt6;
 shade = light*ambientReflCoef
 +(light/((double)(k)+depthCueCoef)
 *(diffReflCoef*n_dot_l+specReflCoef
 *PowerOfN (n_dot_h, highlightCoef)));

 /* second light source */
 n_dot_l2 = -fx/sphere_r;
 n_dot_h2 = (-fx + fz)/(1.4142*sphere_r);
 shade += light/((double)(k)+depthCueCoef)
 *(diffReflCoef*n_dot_l2+specReflCoef
 *PowerOfN (n_dot_h2, highlightCoef));

 color = (unsigned short)(shade * 65534.0);
 RGBVal->red = color;
 RGBVal->green = color;
 RGBVal->blue = color;
 }
 else {
 RGBVal->red = 0;
 RGBVal->green = 0;
 RGBVal->blue = 0;
 };
}

void OpenWindow (void) {
 currentWindow = (WindowPtr)GetNewCWindow
 (windowRscr, NULL, (Ptr)-1);
 SetPort(currentWindow);
 SizeWindow(currentWindow, volSize + 25,
 volSize + 25, 1);
 SetWTitle(currentWindow, &"\pVol3D");
 ShowWindow(currentWindow);
}

Boolean Init (void) {
 short i, j, k;

 InitGraf(&thePort);
 InitFonts ();
 FlushEvents (everyEvent, 0);
 InitWindows ();
 InitMenus ();
 TEInit ();
 InitDialogs (NULL);

 myMenus[appleM] = GetMenu(appleID);
 AddResMenu(myMenus[appleM], 'DRVR');

 myMenus[fileM] = GetMenu(fileID);
 myMenus[editM] = GetMenu(editID);

 for (i=appleM;i<=editM;i++)
 InsertMenu(myMenus[i], 0);

 DrawMenuBar ();

 SetRect(&dragRect, 30, 20,
 screenBits.bounds.right - 10,
 screenBits.bounds.bottom - 30);
 SetRect(&growRect, 50, 50,
 screenBits.bounds.right - 20,
 screenBits.bounds.bottom - 50);
 
 err = SysEnvirons(1, &envRec);
 if (!envRec.hasColorQD) return 0;
 else {
 OpenWindow ();
 palH = GetNewPalette (paletteRscr);
 if (palH == NULL) {
 return 0;
 }
 else {
 SetPalette (currentWindow, palH, 1);
 }
 
 /* set up the offscreen drawing port */
 GetGWorld (&savedPort, &savedDevice);
 SetRect (&copyRect, 0, 0, volSize-1,
 volSize-1);
 LocalToGlobal (&copyRect.top);
 LocalToGlobal (&copyRect.bottom);
 err = NewGWorld (&wallyWorld, 0,
 &copyRect, NULL, NULL, 0);
 GlobalToLocal (&copyRect.top);
 GlobalToLocal (&copyRect.bottom);

 if (err != noErr) return 0;
 else {
 SetGWorld (wallyWorld, NULL);
 if (LockPixels (wallyWorld->portPixMap)) {
 /* draw off screen here */
 EraseRect (&copyRect);
 for(i=0;i<volSize;i++) 
 for (j=0;j<volSize;j++) {
 k = 0;
 do {
 DoColor(i, j, k, &pixColor);
 k++;
 } while ((pixColor.red == 0)
 & (k < volSize));
 SetCPixel (i, j, &pixColor);
 UnlockPixels (wallyWorld->portPixMap);
 SetGWorld (savedPort, savedDevice);
 if (WaitNextEvent (everyEvent, &event, 0, NULL))
 DoEvent ();
 SetGWorld (wallyWorld, NULL);
 LockPixels (wallyWorld->portPixMap);
 }
 UnlockPixels (wallyWorld->portPixMap);
 }
 else return 0;
 
 /* the drawing is done set the current
 port back to the display window */
 }
 SetGWorld (savedPort, savedDevice);
 if (LockPixels(wallyWorld->portPixMap)){
 InvalRect(&currentWindow->portRect);
 DrawGrowIcon(currentWindow);
 InsetRect (&currentWindow->portRect, 8, 8);
 OffsetRect(&currentWindow->portRect,-8,-8);
 CopyBits(&wallyWorld->portPixMap,
 &currentWindow->portBits, &copyRect,
 &currentWindow->portRect, srcCopy, NULL);
 OffsetRect (&currentWindow->portRect,8,8);
 InsetRect(&currentWindow->portRect,-8,-8);
 UnlockPixels (wallyWorld->portPixMap);
 }
 }
 return 1;
}

void DoAboutBox (void) {

 dPtr = GetNewDialog(aboutDialog,NULL,(Ptr)-1);
 do
 ModalDialog(NULL, &doneDlg);
 while (!doneDlg);
 DisposDialog(dPtr);
}

void CleanUp (void) {
 
 HideWindow (currentWindow);
 DisposeGWorld (wallyWorld);
 DisposePalette (palH);
 DisposeWindow (currentWindow);
 ExitToShell ();
}

void DoCommand (long menuResult) {
 short menuID, menuItem;
 Str255 daName;
 short daErr;

 menuItem = LoWord (menuResult);
 menuID = HiWord (menuResult);

 switch (menuID) {
 case appleID: 
 if (menuItem == appleAbout)
 DoAboutBox ();
 else {
 GetItem(myMenus[appleM],menuItem,daName);
 daErr = OpenDeskAcc(daName);
 if (currentWindow)
 SetPort (currentWindow);
 }
 break;
 case fileID: 
 switch (menuItem) { 
 case fileQuit: 
 CleanUp ();
 break;
 }
 break;
 }
 HiliteMenu(0);
}

void DoEvent (void) {

 switch (event.what) {
 case mouseDown: 
 switch (FindWindow (event.where,
 &whichWindow)) {
 case inMenuBar: 
 DoCommand(MenuSelect(event.where));
 break;
 case inSysWindow: 
 SystemClick (&event, whichWindow);
 break;
 case inDrag: 
 DragWindow(whichWindow, event.where, &dragRect);
 break;
 case inGrow: 
 newSize = GrowWindow (whichWindow, 
 event.where, &growRect);
 SizeWindow(whichWindow,
 LoWord(newSize), HiWord(newSize), 1);
 InvalRect (&currentWindow->portRect);
 break;
 case inContent:
 if (LockPixels(wallyWorld->portPixMap)) 
 {
 InvalRect(&currentWindow->portRect);
 DrawGrowIcon (currentWindow);
 InsetRect(&currentWindow->portRect, 8, 8);
 OffsetRect (&currentWindow->portRect, -8, -8);
 CopyBits(&wallyWorld->portPixMap,
 &currentWindow->portBits, &copyRect,
 &currentWindow->portRect,srcCopy,
 NULL);
 OffsetRect (&currentWindow->portRect, 8, 8);
 InsetRect(&currentWindow->portRect, -8, -8);
 UnlockPixels(wallyWorld->portPixMap);
 }
 break; 
 case inGoAway: 
 if(TrackGoAway(whichWindow, event.where))
 CleanUp ();
 break;
 } /* case findwindow (...) */
 break;
 case keyDown:
 case autoKey: 
 aChar = (char)(BitAnd (event.message, charCodeMask));
 if (BitAnd (event.modifiers, cmdKey))
 DoCommand(MenuKey(aChar));
 break;
 case activateEvt: 
 if (BitAnd(event.modifiers, activeFlag))
 DisableItem(myMenus[editM], 0);
 else
 EnableItem(myMenus[editM], 0);
 DrawMenuBar ();
 SetCursor (&arrow);
 break;

 case updateEvt: 
 BeginUpdate(currentWindow);
 EraseRect(&currentWindow->portRect);
 DrawGrowIcon(currentWindow);
 InsetRect (&currentWindow->portRect, 8, 8);
 OffsetRect(&currentWindow->portRect,-8,-8);

 if (LockPixels (wallyWorld->portPixMap)) {
 CopyBits(&wallyWorld->portPixMap,
 &currentWindow->portBits, &copyRect,
 &currentWindow->portRect, srcCopy, NULL);
 UnlockPixels (wallyWorld->portPixMap);
 }
 
 OffsetRect(&currentWindow->portRect, 8, 8);
 InsetRect (&currentWindow->portRect, -8, -8);
 EndUpdate(currentWindow);
 break;
 
 case osEvt:
 if (BitAnd(event.message, resumeFlag)) {
 DisableItem(myMenus[editM], 0);
 InvalRect(&currentWindow->portRect);
 }
 else EnableItem(myMenus[editM], 0);
 break;
 }
}

void main (void) {
 currentWindow = NULL;
 if (Init ()) {
 InitCursor ();
 while (1)
 if (WaitNextEvent (everyEvent, &event,
 sleepTicks, NULL))
 DoEvent ();
 }
}
 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Capto 1.2.9 - $29.99
Capto (was Voila) is an easy-to-use app that takes capturing, recording, video and image editing to the next level. With an intelligent file manager and quick sharing options, Capto is perfect for... Read more
Opera 51.0.2830.40 - High-performance We...
Opera is a fast and secure browser trusted by millions of users. With the intuitive interface, Speed Dial and visual bookmarks for organizing favorite sites, news feature with fresh, relevant content... Read more
GarageSale 7.0.13 - Create outstanding e...
GarageSale is a slick, full-featured client application for the eBay online auction system. Create and manage your auctions with ease. With GarageSale, you can create, edit, track, and manage... Read more
1Password 6.8.7 - Powerful password mana...
1Password is a password manager that uniquely brings you both security and convenience. It is the only program that provides anti-phishing protection and goes beyond password management by adding Web... Read more
Evernote 7.0.1 - Create searchable notes...
Evernote allows you to easily capture information in any environment using whatever device or platform you find most convenient, and makes this information accessible and searchable at anytime, from... Read more
MacUpdate Desktop 6.2.0 - $20.00
MacUpdate Desktop brings seamless 1-click app installs and version updates to your Mac. With a free MacUpdate account and MacUpdate Desktop 6, Mac users can now install almost any Mac app on... Read more
HoudahSpot 4.3.5 - Advanced file-search...
HoudahSpot is a versatile desktop search tool. Use HoudahSpot to locate hard-to-find files and keep frequently used files within reach. HoudahSpot will immediately feel familiar. It works just the... Read more
EtreCheck 4.0.4 - 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
WhatsApp 0.2.8361 - Desktop client for W...
WhatsApp is the desktop client for WhatsApp Messenger, a cross-platform mobile messaging app which allows you to exchange messages without having to pay for SMS. WhatsApp Messenger is available for... Read more
iClock 4.2 - Customize your menubar cloc...
iClock is a menu-bar replacement for Apple's default clock but with 100x features. Have your Apple or Google calendar in the menubar. Have the day, date, and time in different fonts and colors in the... Read more

Latest Forum Discussions

See All

All the best games on sale for iPhone an...
Hi there, and welcome to our round up of all the best games that are on sale for iOS at the moment. It's not a vintage week in terms of numbers, but I'm pretty sure that every single one of these is worth picking up if you haven't already played... | Read more »
Disc Drivin' 2 Guide - Tips for the...
We're all still playing quite a bit of Disc Drivin' 2 over here at 148Apps, and we've gotten pretty good at it. Now that we've spent some more time with the game and unlocked more powerups, check out some of these more advanced tips: | Read more »
Alto's Odyssey Guide - How to Tackl...
Alto’s Odyssey is a completely stunning and serene runner, but it can also be a bit tricky. Check out these to try and keep your cool while playing this endless runner: Don’t focus too much on tasks [Read more] | Read more »
Here's everything you need to know...
Alto's Odyssey is a really, really good game. If you don't believe me, you should definitely check out our review by clicking this link right here. It takes the ideas from the original Alto's Adventure, then subtly builds on them, creating... | Read more »
Alto's Odyssey (Games)
Alto's Odyssey 1.0.1 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0.1 (iTunes) Description: Just beyond the horizon sits a majestic desert, vast and unexplored. Join Alto and his friends and set off on an endless... | Read more »
Vainglory 5v5: Everything you need to kn...
Vainglory just got bigger. [Read more] | Read more »
Check out these 5 games that are a lot l...
So you're in love with Minecraft, but you're looking for something else to play as well? You've come to the right place then, because this list is all about games that are a bit like Minecraft. Some of them, more than others. [Read more] | Read more »
Our top 5 characters from casual RPG Cre...
Creature Quest definitely lives up to its name with a host of collectible creatures based on fantasy tales and world mythologies. To celebrate Creature Quest’s first birthday, we’re going to lay out what we think are the five best characters in the... | Read more »
Around the Empire: What have you missed...
Did you know that Steel Media has a whole swathe of other sites dedicated to all aspects of mobile gaming? Sure you'll get the very best iPhone news, reviews, and opinions right here at 148Apps, but we don't want you missing out on a single piece... | Read more »
All the best games on sale for iPhone an...
Oh hi there, and welcome to our round-up of the best games that are currently on sale for iPhone and iPad. You thought I didn't see you there, did you, skulking behind the bushes? Trust me though, the bushes aren't where the best deals are. The... | Read more »

Price Scanner via MacPrices.net

Apple Canada offers 2017 21″ and 27″ iMacs fo...
 Canadian shoppers can save up to $470 on the purchase of a 2017 current-generation 21″ or 27″ iMac with Certified Refurbished models at Apple Canada. Apple’s refurbished prices are the lowest... Read more
9″ iPads available online at Walmart for $50...
Walmart has 9.7″ Apple iPads on sale for $50 off MSRP for a limited time. Sale prices are for online orders only, in-store prices may vary: – 9″ 32GB iPad: $279.99 $50 off – 9″ 128GB iPad: $379.99 $... Read more
15″ Apple MacBook Pros, Certified Refurbished...
Save $360-$420 on the purchase of a 2017 15″ MacBook Pro with Certified Refurbished models at Apple. Apple’s refurbished prices are the lowest available for each model from any reseller. An standard... Read more
Amazon restocks MacBook Pros with models avai...
Amazon has restocked 15″ and 13″ Apple MacBook Pros with models on sale for up to $251 off MSRP. Shipping is free. Note that stock of some Macs may come and go (and some sell out quickly), so check... Read more
Lowest price of the year: 15″ 2.8GHz Apple Ma...
Amazon has the 2017 Space Gray 15″ 2.8GHz MacBook Pro on sale today for $251 off MSRP. Shipping is free: – 15″ 2.8GHz Touch Bar MacBook Pro Space Gray (MPTR2LL/A): $2148, $251 off MSRP Their price is... Read more
Apple restocks full line of Certified Refurbi...
Apple has restocked a full line of Apple Certified Refurbished 2017 13″ MacBook Pros for $200-$300 off MSRP. A standard Apple one-year warranty is included with each MacBook, and shipping is free.... Read more
Lowest sale price available for 13″ 1.8GHz Ma...
Focus Camera has the 2017 13″ 1.8GHz/128GB Apple MacBook Air on sale today for $829 including free shipping. Their price is $170 off MSRP, and it’s the lowest price available for a current 13″... Read more
21-inch 2.3GHz iMac on sale for $999, $100 of...
B&H Photo has the 2017 21″ 2.3GHz iMac (MMQA2LL/A) in stock and on sale for $999 including free shipping plus NY & NJ tax only. Their price is $100 off MSRP. Read more
Apple refurbished Mac minis in stock again st...
Apple has restocked Certified Refurbished Mac minis starting at $419. Apple’s one-year warranty is included with each mini, and shipping is free: – 1.4GHz Mac mini: $419 $80 off MSRP – 2.6GHz Mac... Read more
Tuesday MacBook Deals: $250 off 15″ 2.9GHz Ma...
Adorama has the Silver 15″ 2.9GHz Apple MacBook Pro on sale today for $250 off MSRP. Shipping is free, and Adorama charges sales tax for residents in NY & NJ only: – 15″ 2.9GHz Silver MacBook Pro... Read more

Jobs Board

*Apple* Retail - Multiple Positions - Apple,...
Job Description:SalesSpecialist - Retail Customer Service and SalesTransform Apple Store visitors into loyal Apple customers. When customers enter the store, 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* Solutions Consultant - Apple (United...
# Apple Solutions Consultant Job Number: 113523441 Orange, CA, California, United States Posted: 21-Feb-2018 Weekly Hours: 40.00 **Job Summary** Are you passionate Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description:SalesSpecialist - Retail Customer Service and SalesTransform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Sr. Experience Designer, Today at *Apple* -...
# Sr. Experience Designer, Today at Apple Job Number: 56495251 Santa Clara Valley, California, United States Posted: 18-Jan-2018 Weekly Hours: 40.00 **Job Summary** Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.