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

Latest Forum Discussions

See All

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.