TweetFollow Us on Twitter

Installer Plugins

Volume Number: 25
Issue Number: 06
Column Tag: Installers

Installer Plugins

Build a basic installer plug-in using Xcode

by José R.C. Cruz

Introduction

Up to now, you know two ways to customize an install session. For instance, to check if the target is the right one for the payload, you use a requirements script. To prepare the target to receive the payload, you use an action or an install script. Yet, there are cases where you may find these approaches inadequate. To handle those cases, you may need an installer plug-in.

This article will give you the background you will need to write your own installer plug-in. First, it explains how the plug-in fits into the basic install session. Then, it describes the plug-in API defined by the InstallerPlugin.framework. Next, it presents the plug-in template and its constituent files. Finally, it shows how to build a basic plug-in and test it in a basic package.

As always, you can get a copy of the featured projects from the MacTech website. Just go to the following URL to download your copy.:

ftp.mactech.com/src/mactech/volume25_2009/25.06.sit

Enter The Plug-In


Figure 1. Sequence of panels in a basic install session.

Several panels make up a basic install session (Figure 1). The Welcome panel appears when users start the session by double-clicking the package. The second panel, ReadMe, provides users with more information about the package's payload. The third one, License, shows the manufacturer's license terms governing the use of the payload. User can either reject these terms, thus ending the session, or accept them to continue. Next, the Select Destination panel lets users choose the target volume on which to install the payload. After that, the Custom Install panel allows users to select which payload to install on the target. Then the Standard Install panel gives users one last chance to change their minds before the actual install occurs. Finally, the Conclusion panel summarizes the results of the installation.

Not all of these panels need appear in an install session. For instance, the Readme and License panels (grey) are optional. The install session skips these panels if they have no text to display. The Select Destination and Custom Install panels (orange) are controlled by the package. They appear only if users are allowed to change the target volume or choice of payloads during the session.

Now suppose your package needs extra information from the users, and none of the panels are up to the task. For this case, you need to write an installer plug-in. A plug-in can insert a custom panel at specific points of the session (Figure 2). It resides in the directory Contents/Plugins of the package. Also, a plug-in has full access to the Cocoa framework. This allows the plug-in to do a wide variety of tasks, tasks that are either difficult or impossible to do with a script.

Yet, like any software technology, an installer plug-in has its limits and issues. For one, it cannot replace any of the basic install panels. A plug-in must always provide a panel -- paneless plug-ins are not supported. Also, you cannot debug a plug-in using Xcode's source debugger. Your only recourse is to have the plug-in send debug messages to the console.log file using either NSLog() or ASL (Apple System Log). Another issue is that only meta-packages and distribution packages support installer plug-ins. The new flat-file Leopard package does not support them at this time.

Last of all, at the time of writing, the plug-in API is still poorly documented. Your only options so far are to study the header files in the InstallerPlugin framework, and the sample plug-in project from Apple.


Figure 2. A plug-in in an install session

The Plug-in Framework

The InstallerPlugin framework serves as the basis of all installer plug-ins. This framework is located in /System/Library/Frameworks of the OS X boot volume. The framework comes with four header files, one of which, InstallerPlugins.h, is the main header. The other headers define three classes your plug-in can use. Figure 3 shows how these classes relate to each other.


Figure 3. The InstallerPlugin framework

To add this framework to your Xcode project, select the Frameworks and Libraries group on the Groups & Files pane of the editor window. Choose Add to Project from the Project menu, and use the Open File dialog to select the framework. Click the Add button to include the framework to the project. Then add the following line to your header file

   #import <InstallerPlugins/InstallerPlugins.h>

Let us now examine what each class has to offer.

The InstallerPane class

The InstallerPane class (Figure 4) handles the display of the custom panel. It also manages the interactions between the panel and the users. In short, this class serves as the controller for that panel.


Figure 4. The InstallerPane class

The class has six private outlets, four of which give the views that are linked to the class. For instance, the contentView outlet is the panel itself. The initialKeyView outlet is the first control widget that gets user focus after the panel is displayed. The firstKeyView outlet points to the current widget that gets any keyboard events, while the lastKeyView outlet points to the last widget to get any events.

The nextPane outlet returns the installer panel that follows the current one. And the outlet parentSection returns the InstallerSection instance for that panel (more on this later).

The InstallerPane class also comes with a wide range of messages. In this article, we will focus only on those messages that deal with panel behavior. There are eleven of these messages, which falls under two groups. The first group consists of delegate messages that the class gets at each panel event (Figure 5).


Figure 5. Panel events and delegate messages

Before the panel appears in the install session, it sends a willEnterPane message to the InstallerPane class. This allows the class to prepare the resources it needs to support the panel. Next, the panel appears and sends a title message to the class. The class responds with a localized NSString, which the panel displays near its upper-right corner (green). Then the panel sends a didEnterPane to the class. The latter can respond either by setting the default values on the panel or by starting the desired services.

When users click the Go Back or Continue button, the panel first sends a shouldExitPane to the InstallerPane class. If the class returns a NO, the panel remains active. On the other hand, if the class returns a YES, the panel sends a willExitPane back to the class. The class uses this moment to process the user data from the panel. The panel then disappears and sends a didExitPane back to the class. This is where the class can dispose the resources it used to support the panel.

The second group of messages allows the InstallerPane class to control some panel activity. For instance, to enable the Continue button, send a setNextEnabled message with a YES argument.

   [self setNextEnabled:YES];

To read the state of the Continue button, send a nextEnabled message. If the button is enabled, the message returns a YES; otherwise, it returns a NO.

   tFlg = [self nextEnabled];

To display the next panel, use the gotoNextPane message. For the previous panel, use the gotoPreviousPane message.

   tFlg = [self gotoNextPane];

These messages have the same effect as users clicking the Continue or Go Back buttons. Both return a YES if the desired panel appears without errors. On the other hand, if the panel does not exists or if an error occurs, both messages return a NO.

The InstallerSection class

Next is the InstallerSection class (Figure 6), which works as a controller for InstallerPane. It supplies the plug-in with data on the current install session. The InstallerPane class carries an instance of InstallerSection in its parentSection outlet. To access the instance, send a section message from InstallerPane.

   tSct = [self section];


Figure 6. The InstallerSection class

The InstallerSection class has one private outlet, firstPane. This outlet stores an instance of the InstallerPane class. By default, this is the same InstallerPane whose the custom panel appears during the install session. Since this outlet exists, it implies that a plug-in can have multiple instances of InstallerPane, each one with its own custom panel. We will explore this possibility in a future article.

Next, the InstallerSection class comes with ten methods, some of which you can override. This article, however, will focus only on those methods that a basic plug-in can use. For instance, to get the current panel, use the firstPane or activePane method. Either method will return the same InstallerPane instance if the plug-in has only one custom panel.

   tPnl = [[self section] firstPane];

To get the nib that carries the custom panel, send a bundle message to InstallerSection. This returns the bundle as an NSBundle object.

   tBndl = [[self section] bundle];

To read the title string for the active panel, call the title method. InstallerSection responds by returning the panel title as an NSString.

   tTitle = [[self section] title];

And to find out the current install state, use the state accessor. This gives you an instance of the InstallerState class, which is described next.

   tState = [[self section] state];

The InstallerState class

As stated earlier, the InstallerState class (Figure 7) returns the current state of the install session. Like InstallerSection, this class is instantiated by InstallerPane once the latter displays its panel. To access the instance, use the state accessor of InstallerSection.


Figure 7. The InstallerState class

There are four sets of methods in the InstallerState class. Each set corresponds to a specific stage in the install session. The first set returns the results of the License panel. To find out if users accepted the license terms, use the licenseAgreed method. The method returns a YES if users did accept the terms; otherwise, it returns a NO.

   tAgree = [[[self section] state] licenseAgreed];

To find out which localized license is displayed, use licenseAgreedLanguage. This method returns the language as an NSString.

   tAgree = [[[self section] state] licenseAgreedLanguage];

The second set of methods return the results of the Select Destination panel. They tell the plug-in where the package will install its payloads. To get the selected target volume, use the targetVolumePath method.

   tVol = [[[self section] state] targetVolumePath];

The method returns the mount point of the selected volume as an NSString. To get the final destination for the payload, call the targetPath method.

   tPth = [[[self section] state] targetPath];

This returns the destination's absolute path as an NSString. The returned path will also have the volume's mount point as part of its string.

The next set of methods give the results of the Custom Install panel. They tell the plug-in which payloads where chosen by the users. For a list of all payloads, call the method choiceDictionaries.

   tList  = [[[self section] state] choiceDictionaries];

The method returns its list as an NSArray. For a specific payload choice, use the method choiceDictionaryForIdentifier. Then pass the payload's choice ID as input.

   tPayload  = [[[self section] state] 
         choiceDictionaryForIdentifier:@"foobar"];

This returns the choice settings as an NSDictionary. There are three entries in this dictionary, each entry with its own unique key. The choice ID, for instance, uses InstallerState_Choice_Identifier as its key, the payload's destination path InstallerState_Choice_CustomLocation. And the choice state is under the key InstallerState_Choice_Installed. Figure 8 shows which field on the choice's Configuration panel corresponds to which key.


Figure 8. The choice states and their keys

The Plug-in Template

To create an installer plug-in, use the Xcode project template, aptly named, Installer Plugin. Xcode 3.0 files this template in the directory /Developer/Library/Xcode/Project Templates/Standard Apple Plug-ins. You can, of course, write your own plug-in project from scratch. Using the template, however, reduces the amount of guesswork on your part.

Figure 9 shows the bundles and files of the project template. Note that there are two project bundles: one with a .xcode suffix, the other .xcodeproj. Use the .xcodeproj bundle if your Xcode IDE is version 2.x or newer; use the .xcode bundle for older versions of Xcode.


Figure 9. The plug-in project template

Note also that there are only three items that you should be updating. The rest of the project items have default settings that will suffice for most cases.

The InstallerSection.plist file

This file defines where your custom panel will appear in the install session. This file must be placed in the Contents/Plugins directory of your installer package. Without this file, your package will ignore your installer plug-in.

Listing 1 shows the default contents of that file. Note the file list only six of the panels that appear in the install session. The first three on the list refer to the Welcome, Readme, and License panels. The Target entry is the Select Destination panel, and PackageSelection is either the Custom or Standard Install panel. Finally, the Install entry is the progress panel, which appears when the package starts installing its payloads.

Listing 1. The InstallerSection.plist file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
   <key>SectionOrder</key>
   <array>
      <string>Introduction</string>
      <string>ReadMe</string>
      <string>License</string>
      <string>«PROJECTNAME».bundle</string>
      <string>Target</string>
      <string>PackageSelection</string>
      <string>Install</string>      
   </array>
</dict>
</plist>

Note as well that the file places the plug-in between the License and Target entries. It also lists the plug-in under the generic name of PROJECTNAME. When you create your plug-in project using this template, Xcode replaces PROJECTNAME with your plug-in's name. If you move your plug-in's entry in the list, you change the position of its panel in the install session. Also, if you delete one of the entries in the list, you prevent that panel from appearing during the session. But be careful with this last step, as it may lead to unexpected results.

The InstallerPlugin.nib bundle

This bundle defines the look and feel of your plug-in's panel. Like all nib bundles, you use the Interface Builder to make changes to this bundle. Figure 10 shows the five default objects in this bundle. Two of the objects are for your plug-in's use. For instance, the View object is, obviously, an instance of NSView. This object carries the interface of your custom panel. Double-clicking it gives you an empty panel that is 418 pixels by 330 pixels in size. Do not, however, change the size of the panel. If you do, your interface widgets will appear misaligned during the install session.


Figure 10. The InstallerPlugin.nib bundle

The <<PROJECTNAMEASIDENTIFIER>>Pane is an instance of InstallerPane. Its name is set to the project's name after you create the project. For instance, if your plug-in project is Foobar, the InstallerPane object gets the name FoobarPane.

The other three objects are the usual proxy objects you find in most nib bundles. But in this nib, the File's Owner proxy refer to the instance of InstallerSection. Its one outlet, firstPane, is linked to the nib's InstallerPane instance. The Application proxy refers to the global NSApplication object, which will be the Installer utility for the plug-in. And the First Responder proxy refer to the first object in the responder chain. This proxy is currently not attached to any object on the nib.

The Pane files

Finally, there are the two files: Pane.h and Pane.m. These files define the InstallerPane controller for your panel. Again, when Xcode creates your plug-in project, it prefixes the project name to each file. Do not, however, change the assigned file names to something else. If you do, your plug-in will be unable to display its panel correctly.

Treat these files as you would any controller class files. For instance, you can add outlets and link them to specific widgets on the custom panel. You can define actions that your panel widgets can call at runtime. You can even add code to manipulate user data or pass that same data to your own custom model class.

CAVEAT

At the time of writing, the plug-in template has one interesting flaw. When you use it to create a project, the project's nib bundle loses all six outlets for its InstallerPane instance. Your plug-in will still work with this flaw present, but it will be unable to query the current install state.

To work around this flaw, save your nib bundle as NIB 2.x. With a text editor, open the file classes.nib that is inside your bundle. Then add the lines in Listing 2 to that file. These lines will restore the six outlets to your bundle. Save your changes when done.

Whether this flaw is fixed in Xcode 3.1 is unconfirmed.

Listing 2. Adding outlets to classes.nib

         <key>OUTLETS</key>
         <dict>
            <key>contentView</key>
            <string>NSView</string>
            <key>firstKeyView</key>
            <string>NSView</string>
            <key>initialKeyView</key>
            <string>NSView</string>
            <key>lastKeyView</key>
            <string>NSView</string>
            <key>nextPane</key>
            <string>InstallerPane</string>
            <key>parentSection</key>
            <string>id</string>
         </dict>

To Create A Plug-in

Let us now build a simple plug-in using the Installer Plug-in template. Our plug-in will ask users to enter their name, company, and product serial number during the install session. If the serial number is correct, the plug-in will enable the Continue button. Otherwise, it will display an alert dialog and disable the same button.

So, start up your copy of Xcode. Choose New Project from the File menu and pick Installer Plug-in from the list of project templates. Click the Continue button and set the project's name to Register. Leave the project's directory at the default location. Click the Finish button to create and open your new project.

Defining the panel

Select the entry RegisterPane.h from the Groups & Files pane. Add the following outlets to that file's @interface block.

   IBOutlet NSTextField   *oUser;
   IBOutlet NSTextField   *oComp;
   IBOutlet NSTextField   *oSerial;

Then add the following action to the same block.

   - (IBAction) registerCheck:(id)aSnd;

Next, double-click the Register.nib entry from the pane, thus opening the bundle in Interface Builder. From the Register window, select the icon «PROJECTNAMEASIDENTIFIER»Pane. Choose Identity Inspector from the Tools menu and set the Class field to RegisterPane. Scroll down to the Interface Builder Identity pane and set the Name field to RegisterPane. Save your changes by choosing Save from the File menu. To find out if your changes are correct, go to the Class Outlets pane of the palette. You should see your three outlets listed in that pane. Close the palette when you are done.

Now double-click the View icon on the Register window. Lay out the panel as shown in Figure 11. There are two sets of widgets on the panel. The first set consists of NSTextFields serving as static labels. The second set consists of NSTextFields serving as editable fields.


Figure 11. Layout of the custom panel

Select the RegisterPane icon and choose Connections Inspector from the Tools menu. With your pointing device, link each editable field to the right outlet (Figure 12, red). For this plug-in, the oUser outlet links to the User field, the oComp to the Company field, and oSerial to Serial. Then link all three editable fields to the registerCheck action (blue). Take care to leave the template's preset links alone. Save your changes and switch back to the Xcode editor.


Figure 12. Linking the panel widgets

Implementing the panel

We now enter the code-writing part of the project. Select the entry RegisterPane.m from the Groups & Files pane. Go to the delegate method didEnterPane and enter the code shown in Listing 3. This method sets each of the outlets to their default values. Then it disables the Continue button and enables the Go Back button.

Listing 3. Setting the default panel data

- (void)didEnterPane:(InstallerSectionDirection)aDir
{
      // initialize the following outlet fields
    [oUser setStringValue:@"your name goes here"];
    [oComp setStringValue:@"your company name"];
    [oSerial setStringValue:@"123456789"];
    
      // disable the Continue button
    [self setNextEnabled:NO];
    
      // enable the Go Back button
    [self setPreviousEnabled:YES];
}

Next, enter the code in Listing 4 to the delegate method shouldExitPane. First, the method reads the hash values from the oUser and oComp outlets. It then combines the two hash values and compares the results with the value from oSerial. If both values are different, the method displays a warning dialog to the users. It then disables the Continue button and returns a NO to prevent the panel change. On the other hand, if both values are the same, the method returns a YES to allow the change.

Listing 4. Checking the user data

- (BOOL)shouldExitPane:(InstallerSectionDirection)aDir
{
    NSUInteger tUsr, tCmp, tXor, tSN;
    NSAlert *tWrn;
    
   // check the direction of movement
    if (aDir == InstallerDirectionForward)
    {
        // read the hash values of each registration
        tUsr = [[oUser stringValue] hash];
        tCmp = [[oComp stringValue] hash];
        tXor = tUsr ^ tCmp;
        // read the serial number
        tSN = [oSerial intValue];
        if (tSN != tXor)
        {
            // create a warning dialog
            tWrn = [[NSAlert alloc] init];
            if (tWrn != nil)
            {
                // initialize the dialog
                [tWrn addButtonWithTitle:@"OK"];
                [tWrn setMessageText:@"Invalid serial number"];
                [tWrn setInformativeText:
            @"Please check and re-enter your registration information."];
                [tWrn setAlertStyle:NSInformationalAlertStyle];
                
                // display the warning dialog
                [tWrn runModal];
                
                // dispose the warning dialog
                [tWrn release];
            }
      // disable the Continue button
            [self setNextEnabled:NO];
            
            // prevent the panel movement
            return (NO);
        }
    }
// allow the panel movement
    return (YES);
}

Finally, to the registerCheck action, enter the code in Listing 5. This action checks the data in each outlet. If all outlets have a non-zero length string, the action then enables the Continue button. Otherwise, that same button remains disabled. Save your changes after you have updated these three methods.

Listing 5. Responding to the user entry

- (IBAction) registerCheck:(id)aSnd
{
    BOOL tChk;
    // check the registration fields
    tChk = ([[oUser stringValue] length] > 0);
    tChk &= ([[oComp stringValue] length] > 0);
    tChk &= ([[oSerial stringValue] length] > 0);
    
    // enable the Continue button
    [self setNextEnabled:tChk];
}

Now, a few words before we proceed to the next part of our project. First, note the use of the NSString hash function to generate the values for oUser and oComp. While this function is fine for our sample plug-in, it is impractical for real-world use. Future versions of NSString may behave differently. As a result, their hash functions may return a different value for the same string. A more reliable solution is for you to use your own hash algorithm.

Second, the plug-in only checks if the registration data is valid before it allows or deny product installation. This, again, is impractical because users can defeat the check by removing the plug-in. One good solution is to have the plug-in write its results to a hidden file. The installed payload can then look for this file and even read its data. If the data is correct, the payload behaves normally. If not, or if the file missing, the payload displays a reminder dialog or it runs in demo mode.

Building the plug-in

We are now ready to build our plug-in. But first, we must define where our plug-in's panel will appear in the install session. In this case, we want our panel to come after the Select Destination panel. Select the entry InstallerSection.plist from the Groups & Files pane. Locate the entry Register.bundle and move it after the entry for Target (Listing 6). Save your changs when done.

Listing 6. Modified contents of InstallerSection.plist

   <array>
      <string>Introduction</string>
      <string>ReadMe</string>
      <string>License</string>
      <string>Target</string>
      <string>Register.bundle</string>
      <string>PackageSelection</string>
      <string>Install</string>      
   </array>

Now build the plug-in by clicking the Build button on the Xcode toolbar. Xcode compiles each project file and links them to create the plug-in bundle. It then places the bundle, named Register.bundle, in the in the project subdirectory build/Release/.

Installing and testing the plug-in

To test the plug-in, you will need a basic installer package. Now when you prepare your installer project, make sure to set its Minimum Target to MacOS X 10.4. This will tell PackageMaker to use the distribution bundle as the package format.

In this article, we will use Foobar_Demo as our installer project. Its payload consists of three TIFF files, which will go into the directory /Users/Pictures. Build the package by choosing Build from the Project menu. When prompted, use Foobar as the package name. Go to the Finder and control-click the package to display its contextual menu. Choose Show Package Contents to open the bundle in a separate Finder window. Go to the Contents directory and create a new subdirectory named Plugins. Then copy the plug-in Register.bundle and the file InstallerSection.plist into that subdirectory (Figure 13). Close the Finder window when done.


Figure 13. Installing the plug-in and plist

Now double-click the Foobar package to start the install session. First, you get a modal dialog (Figure 14) warning you that the package will perform a custom task. This means the package recognizes your plug-in's presence. But it can also mean that the package contains an install action or script. Click Continue to proceed.


Figure 14. Warning the user

After the package displays its Welcome panel, you will see the name Register added to the list of panels (Figure 15, orange). You will also see that same name after Destination Select. This means the package knows that the plug-in has a custom panel, and it knows when to display the panel.


Figure 15. The Welcome panel with an updated list

Click the Continue button until you reach the Register panel. For the User field, enter the name "Alan Smithee" as the user. For the Company field, enter "Foobar". Leave the Serial field unchanged. You should see the Continue button enabled at this point. Now click that button. The package will display a warning dialog (Figure 16) telling you that the registration data is wrong.

Dismiss the dialog by clicking its OK button. Carefully type 1239302760 into the Serial field and then click the Continue button. This time, the package will display the next panel, which is the Custom Install panel.


Figure 16. Incorrect registration

Concluding Remarks

An installer plug-in is another way to customize your install session. With a plug-in, you can display a custom panel for other users to interact. You can use the Cocoa framework to do tasks that are hard, if not impossible, to do with an action or script.

Xcode comes with a basic template that you can base your plug-in project. This template sets the necessary files and nibs needed for such project. Adding a plug-in is as simple as a drag and drop. But keep in mind that only a meta-package and a distribution package will use plug-ins. The new flat-file package, introduced in 10.5, will ignore any plug-ins it may carry.

Recommended References

As stated earlier, Apple has yet to document the task of writing an installer plug-in. So far, your best options, besides this article, are to study their sample plug-in project, or the header files of the InstallerPlugin framework. You can also query the Installer-dev list archives for possible answer to your questions.

Stéphane Sudre, maker of Iceberg, also wrote a couple of pieces on the topic. Listed below are his online works for your benefit.

Stéphane Sudre. "Defining Installer Plugins". Iceberg Users Guide. Copyright 2008. Accessed on 2008 Aug 14. Online:

http://s.sudre.free.fr/Software/Iceberg.html

Stéphane Sudre. "Installer Plugins". Installation - The Lost Scrolls. 2008 Apr 10. Online: http://s.sudre.free.fr/Stuff/Installer/Installer_Plugins/index.html


JC is a freelance engineering writer from North Vancouver, British Columbia. He spends his time writing technical articles; tinkering with Cocoa, REALbasic, and Python; and visiting his foster nephew. He can be reached at anarakisware@gmail.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Fantastical 2.5.4 - Create calendar even...
Fantastical 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 details... Read more
Overflow 3.0.4 - Application launcher th...
Overflow is an application designed to quickly launch applications, open documents, or access folders while reducing the number of items needed in your Dock. Anything you want can be added to the... Read more
Numi 3.19 - Menu-bar calculator supports...
Numi is a calculator that magically combines calculations with text, and allows you to freely share your computations. Numi combines text editor and calculator Support plain English. For example, '5... Read more
Things 3 3.7 - Elegant personal task man...
Things is a task management solution that helps to organize your tasks in an elegant and intuitive way. Things combines powerful features with simplicity through the use of tags and its intelligent... Read more
OmniGraffle 7.9 - Create diagrams, flow...
OmniGraffle helps you draw beautiful diagrams, family trees, flow charts, org charts, layouts, and (mathematically speaking) any other directed or non-directed graphs. We've had people use Graffle to... Read more
App Tamer 2.4.1 - Efficiently manage you...
App Tamer tames your processor-monopolizing apps and keeps them from chewing up excessive CPU time and battery life. Powered by a unique AutoStop feature, App Tamer stops each application when you... Read more
Overflow 3.0.3 - Application launcher th...
Overflow is an application designed to quickly launch applications, open documents, or access folders while reducing the number of items needed in your Dock. Anything you want can be added to the... Read more
Deeper 2.3.3 - Enable hidden features in...
Deeper is a personalization utility for macOS which allows you to enable and disable the hidden functions of the Finder, Dock, QuickTime, Safari, iTunes, login window, Spotlight, and many of Apple's... Read more
Maintenance 2.4.2 - System maintenance u...
Maintenance is a system maintenance and cleaning utility. It allows you to run miscellaneous tasks of system maintenance: Check the the structure of the disk Repair permissions Run periodic scripts... Read more
OnyX 3.4.9 - Maintenance and optimizatio...
OnyX is a multifunction utility that you can use to verify the startup disk and the structure of its system files, to run miscellaneous maintenance and cleaning tasks, to configure parameters in the... Read more

Latest Forum Discussions

See All

The best iPhone games like Pokemon Go
Pokemon GO is still the, if you'll excuse the pun, go-to game if you want some AR action on your phone. But it's not the only choice out there, and if you've got a hankering for something a bit different, then your eyes might already have started... | Read more »
These are the iPhone games to look forwa...
Every Friday here at 148Apps we like to do one thing. And that one thing is make sure that you've got a really good idea of what you're going to be playing come this time next week. That's right, it's time for us to round-up the best games that... | Read more »
We talked to Tony Swatton, legendary Hol...
Sometimes we like to tell you about the awesome stuff going on around mobile gaming, not just about how awesome mobile games are. And this is definitely one of those times. Because what's more awesome than real-life weapons based on things from... | Read more »
Which of these 5 games do you think shou...
You know what time it is, right? It's time for you, our esteemed and charming readership, to choose which of the five games below deserves to be crowned our game of the week. It's a pretty good week as well, so we expect that every single vote is... | Read more »
The winner of last week's game of t...
It's been another close run thing in the 148Apps game of the week award, but after checking on some possible areas of contention with the Frankfurt bureau, we can finally reveal that the winner of last week's coveted title is the impressively deep... | Read more »
The best iPhone games like Starcraft
Starcraft sits at the top of the RTS tree for a number of very good reasons. It also isn't on mobile, again, for a number of very good reasons. But that doesn't mean you can't find a way to indulge your sci-fi, competitive, massive, or engaging RTS... | Read more »
The best cyberpunk games for mobile
Cyberpunk games have come and gone through the years, but CD Projekt Red’s upcoming Cyberpunk 2077 has reignited everyone’s interest in dystopian hacker worlds lately. It also got me interested in exploring the App Store to see what the best... | Read more »
Take back a fallen kingdom in new 3D MMO...
If you’ve ever craved to be the hero of your own story within a colourful, yet endangered land in need of saving, Final Destiny could be the journey you’ve been waiting for. A new fantasy 3D MMORPG that sees you team up or fight against others, the... | Read more »
The 5 best dogs in mobile gaming
Not too long ago we talked about all of the finest kitty-cats mobile gaming had to offer, but what about the best dogs on the market? You don't have to have thumbs to be an awesome protagonist. [Read more] | Read more »
The 5 best iPhone games like The Room
The Room has had a massive impact on the world of mobile gaming. Not only is it a brilliant adventure, it also shows how the touchscreen controls on your iPhone can be turned into something far more elegant and tactile than just a bunch of buttons... | Read more »

Price Scanner via MacPrices.net

US Cellular offers new iPhone Xs and Xs Max f...
US Cellular is offering any iPhone Xs or iPhone Xs Max for $800 off for new customers. Their promotion reduces the price of Apple’s new iPhones to a low of $199 for 64GB iPhone Xs models ranging up... Read more
MacPrices Columnist Charles W. Moore Passes A...
NEWS: 09.21.18-: Charles W. Moore (1951-2018), columnist at MacPrices, passed away this past Sunday, September 16 at home in rural Nova Scotia, a province in Canada, after fighting for years with... Read more
Get a new iPhone Xs or Xs Max for $100 off Ap...
Boost Mobile is offering the new iPhone Xs and Xs Max for $100 off from September 21st to October 9th. Their discount reduces the cost of an Xs to $899 for the 64GB models and $999 for the 64GB Xs... Read more
Final weekend for Apple’s 2018 Back-to-School...
Purchase a new Mac or iPad Pro using Apple’s Education discount, and take up to $400 off MSRP. All teachers, students, and staff of any educational institution with a .edu email address qualify for... Read more
Clearance 2017 15″ 2.9GHz MacBook Pros availa...
B&H Photo has clearance 2017 15″ 2.9GHz Touch Bar MacBook Pros available for up to $800 off original MSRP. B&H charges sales tax in NY & NJ only, and shipping is free: – 15″ 2.9GHz Touch... Read more
Apple Should’ve Created A 4-inch Smartphone W...
EDITORIAL: 09.19.18- Apple created the new iPhone XR as an entry level model catered to those on a budget and to bring the high end experience of the iPhone X to the masses at a lower price but I... Read more
Apple offering Certified Refurbished 2017 13″...
Save $230-$200 on the purchase of a 2017 13″ 2.3GHz non-Touch Bar MacBook Pro with Certified Refurbished models at Apple. In many cases, Apple’s refurbished prices are the lowest available for each... Read more
Wide range of Certified Refurbished 12″ MacBo...
Apple has a wide range of Certified Refurbished 2017 12″ Retina MacBooks available for $200-$290 off the cost of new models. Apple will include a standard one-year warranty with each MacBook, and... Read more
13″ 2.3GHz non-Touch Bar MacBook Pros on sale...
B&H Photo has new 2017 13″ 2.3GHz non-Touch Bar MacBook Pros on sale for $150-$200 off MSRP. B&H charges sales tax in NY & NJ only, and shipping is free. – 13-inch 2.3GHz/128GB Space Gray... Read more
Mac mini sales continue with models available...
B&H Photo has Mac minis on sale for $100 off MSRP including free shipping plus NY & NJ sales tax only. Prices start at only $399: – 1.4GHz Mac mini: $399 $100 off MSRP – 2.6GHz Mac mini: $599... Read more

Jobs Board

Best Buy *Apple* Computing Master - Best Bu...
**644175BR** **Job Title:** Best Buy Apple Computing Master **Location Number:** 000195-Wichita Falls-Store **Job Description:** The Core Apple Computing Master Read more
*Apple* Mobile Master - Best Buy (United Sta...
**644334BR** **Job Title:** Apple Mobile Master **Location Number:** 001091-Plattsburg-Store **Job Description:** **What does a Best Buy Apple Mobile Master Read more
Best Buy *Apple* Computing Master - Best Bu...
**644399BR** **Job Title:** Best Buy Apple Computing Master **Location Number:** 000597-Erie-Store **Job Description:** **What does a Best Buy Apple Computing Read more
Geek Squad *Apple* Master Consultation Agen...
**644182BR** **Job Title:** Geek Squad Apple Master Consultation Agent **Location Number:** 000797-Soquel-Store **Job Description:** **What does a Geek Squad Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States- Apple Blossom Mall 1850 Apple Blossom Dr Job ID:1065040 Date:Today Job Description Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.