Overriding HTTP user agent for calls to -initWithContentsOfURL:

Perhaps you need to override the HTTP user agent whenever you call -initWithContentsOfURL: from classes such as NSString, NSDictionary or NSArray, or one of this method’s convenience wrappers such as +stringWithContentsOfURL:, +dictionaryWithContentsOfURL: or +arrayWithContentsOfURL:. So let’s consider how this can be accomplished under iOS.

From what I can see, there is no easy and “clean” way apart from adding a category on the classes where you need to support this and writing your own implementation of -initWithContentsOfURL: and convenience functions (with a slightly different name, of course). These implementations would use NSURLConnection‘s +sendSynchronousRequest:returningResponse:error:. Of course, as with -initWithContentsOfURL: you’d use this replacement method in a background thread to maintain UI responsiveness.

You’d have to write a reimplementation of -initWithContentsOfURL: because the first place you can change this is NSURLRequest, or more specifically, its mutable variant NSMutableURLRequest, using the -setValue:forHTTPHeaderField:. But, if you have tons of code, you probably can’t easily change it to use the new method.

So I dug in and, with a few smart tricks (such as feeding a broken non-NSURL as a NSURL to figure out which methods get called, then implementing them as necessary), I figured out which of several ways for fetching web content is actually used in NSString‘s implementation of -initWithContentsOfURL:. These could have been NSURLConnection or some low level messing with CFNetwork.

It turned out not to matter since NSURLRequest is generated out of the NSURL passed to the method. Customizing the user agent turned out to be just a matter of taking all NSURLRequests, forcing them to become mutable copies in form of instances of NSMutableURLRequest during the initializer and setting the user agent at that time. Specific initializer appearing in iOS implementation used in iOS 5 Simulator that ships with Xcode 4.2.1 appears to be -initWithURL:cachePolicy:timeoutInterval:.

It’s an enormous hack, but I decided to simply swizzle this method out. Swizzling NSURLConnection‘s class method +sendSynchronousRequest:returningResponse:error: did not appear to work – the original method still got called despite my best efforts to figure out what went wrong with swizzling, so I gave up on it. If you can see a mistake in my class swizzling code, please tell me about it in the comments section below.

I definitely have no idea whether or not your app will be rejected for this, but from what I know, method swizzling is not illegal.

//  NSURLRequest+UserAgentFix.m

#define YOUR_USER_AGENT @"Your User Agent"
#import "NSURLRequest+UserAgentFix.h"
#import "NSObject+ISSwizzling.h"
@implementation NSURLRequest (UserAgentFix)
+(void)load
{
    [self swizzleMethod:@selector(initWithURL:cachePolicy:timeoutInterval:)
             withMethod:@selector(initWithURL2:cachePolicy:timeoutInterval:)];
}
-(id)initWithURL2:(NSURL *)URL cachePolicy:(NSURLRequestCachePolicy)cachePolicy timeoutInterval:(NSTimeInterval)timeoutInterval
{
    self = [self initWithURL2:URL cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
    
    if(!self)
        return nil;
    
    if([self class] == [NSURLRequest class])
        self = [self mutableCopy];
    
    if([self class] == [NSMutableURLRequest class])
    {
        NSMutableURLRequest * req = self;
        [req setValue:YOUR_USER_AGENT forHTTPHeaderField:@"User-Agent"];
    }
    
    return self;
}
@end

// NSURLRequest+UserAgentFix.h
#import <Foundation/Foundation.h>

@interface NSURLRequest (UserAgentFix)

@end
// NSObject+ISSwizzling.h
#import <Foundation/Foundation.h>

@interface NSObject (ISSwizzling)
+ (BOOL)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector;
+ (BOOL)swizzleClassMethod:(SEL)origSelector withMethod:(SEL)newSelector;

@end
// NSObject+ISSwizzling.m
#import <objc/runtime.h>
#import "NSObject+ISSwizzling.h"

@implementation NSObject (ISSwizzling)
+ (BOOL)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
    Method origMethod = class_getInstanceMethod(self, origSelector);
    Method newMethod = class_getInstanceMethod(self, newSelector);
    
    if (origMethod && newMethod) {
        if (class_addMethod(self, origSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            class_replaceMethod(self, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
        } else {
            method_exchangeImplementations(origMethod, newMethod);
        }
        return YES;
    }
    return NO;
}
+ (BOOL)swizzleClassMethod:(SEL)origSelector withMethod:(SEL)newSelector
{
    Method origMethod = class_getClassMethod(self, origSelector);
    Method newMethod = class_getClassMethod(self, newSelector);
    
    Class class = object_getClass((id)self);

    if (origMethod && newMethod) {
        if (class_addMethod(class, origSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            class_replaceMethod(class, newSelector, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
        } else {
            method_exchangeImplementations(origMethod, newMethod);
        }
        return YES;
    }
    return NO;
}

@end

Tested on iOS 5 Simulator with NSString‘s +stringWithContentsOfURL:.

NSURLConnection submits GET instead of POST with HTTP 301 Moved Permanently error

If NSURLConnection seems to submit via GET instead of via POST, you may want to check whether server responded with HTTP 301 code. It appears that NSURLConnection forgets all about having to POST if it’s redirected. Strange.

Note that I’m currently using RestKit, so it may be a bug in RestKit, too. Doesn’t seem that way.

Autosaving Core Data managed object context

This has nothing to do with Lion autosaving. It has everything to do with the fact that Apple’s template for Core Data saves the managed object context only at exit.

I’m proposing the following solution. Note that while you’re at it, you may want to move most of Core Data related code that Apple’s “shoebox” Core Data template puts in the AppDelegate. Put it in a singleton class called Database. Expose +sharedDatabase, and #define DB [Database sharedDatabase] in the header. This was somewhat unrelated, but it’s worth mentioning.

Back to autosaving.

We’ll use NSNotificationCenter and we’ll observe for the notification that our managed object context has changed.

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(handleManagedObjectContextChange:) 
                                                 name:NSManagedObjectContextObjectsDidChangeNotification 
                                               object:managedObjectContext];

Then we’ll save after some time, but ignore any errors. Delay is important because notification occurs while the context is dirty, and you cannot really save it at that time. Scheduling a timer is a good way to delay saving a bit to occur later when the execution comes to the runloop. Plus, it also allows us to group several changes by letting us cancel the timer in case another change flows in.

I added an NSTimer *saveDelayTimer to instance variables of my Database class. If you are keeping the managed object context in your app delegate, you can add it there, too.

So let’s take a look at implementations of notification handler, and timer handler.

-(void)handleManagedObjectContextChange:(NSNotification*)note
{
    /*
    NSSet *updatedObjects = [[note userInfo] objectForKey:NSUpdatedObjectsKey];
    NSSet *deletedObjects = [[note userInfo] objectForKey:NSDeletedObjectsKey];
    NSSet *insertedObjects = [[note userInfo] objectForKey:NSInsertedObjectsKey];
     */
    
    [saveDelayTimer invalidate];
    [saveDelayTimer release];
    saveDelayTimer = [[NSTimer scheduledTimerWithTimeInterval:1
                                                       target:self
                                                     selector:@selector(quickSave:)
                                                     userInfo:nil
                                                      repeats:NO] retain];
}
-(void)quickSave:(id)userInfo
{
    // save without UI-displayed errors
    NSError *error = nil;
    [managedObjectContext save:&error];
    if(!error)
        NSLog(@"Quicksave successful");
    else
        NSLog(@"Quicksave failed: %@", error);
    
    [saveDelayTimer invalidate];
    [saveDelayTimer release];
    saveDelayTimer = nil;
}

UPDATED Oct 5th 2011, 13:16

However, this is bad.

Upon saving managedObjectContext, any currently-being-edited, but bound-via-Cocoa Bindings text fields will get unfocused. Let’s keep the focus, text selection and scroll offset!

-(void)quickSave:(id)userInfo
{
    // store focus and selection
    IRAppDelegate *appDelegate = [NSApp delegate];
    NSTextField * focusedTextField = nil;
    NSTableView * focusedTableView = nil;
    NSInteger focusedTableViewColumn = 0, focusedTableViewRow = 0;
    NSRange selection;
    NSRect visibleRect;
    if([appDelegate.mainWindowController.window.firstResponder isKindOfClass:[NSText class]])
    {
        NSText * textBox = (NSText*)appDelegate.mainWindowController.window.firstResponder;
        NSTextField * textField = [textBox parentTextField];
        if(textField)
        {
            // there is a text field that's focused
            focusedTextField = textField;
            selection = [textBox selectedRange];
            visibleRect = [textBox visibleRect];
        }
        NSTableView * tableView = [textBox parentTableView];
        if(tableView)
        {
            // there is a table view that's focused
            focusedTableView = tableView;
            focusedTableViewColumn = [tableView editedColumn];
            focusedTableViewRow = [tableView editedRow];
            selection = [textBox selectedRange];
            visibleRect = [textBox visibleRect];
        }
    }
    
    [appDelegate.mainWindowController.window endEditingFor:nil];

    
    // save without UI-displayed errors
    NSError *error = nil;
    [managedObjectContext save:&error];
    if(!error)
        NSLog(@"Quicksave successful");
    else
        NSLog(@"Quicksave failed: %@", error);
    
    [saveDelayTimer invalidate];
    [saveDelayTimer release];
    saveDelayTimer = nil;
    
    // restore selection
    [focusedTextField becomeFirstResponder];
    [[focusedTextField currentEditor] setSelectedRange:selection];
    [[focusedTextField currentEditor] scrollRectToVisible:visibleRect];

    [focusedTableView becomeFirstResponder];
    [focusedTableView editColumn:focusedTableViewColumn row:focusedTableViewRow withEvent:nil select:YES];
    [[focusedTableView currentEditor] setSelectedRange:selection];
    [[focusedTableView currentEditor] scrollRectToVisible:visibleRect];
}

This uses a small category for finding the owner of NSText (the actual textbox that appears when you begin editing table view or a text field).

// NSText+IRFindParentTextOwner.h

#import 

@interface NSText(IRFindParentTextOwner)
-(NSTextField*)parentTextField;
-(NSTableView*)parentTableView;
@end
// NSText+IRFindParentTextOwner.m

#import "NSText+IRFindParentTextOwner.h"

@implementation NSText(IRFindParentTextOwner)
-(NSTextField*)parentTextField
{
    NSText *textBox = self;
    
    for (NSResponder *parent = textBox.nextResponder; parent; parent = parent.nextResponder) 
    {
        if([parent isKindOfClass:[NSTextField class]])
        {
            return (NSTextField*)parent;
        }
    }
    return nil;
}

-(NSTableView*)parentTableView
{
    NSText *textBox = self;
    for (NSResponder *parent = textBox.nextResponder; parent; parent = parent.nextResponder) 
    {
        if([parent isKindOfClass:[NSTableView class]])
        {
            return (NSTableView*)parent;
        }
    }
    return nil;
}

@end

Getting year, month and day out of NSDate

For some reason, Apple didn’t provide an easy way to extract day, month and year from an NSDate. Probably because they think OS X shouldn’t be Western-centric and presume a Gregorian calendar (although they could have simply extracted this data from current locale). Here is a simple category that allows just that.

// NSDate+IVDateComponents.h

#import 

@interface NSDate (IVDateComponents)

@property (nonatomic, readonly) NSInteger year;
@property (nonatomic, readonly) NSInteger month;
@property (nonatomic, readonly) NSInteger day;
@end
// NSDate+IVDateComponents.m

#import "NSDate+IVDateComponents.h"


@implementation NSDate (IVDateComponents)
-(NSInteger)year
{
	unsigned units = NSYearCalendarUnit;
	NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
	NSDateComponents *components = [calendar components:units fromDate:self];
	
	return [components year];
}
-(NSInteger)month
{
	unsigned units = NSMonthCalendarUnit;
	NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
	NSDateComponents *components = [calendar components:units fromDate:self];
	
	return [components month];
}
-(NSInteger)day
{
	unsigned units = NSDayCalendarUnit;
	NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
	NSDateComponents *components = [calendar components:units fromDate:self];
	
	return [components day];
}
@end

Note that this code is very Western-centric and always presumes the Gregorian calendar. Sue me.

Getting WiX to upgrade existing MSI product

I’ve previously built installers using MakeMSI. It was fun. However, while submitting to an online store, the installer was rejected due to a problem with uninstallation. Since I have no idea what might be going wrong, and there isn’t much one can do to adjust uninstallation with MakeMSI, I decided to go another route, and I’ve decided to try out WiX – Windows Installer XML. In the process, I’ve learned much more about MSI than I wanted to know, and than I knew with MakeMSI (although that was already more than I wanted to know).

To upgrade an existing product, MSI installers use something called “upgrade codes”, versions and languages. MSI installers are databases plus CAB archives packed together. Table called “Upgrade” defines conditions under which a MSI will be upgraded: the minimum version, maximum version, language and action taken (plus some more attributes). This table is used by an action called “FindRelatedProducts“. Actions taken are specified in tables such as “InstallExecuteSequence” and “InstallUISequence”. These tables specify the “Action”, “Sequence” (smaller sequence means earlier execution) and the “Condition”. You can view all these tables using a tool called Orca.

That’s what MSI does. There is a reason I talked about this; please bear with me. However, we jsut want to get WiX to fill out the tables properly. This entire post is my understanding of something called a “major upgrade”.

First of all, each product has an ID and an upgrade code. A product contains a package which may or may not have its ID (I omitted it). You will want the product ID to be autogenerated – replace it with a *. If the product ID does not change, Windows Installer will complain with: “Another version of this product is already installed”.

Upgrade code must be unique per product, and must remain the same in future upgrades. This, along with the version (also specified in the product), is actually used to track multiple product versions. First three segments of a version are used by Windows Installer, so “1.0.0.0” and “1.0.0.1” are the same, but “1.0.0.0” and “1.0.1.0” are not the same to Windows Installer.

To generate “FindRelatedProducts” action, you need to add the following somewhere in your :


So, having set the product ID to “*”, having set the upgrade code to what I used in MakeMSI, and having incremented version, the thing still does not work.

In cases like this, you will want to log the installation:

msiexec /lvx* log.txt /i game.msi

This specifies: “Hey, I’d like to log verbosely, extra debugging information, and all other log messages. Save this to log.txt. Run installation of game.msi.”

In there, I found this:

MSI (c) (10:20) [13:55:10:955]: Doing action: FindRelatedProducts
MSI (c) (10:20) [13:55:10:955]: Note: 1: 2205 2:  3: ActionText 
Action 13:55:10: FindRelatedProducts. Searching for related applications
Action start 13:55:10: FindRelatedProducts.
MSI (c) (10:20) [13:55:10:959]: FindRelatedProducts: current install is per-user.  Related install for product 'GUID HERE' is per-machine.  Skipping...
MSI (c) (10:20) [13:55:10:959]: FindRelatedProducts: current install is per-user.  Related install for product 'GUID HERE' is per-machine.  Skipping...
Action ended 13:55:10: FindRelatedProducts. Return value 1.

As you can see, default installer built by WiX installs per-user, while default installer built by MakeMSI installs per-machine. A per-user installer will not upgrade a per-machine product.

We need to set the package’s install scope“:


I’ll try to update this post with the finished .wix later on. Remind me if I forget to.

References:

Note that above links might NOT be first parts of the articles.