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

Xcode 15.0.1 - Integrated development en...
Xcode includes everything developers need to create great applications for Mac, iPhone, iPad, and Apple Watch. Xcode provides developers a unified workflow for user interface design, coding, testing... Read more
Google Chrome 120.0.6099.62 - Modern and...
Google Chrome is a Web browser by Google, created to be a modern platform for Web pages and applications. It utilizes very fast loading of Web pages and has a V8 engine, which is a custom built... Read more
Dropbox 188.4.6302 - Cloud backup and sy...
Dropbox is a file hosting service that provides cloud storage, file synchronization, personal cloud, and client software. It is a modern workspace that allows you to get to all of your files, manage... Read more
djay Pro 5.0 - Transform your Mac into a...
djay Pro provides a complete toolkit for performing DJs. Its unique modern interface is built around a sophisticated integration with iTunes and Spotify, giving you instant access to millions of... Read more
Things 3.19.4 - Elegant personal task ma...
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
Sublime Text 4169 - Sophisticated text e...
Sublime Text is a sophisticated text editor for code, markup, and prose. You'll love the slick user interface, extraordinary features, and amazing performance. Features Goto Anything. Use Goto... Read more
Typinator 9.1 - Speedy and reliable text...
Typinator turbo-charges your typing productivity. Type a little. Typinator does the rest. We've all faced projects that require repetitive typing tasks. With Typinator, you can store commonly used... Read more
ESET Cyber Security 6.11.414.0 - Basic i...
ESET Cyber Security provides powerful protection against phishing, viruses, worms, and spyware. Offering similar functionality to ESET NOD32 Antivirus for Windows, ESET Cyber Security for Mac allows... Read more
Opera 105.0.4970.29 - High-performance W...
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
Quicken 7.4.1 - Complete personal financ...
Quicken makes managing your money easier than ever. Whether paying bills, upgrading from Windows, enjoying more reliable downloads, or getting expert product help, Quicken's new and improved features... Read more

Latest Forum Discussions

See All

SwitchArcade Round-Up: A Review of the ‘...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for December 6th, 2023. It’s an even quieter Wednesday than usual, and that seemed to spell disaster for having a decent-sized column for you all. I decided to dig my fingers into the... | Read more »
Vampire Survivors Among Us Crossover Eme...
Day of the Devs The Game Awards 2023 Edition just aired, and there were a plethora of announcements of great indie games and updates. I’ll have a round-up of the ones I liked the most in the SwitchArcade, but poncle just announced the first... | Read more »
‘Refind Self: The Personality Test Game’...
The last two months have been so busy that I’ve not been able to make time to play many games until recently. There are still new games coming out even as we head closer to the holidays, but I finally managed to play Playism and Lizardry’s recent... | Read more »
Experience the glory of the Northern Lig...
Dinosaur Polo Club, one of the best developer names out there, have recently announced the final update of 2023 for Mini Motorways. Instead of embracing Christmas, this event is instead inspired by one of the most beautiful natural phenomena, the... | Read more »
‘Disney Dreamlight Valley Arcade Edition...
After a bit of a delay, Disney Dreamlight Valley Arcade Edition () is now available on Apple Arcade worldwide. When Disney Dreamlight Valley Arcade Edition hit early access on PC and consoles including Nintendo Switch, I always assumed it would... | Read more »
‘Devil May Cry: Peak of Combat’ Releases...
It feels like we’ve been covering Devil May Cry: Peak of Combat (), the mobile entry in the superb Devil May Cry series, for as long as we were waiting for Devil May Cry 5. After trailers revealing gameplay, characters, controller support, betas,... | Read more »
‘Marvel Snap’ Dons Its Finest in the New...
It’s been quite a year for the card battler Marvel Snap (Free), which is still one of my favorite mobile games. There have been a bunch of interestingly-themed seasons, sometimes connected to the MCU and sometimes just doing their own thing. Plenty... | Read more »
SwitchArcade Round-Up: ‘A Highland Song’...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for December 5th, 2023. It’s a bit of a short one today since I was busy with a variety of other things, but there are several new releases for us to summarize. There are some really... | Read more »
‘Metal Slug ACA NEOGEO’ Review – Another...
Well, here we go again. The latest addition to SNK and Hamster’s mobile Arcade Archives line is none other than Metal Slug ACA NEOGEO ($3.99), a second take on a game we got a mobile version of a decade back from Dotemu. That was a fine version for... | Read more »
‘Sonic Dream Team’ Apple Arcade Review –...
What an unusual day we have arrived upon today. Now, Sonic the Hedgehog games aren’t a new thing for iOS gaming. The original Sonic the Hedgehog appeared on the classic iPod, so the Blue Blur got in the doors as fast as you would expect him to. The... | Read more »

Price Scanner via MacPrices.net

Apple’s 14-inch M3 MacBook Pros are on Holida...
Best Buy is offering a $150-$200 discount on Space Gray or Silver 14″ M3 MacBook Pros on their online store with prices available starting at $1449 ($1399 for premium My Best Buy members). Prices... Read more
Holiday Sale: 128GB iPhone 15 Pro, 15 Plus, o...
Boost Infinite, part of MVNO Boost Mobile using AT&T and T-Mobile’s networks, is offering the 128GB iPhone 15 Pro, 128GB iPhone 15 Plus, or 128GB & 256GB iPhone 15 for $60 per month including... Read more
Clearance 12.9-inch iPad Pros with M1 CPUs av...
Apple has Certified Refurbished, previous-generation, 12″ M1 iPad Pros available in their online store in a variety of configurations. Models start at $889 and range up to $350 off Apple’s original... Read more
Mac Studios with M2 Max and M2 Ultra CPUs on...
B&H Photo has standard-configuration Mac Studios with Apple’s M2 Max & Ultra CPUs in stock today and on Holiday sale for $200 off MSRP. Their prices are the lowest available for these models... Read more
B&H is offering a $150 discount on 13-inc...
B&H Photo has 13″ MacBook Airs with M2 CPUs and 256GB of storage in stock today and on Holiday sale for $150 off Apple’s MSRP, only $949. Free 1-2 day delivery is available to most US addresses.... Read more
Apple is clearing out last year’s M1-powered...
Apple has Certified Refurbished 11″ M1 iPad Pros available starting at $639 and ranging up to $310 off Apple’s original MSRP. Each iPad Pro comes with Apple’s standard one-year warranty, features a... Read more
Save $50 on these HomePods available today at...
Apple has Certified Refurbished White and Midnight HomePods available for $249, Certified Refurbished. That’s $50 off MSRP and the lowest price currently available for a full-size Apple HomePod this... Read more
New 16-inch M3 Pro MacBook Pros are on sale f...
Holiday MacBook deals are live at B&H Photo. Apple 16″ MacBook Pros with M3 Pro CPUs are in stock and on sale for $200-$250 off MSRP. Their prices are among the lowest currently available for... Read more
Christmas Deal Alert! Apple AirPods Pro with...
Walmart has Apple’s 2023 AirPods Pro with USB-C in stock and on sale for $189.99 on their online store as part of their Holiday sale. Their price is $60 off MSRP, and it’s currently the lowest price... Read more
Apple has Certified Refurbished iPhone 12 Pro...
Apple has unlocked Certified Refurbished iPhone 12 Pro models in stock starting at $589 and ranging up to $350 off original MSRP. Apple includes a standard one-year warranty and new outer shell with... Read more

Jobs Board

Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple 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
Mobile Platform Engineer ( *Apple* /AirWatch)...
…systems, installing and maintaining certificates, navigating multiple network segments and Apple /IOS devices, Mobile Device Management systems such as AirWatch, and Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.