Async Google Spellcheck API Adaptor for TinyMCE

I recently added TinyMCE to a project in order to provide a stripped-down rich text editor with bold, italic and underline capability to a project. I discovered that the spell check functionality either required a client-side plugin for IE or a server-side implementation JSON RPC implementation called by TinyMCE via Ajax. Unfortunately, the only implementations for the server side provided by the TinyMCE project are in PHP and my project is in ASP.Net MVC 4.

Looking at the PHP implementations, one option is to adapt the Google Spellcheck API — which I didn’t even know existed. Basically this API allows you to post an XML document that contains a list of space-delimited words and get back a document which defines the substrings that are misspelled.

Using some examples of how the API works on the Google side, I was able to throw together a class that invokes it using the new async/await pattern in C# to create a Google Spellcheck API client that doesn’t block while wanting for its result.

using System;
using System.IO;
using System.Text;
using System.Net;
using System.Xml;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Diagnostics;

namespace WolfeReiter.Web.Utility
{
/*
 * http post to http://www.google.com/tbproxy/spell?lang=en&hl=en
 * 
 * Google spellcheck API request looks like this.
 * 
 * <?xml version="1.0" encoding="utf-8" ?> 
 * <spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">
 * <text>Ths is a tst</text>
 * </spellrequest>
 * 
 * The response look like ...
 * 
 * <?xml version="1.0" encoding="UTF-8"?>
 * <spellresult error="0" clipped="0" charschecked="12">
 * <c o="0" l="3" s="1">This Th's Thus Th HS</c>
 * <c o="9" l="3" s="1">test tat ST St st</c>
 * </spellresult>
 */

    public class GoogleSpell
    {
        const string GOOGLE_REQUEST_TEMPLATE = "<?xml version=\"1.0\" encoding=\"utf-8\" ?><spellrequest textalreadyclipped=\"0\" ignoredups=\"0\" ignoredigits=\"1\" ignoreallcaps=\"1\"><text>{0}</text></spellrequest>";

        public async Task<IEnumerable<string>> SpellcheckAsync(string lang, IEnumerable<string> wordList)
        {
            //convert list of words to space-delimited string.
            var words = string.Join(" ", wordList);
            var result = (await QueryGoogleAsync(lang, words));

            var doc = new XmlDocument();
            doc.LoadXml(result);

            // Build misspelled word list
            var misspelledWords = new List<string>();
            foreach (var node in doc.SelectNodes("//c"))
            {
                var cElm = (XmlElement)node;
                //google sends back bad word positions to slice out of original data we sent.
                try
                {
                    var badword = words.Substring(Convert.ToInt32(cElm.GetAttribute("o")), Convert.ToInt32(cElm.GetAttribute("l")));
                    misspelledWords.Add(badword);
                }
                catch( ArgumentOutOfRangeException e)
                {
                    Trace.WriteLine(e);
                    Debug.WriteLine(e);
                }
            }
            return misspelledWords;
        }

        public async Task<IEnumerable<string>> SuggestionsAsync(string lang, string word)
        {
            var result = (await QueryGoogleAsync(lang, word));

            // Parse XML result
            var doc = new XmlDocument();
            doc.LoadXml(result);

            // Build misspelled word list
            var suggestions = new List<string>();
            foreach (XmlNode node in doc.SelectNodes("//c"))
            {
                var element = (XmlElement)node;
                if(!string.IsNullOrWhiteSpace(element.InnerText))
                {
                    foreach (var suggestion in element.InnerText.Split('\t'))
                    {
                        if (!string.IsNullOrEmpty(suggestion)) { suggestions.Add(suggestion); }
                    }
                }
            }

            return suggestions;
        }

        async Task<string> QueryGoogleAsync(string lang, string data)
        {
            var scheme     = "https";
            var server     = "www.google.com";
            var port       = 443;
            var path       = "/tbproxy/spell";
            var query      = string.Format("?lang={0}&hl={1}", lang, data);
            var uriBuilder = new UriBuilder(scheme, server, port, path, query);
            string xml     = string.Format(GOOGLE_REQUEST_TEMPLATE, EncodeUnicodeToASCII(data));

            var request           = WebRequest.CreateHttp(uriBuilder.Uri);
            request.Method        = "POST";
            request.KeepAlive     = false;
            request.ContentType   = "application/PTI26";
            request.ContentLength = xml.Length;

            // Google-specific headers
            var headers = request.Headers;
            headers.Add("MIME-Version: 1.0");
            headers.Add("Request-number: 1");
            headers.Add("Document-type: Request");
            headers.Add("Interface-Version: Test 1.4");

            using (var requestStream = (await request.GetRequestStreamAsync()))
            {
                var xmlData = Encoding.ASCII.GetBytes(xml);
                requestStream.Write(xmlData, 0, xmlData.Length);

                var response = (await request.GetResponseAsync());
                using (var responseStream = new StreamReader(response.GetResponseStream()))
                {
                    return responseStream.ReadToEnd();
                }
            }
        }

        string EncodeUnicodeToASCII(string s)
        {
            var builder = new StringBuilder();
            foreach(var c in s.ToCharArray())
            {
                //encode Unicode characters that can't be represented as ASCII
                if (c > 127) { builder.AppendFormat( "&#{0};", (int)c); }
                else { builder.Append(c); }
            }
            return builder.ToString();
        }

    }
}

The GoogleSpellChecker class below exposes two methods: SpellcheckAsync and SuggestionsAsync.

My MVC Controller class exposes this functionality to the TinyMCE by translating JSON back and forth to the GoogleSpell class.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using System.Web.Mvc;
using WolfeReiter.Web.Utility;

namespace MvcProject.Controllers
{
    public class TinyMCESpellcheckGatewayController : AsyncController
    {
        [HttpPost]
        public async Task<JsonResult> Index(SpellcheckRequest model)
        {
            var spellService = new GoogleSpell();
            IEnumerable<string> result = null;
            if(string.Equals(model.method, "getSuggestions", StringComparison.InvariantCultureIgnoreCase))
            {
                result = (await spellService.SuggestionsAsync(model.@params.First().Single(), model.@params.Skip(1).First().Single()));
            }
            else //assume checkWords
            {
                result = (await spellService.SpellcheckAsync(model.@params.First().Single(), model.@params.Skip(1).First()));
            }
            string error = null;
            return Json( new { result, id = model.id, error } );
        }

        //class models JSON posted by TinyMCE allows MVC Model Binding to "just work"
        public class SpellcheckRequest
        {
            public SpellcheckRequest()
            {
                @params = new List<IEnumerable<string>>();
            }
            public string method { get; set; }
            public string id { get; set; }
            public IEnumerable<IEnumerable<string>> @params { get; set; }
        }
    }
}

Integrating the above controller with TinyMCE is straightforward. All that needs to happen is include the “spellchecker” plugin, the “spellchecker” toolbar button and set the spellchecker_rpc_url to point to the controller.

/*global $, jQuery, tinyMCE, tinymce */
/// <reference path="jquery-1.8.3.js" />
/// <reference path="jquery-ui-1.8.24.js" />
/// <reference path="modernizr-2.6.2.js" />
/// <reference path="tinymce/tinymce.jquery.js" />
/// <reference path="tinymce/tiny_mce_jquery.js" />
(function () {
    "use strict";

    $(document).ready(function () {

        $('textarea.rich-text').tinymce({
            mode: "exact",
            theme: "advanced",
            plugins: "safari,spellchecker,paste",
            gecko_spellcheck: true,
            theme_advanced_buttons1: "bold,italic,underline,|,undo,redo,|,spellchecker,code",
            theme_advanced_statusbar_location: "none",
            spellchecker_rpc_url: "/TinyMCESpellcheckGateway", //<-- point TinyMCE to GoolgeSpell adaptor controller
            /*strip pasted microsoft office styles*/
            paste_strip_class_attributes: "mso"
        });
       
    });
}());

That’s all there is to it. Here’s how TinyMCE renders on a <textarea class=”rich-text-“></textarea>.

Tinymce spellcheck

Advertisements

Solving Very Slow Visual Studio Build Times in VMWare

ClockIn 2009, we chose 15″ MacBook Pro BTO over Dell, HP and Lenovo offerings. Apple offered us the best hardware and equivalent lease terms but with much simpler servicing done by ETF rather than (often incorrect) paper statements and checks. 99% of the time we ran this MacBooks with Windows 7 under Boot Camp. When I started doing some iOS development, I ran Snow Leopard and Xcode under VirtualBox and when I got fed up with the flakiness, I used VMware Workstation. OS X didn’t run very well under virtualization mostly because accelerated Quartz Extreme drivers don’t exist for VMware Workstation. Still, it actually worked well enough and was much more convenient than dual booting — which is such a huge time suck. When the lease period ended, we renewed with Apple and decided to just use OS X as the host environment for a variety of reasons:

The transition went very smoothly. I was using a VM with Windows Server 2008 and Visual Studio 2010 for primary .NET web development. Configuring IIS Express to serve outside of localhost bound to a host-only adapter is great for cross-browser testing, but it can be even more useful to enable remote access to Fiddler and point external browsers to the Fiddler proxy running in the VM to get both client debugging and HTTP sniffing at the same time. All of this was working out great.
 
At the start of October I downloaded Windows Server 2012 and Visual Studio 2012 and created a new development VM. In order to minimize disk storage, I moved my source tree to a folder in the host OS X environment and exposed it via the “Shared Folders” feature of VMWare Fusion 5.x to Windows. At the same time I started working on a new project.

 

The Slowening

Once the project grew to 20, 30 and 50k lines of C# code, the build times started to become horrifically slow. When combined with running unit tests, build became a big time for a bathroom break or cup of coffee event like building a project in C 15 years ago. Builds would show cdc.exe running at ~50% CPU (e.g. 1 core) and some other stuff totaling ~70% CPU. The VM was not memory bound and network IO was minimal. This was my first substantial project using Code First EF, so I thought maybe the complex object graph is just hard for the C# compiler to deal with.
 
After a few weeks of increasingly painful build times, I was looking at breaking my solution up so that I could build against pre-compiled DLLs — anything to make it go faster. I ran across a post on SuperUser:

… (Full disclosure: I work on VMware Fusion.)

I have heard that storing the code on a “network” drive (either an HGFS share or an NFS/CIFS share on the host, accessed via a virtual ethernet device) is a bad idea. Apparently the build performance is pretty bad in this configuration.

Oh really? Hmmm. Maybe it isn’t that my class libraries are so complex but something else is going on. Here are some empirical measurements of rebuild time of an actual solution:

VMWare shared folder: 	50 sec
OS X SMB share: 	18 sec
within virtual disk:	 9 sec

Wow. Problem solved. Incremental builds are basically instantaneous and a full rebuild takes 9 seconds when the code is hosted inside the VM image. Not only does hosting the source code within the VM virtual disk make the build go 5.5x faster, the CPU time of csc.exe goes way down. I don’t know how the VMWare shared folder is implemented. It appears as a mapped drive to a UNC name to Windows but it is very slow. Moral of the story is just don’t host your source code on the host machine with VMWare. The performance penalty is just not worth it. If you need to share the source code tree inside the VM with the host OS, create a file share from the VM to the host over a host-only adapter.

 

Update

I can confirm this is still a problem in VMWare Fusion 6. I’m hoping maybe the new SMB implementation in Mavericks might greatly improve the performance of sharing source code from the host OS to the VM.

Update

I just updated my virtual machine to Windows Server 2012 R2 (aka Windows 8.1 server). It is running on VMWare Fusion 6. The build time of this large project is now 15 seconds over VMWare Shared Folders. Significant remaining issue is that Visual Studio uses a whole core of CPU to do nothing — just having a large solution open, not editing anything.

Pimping my 2011 MacBook Pro to 16GB RAM Running at 1600MHz

I am a fairly heavy user of memory-hungry VMWare VMs. I was running into a problem with excessive paging slowing down the host OS or even not being able to launch all the VMs I needed to simultaneously due to memory limitations of my pretty damn new 8GB RAM BTO late 2011 15″  Sandy Bridge MacBook Pro system.

The late 2011 Sandy Bridge 15″ MacBook Pro machines come with 1300MHz 9-9-9 non-ECC DDR3 SO-DIMM RAM configurable up to 8GB in a BTO configuration. The 2012 Ivy Bridge models come with RAM operating at 1600MHz and the Retina MacBook Pro has a 16GB BTO option. The chipsets are similar and I was pretty sure that the non-Retina model can support 16GB of RAM and the Sandy Bridge models can run at the 20% faster 1600MHz just like the Ivy Bridge ones.

I had some trouble finding 16GB 9-9-9  latency non-ECC DDR3 SO-DIMM RAM kits on the aftermarket and none were labelled as “for MacBook Pro”. There are a lot of options at 1333MHz from Kingston, OWC, Corsair, Crucial and iFixit but at 1600MHz there are slim pickings. I suspect that the reason that the 2011 MacBook Pro ships with 1300MHz memory is a cost/availability issue.

Corsair has a kit of 16GB with slower 10-10-10 latency. I’m not sure what the implication is of 10-10-10 latency at 1600MHz vs. 9-9-9 at 1300MHz but I know that Apple specs 9-9-9 memory in their systems, so I soldiered on. The only kit that had specs I was looking for was the HyperX PNP 1600MHz 9-9-9 16GB DDR3 non-ECC SO-DIMM kit from Kingston which I got from Amazon.

Memory access schematic from support.apple.com

Installation is pretty easy. You need a high quality static dissipative Phillips #00 screw driver to remove the 10 screws without damaging the heads. Once the back is off the computer, the memory slots are easily accessible in the center of the machine.

As you can see, the Kingston kit worked. OS X Mountain Lion recognizes the 16GB of RAM at 1600MHz and is quite happy. I have a lot of memory head room now. I can run all of my VM workloads simultaneously with iTunes, Pixelmator, OmniGraffle, MonoDevelop, Xcode, etc., etc. all running at once without any hiccups whatsoever. Overall, I’m very happy with this experiment. The Kinston kit screams.

Update:

I got a Geekbench score of 11020 with the new RAM installed.

Delete Unwanted Windows Apps from Launch Pad with VMWare Fusion

VMWare Fusion has an option to “Run Windows Apps from your Mac’s Applications folder”. This option also integrates with Launch Pad. When playing around with turning this on and off, I got into a situation where there were leftover Windows app icons that Fusion refused to delete.

It turns out that part of the Fusion magic is to put entries into the SQLite database that Dock.app uses to decide what to show in Lanch Pad. You can use the sqlite3 editor at the command-line in terminal to edit the database.

Heres’ the list of columns:

item_id|title|bundleid|storeid|category_id|moddate|bookmark

The sqlite3 command-line app lets you execute arbitrary SQL statements against a SQLite database. In my case, I had turned off the app integration feature with Launch Pad but had stray icons. Here’s a terminal SQL command to clear out any stray magic VMware Fusion virtual icons.

cd ~/Library/Application\ Support/Dock
sqlite3 *.db “delete from apps where bundleid like ‘com.vmware.proxyApp.%’;”
sqlite3 *.db ‘delete from apps where bundleid like “com.vmware.proxyApp.%”;’
killall Dock

The killall command will cause Dock.app to restart. And after restart, all VMWare induced Windows icons will be gone from Launch Pad.

Size an MKMapView to Fit its Annotations in iOS Without Futzing with Coordinate Systems

map viewAn oddity of MKMapView is that it always defaults to showing the western hemisphere of the Earth. That’s almost certainly not what you want. Instead, you have probably set an array of id <MKAnnotation> objects which you want to appear as pins on the map. If the pins were evenly distributed over the USA, then great. Otherwise, the MapView is not at all zoomed the way you want. It baffles me a map view doesn’t just zoom itself by default when you set annotations, but it doesn’t.

One approach might be to loop over all of your annotations and figure out what the max and min x and y values are across them all. Then get the center point of the map view and do some math to convert the coordinates and scale the map view.

Alternatively, MapKit has API that is separate from the map view itself for manipulating rectangles and regions which can do the job automatically.

The main problem is that id <MKAnnotation> objects contain a CoreLocation coordinate – CLLocationCoordinate2D – which represents a GPS coordinate while MapView has an MKCoordinateRegion which has a CLLocationCoordinate2D defining its center but uses an MKCoordinateRegion span struct to define the zoom level of the map view in terms of degrees of arc.

MapKit provides a mix of object-oriented API and C functions to convert an array of CLLocationCoordinate2D structs from id <MKAnnotation> objects into an MKCoordinateRegion suitable for setting the region in a MKMapView. These transformations are straightforward and don’t require fiddling with converting between coordinate systems.

Here is the code in a map view controller

#define MINIMUM_ZOOM_ARC 0.014 //approximately 1 miles (1 degree of arc ~= 69 miles)
#define ANNOTATION_REGION_PAD_FACTOR 1.15
#define MAX_DEGREES_ARC 360
//size the mapView region to fit its annotations
- (void)zoomMapViewToFitAnnotations:(MKMapView *)mapView animated:(BOOL)animated
{ 
    NSArray *annotations = mapView.annotations;
    int count = [mapView.annotations count];
    if ( count == 0) { return; } //bail if no annotations
    
    //convert NSArray of id <MKAnnotation> into an MKCoordinateRegion that can be used to set the map size
    //can't use NSArray with MKMapPoint because MKMapPoint is not an id
    MKMapPoint points[count]; //C array of MKMapPoint struct
    for( int i=0; i<count; i++ ) //load points C array by converting coordinates to points
    {
        CLLocationCoordinate2D coordinate = [(id <MKAnnotation>)[annotations objectAtIndex:i] coordinate];
        points[i] = MKMapPointForCoordinate(coordinate);
    }
    //create MKMapRect from array of MKMapPoint
    MKMapRect mapRect = [[MKPolygon polygonWithPoints:points count:count] boundingMapRect];
    //convert MKCoordinateRegion from MKMapRect
    MKCoordinateRegion region = MKCoordinateRegionForMapRect(mapRect);
    
    //add padding so pins aren't scrunched on the edges
    region.span.latitudeDelta  *= ANNOTATION_REGION_PAD_FACTOR;
    region.span.longitudeDelta *= ANNOTATION_REGION_PAD_FACTOR;
    //but padding can't be bigger than the world
    if( region.span.latitudeDelta > MAX_DEGREES_ARC ) { region.span.latitudeDelta  = MAX_DEGREES_ARC; }
    if( region.span.longitudeDelta > MAX_DEGREES_ARC ){ region.span.longitudeDelta = MAX_DEGREES_ARC; }
    
    //and don't zoom in stupid-close on small samples
    if( region.span.latitudeDelta  < MINIMUM_ZOOM_ARC ) { region.span.latitudeDelta  = MINIMUM_ZOOM_ARC; }
    if( region.span.longitudeDelta < MINIMUM_ZOOM_ARC ) { region.span.longitudeDelta = MINIMUM_ZOOM_ARC; }
    //and if there is a sample of 1 we want the max zoom-in instead of max zoom-out
    if( count == 1 )
    { 
        region.span.latitudeDelta = MINIMUM_ZOOM_ARC;
        region.span.longitudeDelta = MINIMUM_ZOOM_ARC;
    }
    [mapView setRegion:region animated:animated];
}

- (void)viewWillAppear:(BOOL)animated
{
    [self zoomMapViewToFitAnnotations:self.mapView animated:animated];
    //or maybe you would do the call above in the code path that sets the annotations array
}

Targeted Marketing Considered Harmful

I’m concerned that the trend of monetization on the Web and in the Android ecosystem is overwhelmingly based on marketing revenue for free services. In these transactions, the product is not the app or website. You are the product being sold. The product you use is the bait to aggregate a lot of attention on the advertising that is sold and displayed through the app or website. The more information the tech company that is offering the free service knows about you, the more precisely they can target advertisements and the larger fee they can command for impressions.

This is bad for us as users.

I’m not particularly concerned about privacy today. Not yet. There may come a day when passive data about your online behavior informs things like what insurance or jobs you are eligible for. That’s not the problem I’m talking about. I’m concerned about something more insidious. What if the marketing works?

In fact, I’m sure that it does work. In order for this model of free services with advertising to work out financially, the cost of the service must be vastly smaller than the cost of the products you buy because of the marketing. Otherwise, the companies doing the marketing would not see a return on investment (ROI) commensurate with the cost of placing the advertising. The fact is, ad-supported services exist because the value of what you purchase due to being exposed to the advertising is far, far greater than the cost of the service in the first place.

The basic premise of advertising is to sell you something that you would not otherwise have purchased. It works by making you feel want something you didn’t want before. In other words, it affects your well-being and happiness. Because you want this new thing, you are less happy until you buy it.

The basic transaction of a free, ad-supported service is not trading “your attention for a free service” as suggested by Leo Laporte. The transaction is that you are trading your sense well-being and (in aggregate) your money – indirectly – for a free service. In aggregate, this is a significant effect but we don’t notice because we are constantly bombarded with advertising. The better and more targeted the advertising, the worse it is. The assertion that more targeted advertising is better for both advertiser and recipient is totally wrong. Its better for the advertiser and worse for the recipient because it is more effective at making you want the thing and therefore less happy with what you have and who you are today.

I noticed this for the first time when I returned to the US from Africa after Peace Corps where I was exposed to essentially zero advertising. I have found that I have been able to greatly reduce stress and anxiety in my own life by doing simple things to limit my exposure to advertising – the most basic was deciding to eliminate cable and broadcast television 12 years ago. We still enjoy TV shows but we buy them on DVD or Amazon streaming which are both essentially ad-free platforms. In general, I prefer freemium services like Flickr or outright pay-for services and apps because the relationship that I want is to be the customer and not the product. With the exception of digital periodicals like the Economist and NY Times apps, pay-for services are almost exclusively ad-free. That makes sense because the user is the customer not the product.

I think its high time the Internet business community comes up with some new and better strategies for monetization than tracking and ever more targeted advertising. Ad-supported is not purely benign. It’s a strategy that turns your users into your product. It puts internet companies in the business of ever more invasive profiling of their users. The pressure to aggregate data about users inevitably leads to breaches of trust and repeated bad press. After a sufficient kerfuffle, governments get involved and will start imposing regulations. Ultimately, it’s a very dangerous game.

How to Create an Xcode 4.0-style Window-based Application in Xcode 4.2

I’ve decided to get up to speed on iOS programming and, to help me with that, I bought Aaron Hillegass’s iOS Programming: The Big Nerd Ranch Guid 2nd edition. I’ve listened to Aaron on podcasts and have heard that his training method is fantastic. The book does seem to be unusually well written for a tech book but the problem I immediately ran into is that it was written for Xcode 4.0 and iOS 4.3 SDK. I have Lion with Xcode 4.2 and iOS 5 SDK. That’s part of the adventure of discovery and change in tech but unfortunately Apple removed the project type that Hillegass’s examples are based on which is a major stumbling block.

Hillegass uses the “Window-based Application” template for iOS as the basis for all of his projects. That template no longer exists in Xcode. The closest thing in Xcode 4.2 is “Empty Application”. However, that’s not quite the same thing because the now-defunct “Window-based Application” template generated a main window XIB view wired to generated controller but the “Empty Application” has no XIB. In order to make any sense out of the book, you have to create the XIB and wire it up yourself. This is no big deal to anybody with a little experience but to a newb like me, it took a bit of wandering in the wilderness at the Big Nerd Ranch forums to figure out how to manually get the project into the state that the exercises in the book will work.

How to Convert a new “Empty Application” into a “Window-based Application”

Start by creating a new iOS Application project using the “Empty Application” project. This will create a very similar project to the “Window-based Application” project described in iOS Programming 2/e with just a few differences:

  1. The main controller class that gets generated is “AppDelegate” rather than “<project-name>AppDelegate”. The
  2. There is no XIB view.
  3. The window property of AppDelegate doesn’t have the IBOutlet macro to make it visible to the Interface Builder tooling.
    1. Prepare the controller to talk to the Interface Builder tooling

    In the controller class interface definition of the we need to add the IBOutlet macro so that the tooling will recognize the controller. In your controller header file, AppDelegate.h, the line to modify begins with @property and ends with *window. Add IBOutlet in front of the UIWindow type as shown below.

#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) IBOutlet UIWindow *window;

@end

Save this change.

Update the didFinishLaunchingWithOptions: method

The auto-generated first line of code in the didFinishLaunchingWithOptions: method of AppDelegate.m prevents message routing from working properly. Comment it out as below:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    //self.window = [[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]] autorelease];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

Save this change.

Create the XIB file

Right-click on the folder containing your AppDelegate files and choose “Add File”. Select “Window” from the iOS User Interface group. Choose the appropriate device family—for the initial Quiz project that is iPhone. The default file name is Window.xib which works fine or you can name it MainWindow.xib to make it just like in the book.

OS X 10.7 (Lion)-2011-11-23-11-18-56

Change the Class of the XIB file’s owner to UIApplication

Select Window.xib in the Navigator. Select the “File’s Owner” cube in the objects pane just left of the layout canvas and change the Class in the properties pane to UIApplication.

OS X 10.7 (Lion)-2011-11-23-11-34-47

 

Add an object for the controller

Filter the tool pallet on the bottom right to “Objects and Controllers” and drag and drop an Object from the toolbox onto the object pane.

OS X 10.7 (Lion)-2011-11-23-11-37-52

Set the class of the controller object

In the properties window, change the class to controller that was generated for your project by the template. That should be AppDelegate. The Object will automatically be renamed with a space added into the Pascal casing. (You can override this by putting something into the Label field of the Identity pane.)

OS X 10.7 (Lion)-2011-11-23-11-45-00

Route the XIB delegate to the controller

Now we want to wire up the MVC message routing for the view. Select the “File’s Owner” cube in the object pane and control+drag the delegate outlet to the App Delegate object.

OS X 10.7 (Lion)-2011-11-23-12-00-15

 

Route the controller object’s window outlet to the window object

Next perform a similar ctrl+drag of the window outlet from the App Delegate object to the Window object.

OS X 10.7 (Lion)-2011-11-23-12-01-08

All done

Now our Empty Application project is set up just like a Window-based Application as described in iOS Programming 2nd Edition. The file names may be a bit different but that should have no practical  effect. The AppDelegate inherits from UIResponder <UIApplicationDelegate> rather than NSObject<UIApplicationDelegate>. This doesn’t seem to be a problem, either.

Now that I have figured out how to create an wire up a main window XIB file, everything seems to work great as I follow along in the book.

%d bloggers like this: