TweetFollow Us on Twitter

When Wild Animals Bite

Volume Number: 18 (2002)
Issue Number: 10
Column Tag: Mac OS X

When Wild Animals Bite

Or How To Workaround Cocoa bugs

by Andrew C. Stone

Of the last 50 articles I have written about Cocoa and Mac OS X, I think 49 have extolled the virtues of object oriented programming and the unparalleled advantages of using the dynamic runtime of Objective C. You might think I'd been fed some KoolAid, and you might not be too far off. But effects and mileage will vary.

The question that might arise in your mind is "Aren't there any serious drawbacks to using such a high level system?". And the answer to that would be the same as the drawbacks to any complex system: "Yes - Beware of Bit Rot", the inevitable complexification of simple architectures over time as more people work on it.

Once upon a time, the entire Cocoa development system was in the hands of a few able engineers, which kept it focused and robust between system releases. However, with Mac 10.2, aka Jaguar, we've seen a new and dangerous trend. Components that have been working almost flawlessly for over ten years have been "re-plumbed" without enough real world testing, and this can cause trouble for applications which push the envelope. Sometimes the pressure to release a new version is greater than the ability to do adequate quality control, so all of this is quite forgivable.

The same features that seem like an advantage can turn into a disadvantage! Since a Cocoa application links against the runtime AppKit and Foundation frameworks, when improvements are made to a subsystem framework, your application, even unrecompiled, will take advantage of these improvements. For example, Mac OS X 10.2's text system introduced 3 new tab types: decimal, right aligned, and centered tabs. Users of our page layout and web authoring program, Create(R), now automatically have access to these features under 10.2 - even with versions compiled under 10.1. By the same token, some changes in the subsystem framework can break existing, unrecompiled applications.

Because of the relatively small number of Cocoa engineers at Apple compared to the number of traditional Carbon engineers, there has been an increasing tendency to "pollute" our Cocoa purity with underlying Carbon implementations, and consequently, unexpected results can ensue! In Object Oriented theory, all objects are so well encapsulated and isolated that one can just swap out implementations of components and everything should just work. Reality just doesn't conform to these simplistic expectations.

When Carbon Met Cocoa

In the last several years of shipping and maintaining OS X applications, I've found that most of the problems have come up where traditional Mac OS 9 technologies have been shoehorned into the Cocoa model. The interstice between them is riddled with the same ambiguities that bedevils any organization that is a hybrid of philosophies and techniques. Cocoa's AppleScript implementation is an example of this - from the outside, it looks neat and clean, but getting it to work just right is a lot tougher than normal Cocoa programming tasks.

One object whose plumbing was changed in 10.2 is the NSPrintInfo - an object which maintains information about page size, orientation, margins and selected printer. The backend Print Manager grew out of the Carbon Tioga effort and is coded in C and C++. In 10.1, you could set the page size to any value (ideal for custom page sizes) with the simple invocation:

   [myPrintInfo setPaperSize:NSMakeSize(someWidthInPoints, someHeightInPoints)];

And the printInfo dutifully obeyed. This is useful for designing large scale posters to be printed offsite or for web sites with long content. However, in Jaguar, new behavior was introduced which attempts to see if that size is "valid" for the given printer. To be fair to the Apple engineers, I can see how this might be useful. But from my point of view, it broke the existing version of Create(R)! Luckily, since our corporate philosophy is free upgrades downloadable via the Internet, some quick coding, testing and uploading would solve the problem.

This new validation behavior occurs whenever "setPaperSize:" is called, and in the case of Create(R), that's when the document is unarchived from a file. So, when you open an old Create document with a custom size, the old size is lost forever, and all of a sudden, size "A4" is chosen! What's worse is that any attempt to even read this paper size, such as with the public NSPrintInfo API call -(NSSize)paperSize or even -objectForKey on the dictionary returned from -(NSMutableDictionary)dictionary will also trigger this new validation behavior.

Redemption Song

So, what's the workaround? Somehow, we'll find Cocoa's redemption! For any flaw that comes in a shipping OS, there are ALWAYS tricky ways in Cocoa to do post-ship fixes! Once again, Objective C Categories save us from getting egg on our digital faces.

By examining the header file NSPrintInfo.h, we see a private instance variable (IVAR), _attributes, which is the actual mutable dictionary which holds all the values of the PrintInfo:

@interface NSPrintInfo : NSObject<NSCopying, NSCoding> {
    @private
    NSMutableDictionary *_attributes;
    void *_moreVars;
}

Because the IVAR is designated private, even a subclass cannot access it directly, so subclassing NSPrintInfo won't work. Only a category of the class containing the private IVAR can directly access the variable. Here's a category that can return any set value for paperSize, without triggering the unwanted validation behavior:

@interface NSPrintInfo (peek)

- (NSSize)grabOriginalSize;
@end
@implementation NSPrintInfo(peek)
- (NSSize)grabOriginalSize {
    id obj = [_attributes objectForKey:NSPrintPaperSize];
    return obj? [obj sizeValue]: NSZeroSize;
}
@end

Documents which had no custom page size will return NSZeroSize, which indicates that no extra work will be required. So, here's how we'll use the new category method:

            NSPrintInfo *printInfo = [NSUnarchiver unarchiveObjectWithData:obj];
       // a freshly unarchived PrintInfo still has its old values in the dictionary....
            if (printInfo) {
                NSSize raw = [printInfo grabOriginalSize];
                if (!NSEqualSizes(raw, NSZeroSize)) {   
         // it could have been written
                    // by a 10.1 version of Create
                   [self setUpPrintInfo:printInfo toPaperSize:raw];
                }             
                [self setPrintInfo:printInfo];
            }

Now, all we need to do is fake NSPrintInfo out so that it thinks the custom paper size is valid. To do this, I had to dig into undocumented API using class-dump as explained in several of my last few articles, and even more dreaded by an Objective C coder, into the Carbon header files! It turns out that there is a C function to make custom paper sizes programmatically, which stands to reason because the new Page Setup... panel in Jaguar has a popup option to set custom paper sizes:

OSStatus PMPaperCreate(PMPrinter printer, CFStringRef id, CFStringRef name, double width, double height, const PMPaperMargins *margins, PMPaper *paperP);

And we'll call it like this:

        OSStatus osStatus = PMPaperCreate([[pi printer] _printer], 
(CFStringRef)[NSString stringWithFormat:@"%ld",self], 
(CFStringRef)[NSString stringWithFormat:@"%2.2f X %2.2f",userW,userH],(double) paperSize.width,
(double) paperSize.height, marginPtr, &myPaper);

If PMPaperCreate() returns an OSStatus of 0, then a new PMPaper * object (which you'll need to later release after use) has been created in myPaper. The first parameter, PMPrinter, can be returned from the NSPrintInfo via undocumented NSPrintInfo API, -(PMPrinter)_printer:

[[pi printer] _printer]

Another sweet feature of Cocoa is "toll-free bridging" between Core Foundation objects, such as a CFStringRef and the equivalent Cocoa object, in this case, the NSString. We will cast the NSString parameters to CFStringRef's just to hush the compiler. The other fancy dancing that we're doing is using the pointer to memory of the document object as our uniquing strategy for the second argument, but it could be any string as long as it is a unique identifier:

(CFStringRef)[NSString stringWithFormat:@"%ld",self]

Finally, we'll name the custom page in the user's units by converting the points to their measurement units - these functions are included below.

// these functions are declared here:
#import <CoreServices/CoreServices.h>
#import <ApplicationServices/ApplicationServices.h>
typedef struct OpaquePMPaper *PMPaper;
typedef struct {
    double top;
    double left;
    double bottom;
    double right;
} PMPaperMargins;

OSStatus PMPaperCreate(PMPrinter printer, CFStringRef id, CFStringRef name, double width, double height, const PMPaperMargins *margins, PMPaper *paperP);

- (void)setUpPrintInfo:(NSPrintInfo *)pi toPaperSize:(NSSize)paperSize {
    if (floor(NSAppKitVersionNumber) <= NSAppKitVersionNumber10_1) {
        /* On a 10.1 - 10.1.x system */
   // nothing special to do...
    } else {
        /* 10.2 or later system */
        float userW = convertToUserUnits(paperSize.width);
        float userH = convertToUserUnits(paperSize.height);
        PMPaperMargins margins;
        PMPaperMargins *marginPtr = &margins;   
                        // does C suck or what?
        PMPaper *myPaper;
        // Create uses WYSIWYG full page model:
        margins.top = margins.left = margins.bottom = margins.right = 0.0;
        
        OSStatus osStatus = PMPaperCreate([[pi printer] _printer], (CFStringRef)[NSString 
        stringWithFormat:@"%ld",self], (CFStringRef)[NSString stringWithFormat:@"%2.2f X %2.2f",
        userW,userH],(double) paperSize.width,(double) paperSize.height, marginPtr, &myPaper);
        
     if (osStatus  != 0) {
      // You may want to do something else here:
      NSLog(@"Trouble creating a custom page size: %f by %f",paperSize.width, paperSize.height);
        }
    }
     // on 10.1 this just works:
    [pi setPaperSize:paperSize];
}
// getting user units from an NSUserDefault: - sometimes C is cool!
float
pointsFromUserUnits()
{
    switch([[NSUserDefaults standardUserDefaults] integerForKey:@"MeasurementUnits"]) {
            case 0: return 72.0;
            case 1: return 28.35;
            case 2: return 1.0;
            case 3: return 12.0;
            default: return 72.0;
    }
}
float
convertToUserUnits(float points)
{
    return points/pointsFromUserUnits();
}

Conclusion

So now, our printInfo will return the correct new size! Well, the dust hasn't settled entirely, so hopefully this will work and continue to work in the future. Ideally, NSPrintInfo would just "do the right thing" in terms of creating these custom page sizes. The proposed solution doesn't address custom page size uniquing and coalescing of similar-sized pages - but then, I haven't finished coding the solution entirely either! Even the mighty Cocoa has its compatibility weaknesses as it grows, but its native power can even pull the tractor out of the mud when it gets really stuck.


Andrew Stone, founder of Stone Design <www.stone.com>, spends too much time coding and not enough time gardening.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »
Explore some of BBCs' most iconic s...
Despite your personal opinion on the BBC at a managerial level, it is undeniable that it has overseen some fantastic British shows in the past, and now thanks to a partnership with Roblox, players will be able to interact with some of these... | Read more »

Price Scanner via MacPrices.net

You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more
Sunday Sale: 13-inch M3 MacBook Air for $999,...
Several Apple retailers have the new 13″ MacBook Air with an M3 CPU in stock and on sale today for only $999 in Midnight. These are the lowest prices currently available for new 13″ M3 MacBook Airs... Read more
Multiple Apple retailers are offering 13-inch...
Several Apple retailers have 13″ MacBook Airs with M2 CPUs in stock and on sale this weekend starting at only $849 in Space Gray, Silver, Starlight, and Midnight colors. These are the lowest prices... Read more
Roundup of Verizon’s April Apple iPhone Promo...
Verizon is offering a number of iPhone deals for the month of April. Switch, and open a new of service, and you can qualify for a free iPhone 15 or heavy monthly discounts on other models: – 128GB... Read more

Jobs Board

Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
Liquor Stock Clerk - S. *Apple* St. - Idaho...
Liquor Stock Clerk - S. Apple St. Boise Posting Begin Date: 2023/10/10 Posting End Date: 2024/10/14 Category: Retail Sub Category: Customer Service Work Type: Part Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.