Simon Standard is in TheMacBundles: 12 great apps for only $49.95. Click here!
Simon and other Dejal apps are also included in TheMacBundles' innovative new BYOB (Build Your Own Bundle).

Simon Standard is also in VoteBundle: 20 apps started; 40,000 users cast 200,000 votes; 10 remain, for only $39.00. Click here!

Dejal now has a Facebook page; please Like it:

development

DSActivityView updated for iOS 4

DSActivityViewI've committed a minor update to the DSActivityView open source project for iOS. See the DSActivityView introductory post for more information, including a video demo.

This update adds a new prefix to the class methods to create the activity view, to make it more clear that they return a retained object, rather than autoreleased.

So you'd now display the activity view via something like this:

[DSActivityView newActivityViewForView:self.view]

This update also includes a bug fix for iOS 4, where the activity view would appear behind the keyboard.

It is compatible with iOS 3.0 and later, including iOS 4, on iPad, iPhone and iPod touch.

You can get the project from my Dejal Open Source Subversion repository via this Terminal command:

svn checkout http://dejal.svn.beanstalkapp.com/open/DSActivityView

Or browse the source directly on the web.

Building Universal iPad/iPhone apps

Warning, developer topic... uninterested customers can skip on to the next post....

I was having difficulty getting my new app, Tweeps, working as a universal app: running natively on both iPhone and iPad from one binary.

For some reason, I was in a Mac universal mindset. For Mac apps, a universal app uses two separate targets: one for PowerPC, one for Intel. This is necessary since they are of course very different architectures, so have to be compiled separately.

In iPhone OS, that isn't the case — both iPhone and iPad have the same processor architecture, so both editions can use the same code without needing conditional compilation.

However, there are important differences. Currently, iPhone (and iPod touch) is on iPhone OS 3.1.3, whereas iPad is on OS 3.2. The iPhone can't use OS 3.2, and iPad can't use 3.1. So extra steps are required.

The way this works is to set the Base SDK to the latest one you want to use (in this case 3.2), and the Deployment Target to the earliest you want to support (3.1). You can then use any available APIs from 3.1 and earlier with impunity, and can use APIs from 3.2 if you check that they are available before using them.

The recommended way to check for a new method is to use +instancesRespondToSelector:. For example, 3.2 renames the method to hide the status bar. So to use the new method if available, or fall back to the old method, you'd write:

    if ([UIApplication instancesRespondToSelector:@selector(setStatusBarHidden:withAnimation:)])
        [[UIApplication sharedApplication] setStatusBarHidden:hiding withAnimation:YES];
    else
        [[UIApplication sharedApplication] setStatusBarHidden:hiding animated:YES];

Sometimes, you need to take alternative code paths depending on whether you're running on iPad or iPhone. So the way to do that is:

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
        ...

I used the following definition to save some typing:

#define IS_IPAD        (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)

That's all fine and good. But things get more curly when you want to use new classes. Apple's TopPaid sample code demonstrates the best way to handle this. You can load different xibs depending on which device you're running on, to set up the root views (e.g. to use a split view or a navigation controller). Each xib would load a different controller, which would contain relevant properties for each view.

There's one final gotcha that caused me trouble recently: you have to be careful about programmatically allocating newly introduced classes. If you run and it crashes with an error like the following, this is the issue:

dyld: Symbol not found: _OBJC_CLASS_$_UIPopoverController

The solution is to use NSClassFromString to resolve the class name, like so:

    Class popoverClass = NSClassFromString(@"UIPopoverController");
   
    if (popoverClass != nil)
        popoverController = [[popoverClass alloc] initWithContentViewController:contentViewController];

What got me was that this is necessary even in code only called on iPad, which I thought counter-intuitive. But it makes sense on further reflection, as the dynamic nature of ObjC means that it wants to resolve all class references when loading the bundle on startup — it doesn't know that that code won't ever get called if the current device happens to be an iPhone.

I hope this helps others having difficulty building universal iPhone/iPad apps.

More Tweeps for iPad mockups

I previously posted some mockups of an iPad edition of Tweeps, my new iPhone OS app to easily manage Twitter accounts.

But as I said, I've been struggling with coming up with a satisfactory design. My latest thought is that I shouldn't try to emulate a physical object like a notepad or book, but would be better following Apple's example with apps like Mail, and use a split view.

So here are my latest design mockups (again done in OmniGraffle).

In landscape orientation, a split view shows a list of pages on the left, and the details on the right; in this case, the main Profile page for your own account (click to see full-sized):

In portrait, the left view appears in a "popover" instead:

I'm not sure if the Edit button should be in the left or right views... but there's more room in the right, so that seems reasonable.

Here's a sample of the Following page in landscape:

What do you think? I think this is a better design, more clean and consistent — and allows me to use much the same color theme as on the iPhone, as a minor bonus.

Tweeps for iPad mockup

Now that Tweeps is available in the iPhone App Store, I'm starting work on the iPad edition.

Obviously, the iPad has a lot more screen space than the iPhone, so a different design is needed to take full advantage of this extras space. I've been thinking about iPad design concepts ever since the iPad was announced, but have yet to come up with something that entirely satisfies me.

A difficulty with coming up with a good design is that Tweeps can show any number of levels. You start with a list of your accounts, then show your profile overview, then can show a list of people you're following (for example), then delve deeper by showing the profile overview for one of them, and their followers, and so on to any number of levels. This works fine with the navigation display in the iPhone edition, where you can keep pushing views onto the screen, but is harder with a more traditional interface.

For quite a while, I've been thinking about something like the iPad Contacts app design, with a two-page book metaphor. The idea would be to display the profile details on the left page, and the avatar, web, map, following, followers etc views on the right (one at a time). It'd then flip the page when viewing a different person's information. That seems like a reasonable approach, though the very different content displays on the right seems to break the book metaphor.

The latest idea I've been exploring is more of a notepad metaphor. The idea is a single notepad page with the profile overview, and bookmark tabs (like Post-it® flags) sticking out from the right for related pages like those listed above. So you touch a tab to flip the notepad to that page. There would also be bookmark tabs on the left side to go back to the previous profile(s) or the accounts list.

Here's a rough mockup, done in the great OmniGraffle (click to see full-sized):

Don't worry about the fine details; as I said, it's very rough.

You'd tap the Following tab on the right to flip pages to the Following view, which would be similar to that in Tweeps now, except would have room to show more information about each person:

You could then go back to the profile overview via the new tab with the avatar icon on the left, or go straight to other pages via the other tabs on the right.

If you tap a row in the Following list, it'd flip the page to the profile overview for that person, and the tabs on the right would then show more information about them.

It might look better with a black background, to merge into the iPad bezel, as follows. In which case I'd eliminate the space around the edges (still shown in this mockup), providing more room for the content:

What do you think? Would this design work, or am I on the wrong track? Should I forget about trying for a real-world look? I'd love to hear other design ideas too.

In App Purchase for free iPhone apps

iPhone App StoreI've been meaning to follow up on this for the past few days. Back in March, I wrote a blog post titled "Apple, please support iPhone trial apps", where I urged Apple to reconsider their position that "free apps will always be free."

I discussed how not allowing free apps to use the new In App Purchase feature introduced in iPhone OS 3.0 was very limiting — this restriction prevented free trials of apps, as is very popular in Mac software, meaning that the only way a developer could provide a trial is to have two separate apps (e.g. Lite and Pro editions), which is more hassle for the developer and customers, including issues with migrating data.

So you can imagine how pleased I was when I learnt recently that Apple has removed this restriction. Now any app can use In App Purchase, including free ones. So now the trial distribution model is finally feasible.

There is no more need to release Lite and Pro editions. Developers can release a single application as a free download, perhaps with limited features, then enable people to purchase an upgrade to the "full" edition.

There are still some restrictions. The most common trial model for Mac software is a time-limited demo, where the application lets people evaluate it for a certain time period (e.g. 14 or 30 days), then disables some functionality if still not purchased. My Mac apps do this, as do most other "shareware" apps. This is not allowed on the iPhone App Store. iPhone apps have to be fully functional, and can't disable essential features.

But there are other things that can be done, like provide a basic set of functionality, and perhaps display ads, then the purchase provides extended functionality and removes the ads. So the app remains useful forever as a free app, but becomes more powerful (and without distracting ads) if the user chooses to purchase.

This is what I plan to do for my future iPhone apps. For the secret project I'm working on now, I'll release it as a free app, with ads and perhaps some feature limits (but nothing that impinges on the usability), then offer In App Purchase to disable the ads and extend the functionality.

It looks like it'll be quite easy to set up; for this sort of situation, it appears that the purchase can be handled entirely via Apple's server, storing the purchase in the iTunes account, enabling multiple phones used by that account to use the full app (e.g. when buying a new phone).

I haven't decided on terminology for this yet... call the purchased upgrade the "Paid" or "Premium" edition, or something else.

It'll be most interesting to see how many app developers adopt this technique. I've seen a few so far, and expect it to be quite popular, especially with the indie developers who are used to this kind of distribution model.

But, thank you Apple!

A fork of DSActivityView: WTFeedbackView

iPhone developers: you may have seen my DSActivityView open source project. Another developer was inspired by it, and created his own variation, WTFeedbackView, with support for progress bars, among other changes.

Here's his introduction:

WTFeedbackView is a class to display a HUD-like view with either an activity indicator view or a progress view. It's based on DSActivityView by David Sinclair (http://www.dejal.com/developer/dsactivityview), with some significant additions and modifications.

More specifically, WTFeedbackView offers:

  • Client access through a single class, by means of class methods only, for all features;
  • Changes to the text being shown trigger an animation that resizes the HUD view appropriately;
  • Three built-in styles (like DSActivityView):
    • Simple style: displays a transparent view containing an activity indicator view next to the text explaining the ongoing activity;
    • Bezel style: displays a dark semi-transparent view containing either an activity indicator view or a progress view, above the text explaining the ongoing activity;
    • Keyboard style: same as the Bezel style, but covering only the keyboard;
  • Three built-in kinds:
    • Activity kind: displays only an activity indicator view, plus the text explaining the ongoing activity;
    • Progress kind: displays only a progress view, plus the text explaining the ongoing activity;
    • Flexible kind: contains both an activity indicator view and a progress view (plus the text explaining the ongoing activity), but displays only one at a time, on demand. This is useful when the ongoing activity has parts whose lengths are sometimes known and sometimes unknown. Rather than create a new feedback view for each part, a single one can be used, minimizing screen distractions;
  • Easy updating of the progress view, through a class method +updateProgress: (CGFloat) progress;
  • Thread-safety where needed. For instance, +updateProgress: can be safely invoked from a background thread;
  • Possibility to subclass WTFeedbackView to create custom feedback views having the general behavior of WTFeedbackView but looking differently.

Although iPhoneOS 3.x isn't a requirement, WTFeedbackView and the demo were compiled using iPhoneOS 3.1.2. They were *not* tested on any version prior to 3.x, so don't assume they will work with 2.x.

Enjoy!
Wagner

I'm hosting it for him: download now.

iPhone Open Source: detect tap outside a button like table's Delete

Sorry customers, another development blog post. :)

For a new iPhone app I'm working on (shh), I have a button that I wanted to behave like the Delete button in a table view. You know, when you tap the delete toggle to the left of a cell, a red Delete button appears. And tapping anywhere other than that button will hide it without doing anything else:

Table Delete button
(Contacts app)

I couldn't see any obvious way to do it, so asked on the iPhone Developer forums, and got a helpful reply suggesting a UIWindow subclass, overriding -sendEvent:.

I tried implementing that, but what I really wanted was to override -hitTest:withEvent:, since I wanted to block taps on views other than a specific button, and the documentation says one should always invoke the superclass of -sendEvent:.

Then I noticed that -hitTest:withEvent: is actually defined in UIView, and further experimenting with the table Delete feature showed that it appears to be implemented UITableView, since the cancel tap behavior only occurs in the table, not the navigation bar or toolbar. Besides, implementing in a UIView subclass is more focal, so a better choice.

So here is my UIView subclass to do this. It uses a delegate approach, with a protocol to declare the method:

@class DSView;

@protocol DSViewDelegate <NSObject>
@optional

- (UIView *)view:(DSView *)view hitTest:(CGPoint)point
       withEvent:(UIEvent *)event hitView:(UIView *)hitView;

@end

And the actual subclass interface; as conventional, the delegate is not retained:

@interface DSView : UIView
{
    id <DSViewDelegate> DS_viewDelegate;
}

@property (nonatomic, assign) id <DSViewDelegate> viewDelegate;

@end

With the implementation just overriding the hit test method. It simply invokes the superclass then gives the delegate a chance to change it (or perform some other action) if it implements the delegate protocol method:

#import "DSView.h"

@implementation DSView

@synthesize viewDelegate = DS_viewDelegate;

/*
hitTest:withEvent:

Overrides this method to add support for the -view:hitTest:withEvent:hitView view delegate behavior.

Written by DJS 2009-09.
*/

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
{
    UIView *hitView = [super hitTest:point withEvent:event];
   
    if ([self.viewDelegate respondsToSelector:
        @selector(view:hitTest:withEvent:hitView:)])
        return [self.viewDelegate view:self hitTest:point
            withEvent:event hitView:hitView];
    else
        return hitView;
}

@end

To use this, simply change a container UIView to DSView in the view hierarchy, then set the delegate property to your view controller (via code or IB):

self.view.viewDelegate = self;

Then implement the -view:hitTest:withEvent:hitView: delegate method in your view controller, e.g. as follows — this will cause a tap on some special control to go through as normal, but tapping anywhere else in the view will hide the special control, without passing the tap on to whatever was actually tapped:

- (UIView *)view:(DSView *)view hitTest:(CGPoint)point
       withEvent:(UIEvent *)event hitView:(UIView *)hitView;
{
    if (hitView == someSpecialControl)
        return hitView;
   
    someSpecialControl.hidden = YES;
   
    return nil;
}

I hope this is useful to someone else too.

You can get the code from my Dejal Open Source Subversion repository via this Terminal command:

svn checkout http://dejal.svn.beanstalkapp.com/open/DSView

Or browse the source directly on the web.

If you haven't seen it already, check out DSActivityView, too.

DSActivityView updated

DSActivityViewI've committed a minor update to the DSActivityView open source project for iPhone. See the DSActivityView introductory post for more information, including a video demo.

This update adds a showNetworkActivityIndicator boolean property. It is NO by default, but if set to YES the network activity indicator in the status bar will be displayed, and automatically hidden when the DSActivityView is removed.

You can toggle this property as needed while the activity view is in use. For example, you might have the network activity indicator appear while fetching some data from the internet, then disable it while parsing it (while the activity view is still visible).

Of course, you can easily show and hide the network activity indicator yourself, but this tweak saves having to remember to disable both it and the DSActivityView.

You can set this property via:

[DSActivityView activityViewForView:self.view].showNetworkActivityIndicator = YES;

or to toggle it on an already-visible activity view:

[DSActivityView currentActivityView].showNetworkActivityIndicator = YES;

You can get the project from my Dejal Open Source Subversion repository via this Terminal command:

svn checkout http://dejal.svn.beanstalkapp.com/open/DSActivityView

Or browse the source directly on the web.

Announcing DSActivityView: open source for iPhone developers

DSActivityViewI recently wrote a reusable class for a couple of iPhone apps I'm currently working on, called DSActivityView. I decided to release it as open source. Read on for details.

Firstly, I should say that this work was inspired in part by Matt Gallagher's excellent article, Showing a "Loading..." message over the iPhone keyboard. My code only uses the -keyboardView method from his article, but he deserves credit and thanks for that and many other helpful articles. If you're not reading his blog, Cocoa with Love, you're doing yourself a disservice.

Back to my class. Actually, there are three classes: DSActivityView, DSBezelActivityView, and DSKeyboardActivityView. They provide three styles of activity view, and could easily be extended to support more.

DSActivityView

DSActivityViewThis does a simple horizontal-style loading view, intended for situations where you have a blank view while loading data. It can be displayed very easily — for the default "Loading..." label text, simply use:

[DSActivityView activityViewForView:self.view];

The activity view is automatically added as a subview of the specified view (e.g. the current content view). No need to save the result to an ivar. It automatically supports rotation to any orientation, too.

You can specify a custom label via:

[DSActivityView activityViewForView:self.view withLabel:@"Processing..."];

Or specify a custom width, e.g. so you can change the label while it is being displayed without upsetting the geometry, via:

[DSActivityView activityViewForView:self.view withLabel:@"Connecting..." width:100];

Then when you're done with it, simply invoke this to get rid of it:

[DSActivityView removeView];

DSBezelActivityView

DSBezelActivityViewThis is a subclass of DSActivityView, which displays an animated round-rect-enclosed variation: it animates into view by zooming from full-screen, with a gray background fading in to cover the passed view, and animates out by zooming to half size and fading out the background (see below for a movie showing it in action). It is ideal for situations where you have content visible already, but want to do a network operation to validate or send data, or some other time-consuming activity.

Display it via:

[DSBezelActivityView activityViewForView:self.view];

The [DSBezelActivityView activityViewForView:withLabel:] and [DSBezelActivityView activityViewForView:withLabel:width:] variations are also available. To remove with animation, call:

[DSBezelActivityView removeViewAnimated:YES];

DSKeyboardActivityView

DSKeyboardActivityViewThis is a subclass of DSBezelActivityView, which displays over the keyboard, somewhat like the OS 2 Text app used to do. It is useful to simply prevent further typing while validating a field or sending data (though you might also want to disable the field, to prevent pasteboard operations on it). No need to specify a view to use for this, since it uses the keyboard:

[DSKeyboardActivityView activityView];

Plus a [DSKeyboardActivityView activityViewWithLabel:] variation for custom text. Remove it the same as for the bezel style:

[DSKeyboardActivityView removeViewAnimated:YES];

Demo

I've included a demo project that builds an app to show the various options: the three styles, default or custom label text, covering just the content view or whole window, etc. It requires iPhone OS 3. Here's a movie showing the demo app running:

You can get the project from my Dejal Open Source Subversion repository via this Terminal command:

svn checkout http://dejal.svn.beanstalkapp.com/open/DSActivityView

Or browse the source directly on the web.

You can also download a snapshot, though it may not remain up-to-date; using Subversion is the recommended approach.

Follow @dejalopen on Twitter for automated Subversion commit message updates. You may also like to follow @dejaldevdiary for my behind-the-scenes development diary, and @dejal for general Dejal and personal tweets. Finally, there's also a RSS feed for the repository.

I hope these classes are useful. You are welcome to use them in any project, commercial or otherwise. I just ask that you give me credit; see the DSActivityView header for the easy and free licensing terms. If you do use this code in any form, please tell me (or comment here).

If you make improvements, e.g. to add other activity styles or fix bugs, please send them to me so I can share them with the community. Thanks.

Enjoy!

Anatomy of a feature

Brent Simmons wrote today about the anatomy of a feature, an article that really resonated with me.

It's tempting to think that adding a feature like this is just about adding the functionality — but there's a bunch more to it than that.

I get a lot of feature requests for my apps, which are certainly very welcome. But I think a lot of people don't quite realize how much work even the most trivial-sounding feature enhancement can be.

Brent gives a very clear and accurate picture of the process many developers, myself included, go through when considering and implementing such changes. Every aspect of them needs to be carefully analyzed and refined. Perhaps someone asks for a specific feature, but I can tell that what they really want is something different — they just came up with what sounded to them like an easy compromise, when the ideal solution might in fact be easier, as well as better for the overall app. Happens all the time.

But as I said, I do really value feature requests (and bug reports). I want my apps to work well and be as helpful as possible to my customers. For that reason, I keep track of such requests for each app, and also keep a running tally of "votes" for each feature (which sometimes requires some interpretation when different people have different takes on something). When lots of people are asking for the same thing, it rises to the top of my list, and I make it a priority for the next release. But only if I can do it in a way that is consistent with the design goals of the app. That's the tricky part.

Good thing I enjoy planning; I spend much more time analyzing and planning features than actually writing them.

Follow @dejaldevdiary for David's Dev Diary

Just thought I'd mention for any Mac or iPhone developers who read my blog, or customers who are interested in a behind-the-scenes look at my development process:

I recently joined the club and created a David's Dev Diary account on Twitter. It is a separate place for me to post a potentially boring diary of my development work. It is focused purely on the technical aspects that I normally wouldn't bother mentioning on my main Twitter account, @dejal.

Follow @dejaldevdiary on Twitter for all the highly exciting technical details (and maybe a hint or two about what's coming up... e.g. I'm currently working on a secret new iPhone app).

For a list of other developers writing diaries, check out the Dev Diaries website.

Not enough time!

I'm feeling vexed. I have several projects I'd like to work on, but just don't have the time at present.

I have big plans for Simon, Time Out, Caboodle, and my other apps. Each has a long list of great feature enhancements for the next several versions.

Plus I'm working on a semi-secret new project that includes an iPhone app and Mac app that sync via a web app. That is somewhat vexing in itself; it takes time to develop quality software, so that time delays updates to my other products. (I do plan on sneaking in some updates soon, though.)

To top it off, I also do contract work for a client in New Zealand, and am currently working on a major update for that, with a fixed deadline that pushes other work aside. I might do an iPhone app for them, too.

I work seven days a week, doing Dejal customer support mostly in the mornings and development in the afternoons and evenings. But I wish I had more hours in the day so I could do more — or more precisely, get things done more quickly. I'll get to everything eventually, but just not as quickly as I'd like.

I have a really high-tech scheduling system, consisting of printed pages for each quarter of the year, and Post-It notes of different colors for each project:

Schedule

(The gaps are reserved for safety margins, if things take longer than expected, as they often do, and for bug-fix updates.)

This is mounted near the ceiling on the wall in front of me, so I can easily see it whenever desired. It is frustrating when I have to move the Post-Its around due to things taking longer than expected, though... and more so when I have to push a project off the end of the year.

It should be noted that these Post-Its only represent significant updates and new projects; bug-fix updates and very minor updates can be snuck in at any time, as needed.

But enough moaning... I've got work to do!

Developers should iPhone-optimize their sites

A while ago I wrote how the Dejal site is iPhone-optimized: when you view it on an iPhone or iPod touch, the website content is reformatted to fit neatly in the 320-pixel-wide display:

I would suggest that any developers who write iPhone software should do this too. So here's some technical info on what I did. This isn't necessarily the best solution, but it works for me, and isn't very difficult.

Firstly, of course, you need to be able to detect whether you're running on an iPhone or elsewhere. The standard way to do this is by looking at the "user agent" value of the HTTP session. In PHP, you can simply look at the $_SERVER['HTTP_USER_AGENT'] global variable. I have the following function in a utility PHP file included on every page (via the header code):

    function getIsIPhonePlatform()
    {
        global $private_is_iphone_platform;
        
        if (isset($private_is_iphone_platform))
            return $private_is_iphone_platform;
        
        $user_agent = $_SERVER['HTTP_USER_AGENT'];
        $private_is_iphone_platform = stristr($user_agent, 'iPhone') || 
            stristr($user_agent, 'iPod') ||
            stristr($_GET['platform'], 'iPhone');
        
        return $private_is_iphone_platform;
    }

This function returns whether or not the user agent is an iPhone or iPod touch, either by returning the state if already known, or determining it if not. It also allows testing the iPhone-optimized pages from your Mac by adding "platform=iphone" to a page's URL parameters (try it; it's fun!).

Not everyone would want the pages optimized, though: a great thing about the iPhone is MobileSafari does an excellent job of rendering "real" web pages. So I also added a checkbox at the bottom of every page to toggle iPhone-optimized mode off and on. The state is recorded in cookie. So I have another function to read that, on the iPhone platform:

    function getIsIPhoneOptimized()
    {
        global $private_is_iphone_optimized;
        
        if (isset($private_is_iphone_optimized))
            return $private_is_iphone_optimized;
        
        $private_is_iphone_optimized = getIsIPhonePlatform();
        
        if ($private_is_iphone_optimized && isset($_COOKIE['iphone_optimized']))
            $private_is_iphone_optimized = $_COOKIE['iphone_optimized'];
       
        return $private_is_iphone_optimized;
    }
Then the checkbox is actually output (in the footer's include file) via somewhat messy code that uses PHP to output the JavaScript to set the cookie and reload the page when the checkbox is toggled, and the checkbox itself:
    function outputIPhoneOptimizationCheckbox()
    {
        if (getIsIPhonePlatform())
        {
            $query_params = $_SERVER['QUERY_STRING'];
            
            if ($query_params != '')
                $query_params = '?' . $query_params;
            
            echo('<script type="text/javascript">' . "\n");
            echo('<!--' . "\n\n");
            echo('function iphoneOptimizedToggled()' . "\n");
            echo('{' . "\n");
            echo('  document.cookie=\'iphone_optimized=' .
                !getIsIPhoneOptimized() . '; path=/\';' . "\n");
            echo('  window.location.reload(true);' . "\n");
            echo('}' . "\n\n");
            echo('-->' . "\n");
            echo('</script>' . "\n\n");
            
            echo('<p><input type="checkbox" id="iphone_optimized_checkbox"
                onclick="iphoneOptimizedToggled()"');
            
            if (getIsIPhoneOptimized())
                echo(' checked="checked"');
            
            echo(' /><span id="iphone_optimized_label"
                onclick="iphoneOptimizedToggled()">
                Display site optimized for iPhone</span></input>' . "\n");
            echo('</p>');
        }
    }
And finally, the getIsIPhoneOptimized() function is called in the header to use iPhone-optimized or normal style sheets. This is actually a simplification; it actually uses several style sheets, including some common ones and some platform-dependent ones. It also sets the viewport appropriately for each platform — that is a key aspect for iPhone optimization:
    if (getIsIPhoneOptimized())
    {
        echo('<link rel="stylesheet" href="/iphone/header.css"
            type="text/css" media="all" />' . "\n");
        echo('<meta name="viewport"
            content="width=device-width, user-scalable=no" />' . "\n");
    }
    else
    {
        echo('<link rel="stylesheet" href="/mac/header.css"
            type="text/css" media="all" />' . "\n");
        echo('<meta name="viewport" content="width=900" />' . "\n");
    }
Of course, configuring the CSS appropriately is another story, but not too difficult... and very site-specific. Feel free to explore my CSS files if desired: I hope this is helpful. There are a number of other aspects, like fitting images in the available space, supporting movies that can play on the iPhone, and more. If there's interest, I might write more about this in the future.

Apple, please support iPhone trial apps

iPhone App StoreThe new in-app purchasing feature in iPhone OS 3.0, as discussed in the keynote, promises to be a great addition. But it seems one of the most popular uses of this feature has been deliberately blocked: a shareware-like trial model.

They said in the keynote that "free apps will always be free"... which sounds good on the surface, but eliminates one of the most popular software distribution models.

It would be great if users could download an app for free, try it for a while to evaluate (perhaps with feature restrictions or a time limit), then use in-app purchasing to buy the full edition.

Apple and developers would still get paid, but users would be able to better evaluate whether or not they want to purchase the product. With high quality products, that would lead to more overall sales — and people would vote with their dollars to encourage quality apps.

Without such a mechanism, the only way to let users try a product before buying is to release two editions, e.g. a free Lite edition and a paid Pro edition. While that has some merit in itself, it adds additional hassles for the users (e.g. transferring data between the two apps, since each app sandboxes its own data) and having to find the Pro edition, buy and download it, re-configure it, and probably delete the Lite edition.

With in-app purchasing available, there should be no technical barrier to allowing free trial apps that can be purchased after trying them out.

Developers, if you want this option, please tell Apple — file a bug report duplicating mine, rdar://problem/6699761 (this link only works for Apple employees).

The iPhone App Store is broken

iPhone App StoreWell, maybe not broken, but definitely showing some cracks.

The iPhone App Store is a great concept. One central place to get all applications for the iPhone; everything is there, and only there, available right on your iPhone.

But is it everything? Of course, Apple rightly filters out malware and illegal applications... but they have caused some controversy of late by refusing entry to generally useful applications. Worse, they've rejected a couple of apps recently because they "duplicate existing functionality".

One such is a podcasting app, which they say duplicates the iPod app... but seems to have offered other benefits. Another is a Gmail reader, which apparently is more convenient than using Apple's Mail or Safari to access Gmail.

Notice something in common there? Both rejected apps would have competed with Apple's own apps. Yet Apple has no problem with dozens of flashlight and sudoku apps. So it seems to a plain-and-simple anti-competitive move on Apple's part. That is not playing nicely, and potentially illegal. Governments tend to frown on that kind of behavior.

Why do I care?

I haven't written any iPhone apps yet... but I'd like to, in due course. I've written up a rough design for one new app, and would likely want to write companions for some of my Mac apps, like Dejal Simon. However, episodes like these make me and many other developers hesitate to begin, or continue.

Writing software is hard; it can take months to write even a relatively small application properly. What if we come up with a great idea, spend months of time designing, developing and polishing it, then submit it to the App Store, only to have it rejected based on some unannounced policy, or whim? That would be a huge waste of time and effort, which makes developers like me concerned about whether it's worth starting.

What Apple needs to do is provide a clear, detailed description of the iPhone App Store policies, and stick with it. Perhaps offer a contact point for discussions early on, to ensure that an app concept is worth pursuing.

And preferably make the policies as open as possible — none of this anti-competitive behavior. If someone writes an alternative email app or web browser, let them release it. If it is superior to Apple's, it will flourish, and everyone will benefit (including Apple, via their sales cut). If it isn't any good, it'll fade into obscurity.

It's really in Apple's interest to do this, to ensure developers put the effort into building quality apps for the platform. Apple, the ball is in your court. Make it right.

Mac OS X versions for Simon

Since a few people have asked, regarding my announcement yesterday that Simon version 2.4 now requires a minimum of Tiger (Mac OS X 10.4), here's the current breakdown of OS versions for Simon:

  • 63% are on 10.5.2
  • 30% are on 10.4.11
  • 3% are on 10.3.9
  • 4% are on other OS versions (10.4.x or 10.5.x)

So a few people will be affected by this change, but a relatively small number. And that's always dropping, as more people buy new machines or get around to upgrading their OS.

For people stuck on 10.3.9, I'm sorry for the inconvenience... but it had to happen eventually, and Simon 2.3.5 is a fine version.

Interestingly, Simon is a little ahead of the curve when it comes to Leopard adoption. For my other products (excluding Narrator 2, which requires Leopard), the percentage averages to about 56% on 10.5.2, 40% on 10.4.11, 3% on 10.3.9, and 1% on others. So I could drop 10.3.9 support for those too, though I won't until necessary. It'll definitely happen eventually, but not for a while.

A professional press release via prMac

On Monday I did a major upgrade of Dejal Narrator, my app to read out stories in multiple voices, to version 2.0. I usually send out press releases when I do major and minor product releases, but have previously just written and sent the releases myself, using a collection of email addresses I've gathered over the years.

But for this release, I decided to try something different. I had tried free distributions via prMac.com in the past, often while doing my own releases too. It seemed like a good service, but the three-day delay for free releases lacked the immediacy I wanted. So this time I put it to a real test: I used their Writing Service to craft a press release using their experience and skills to get the message across, and paid for the Extended Distribution to get the release out immediately and to a wider audience.

So how'd it work for me? Ray from prMac was prompt and friendly, quickly crafting a release that captured the essence of the product. There was opportunity to review and tweak the wording, but very few changes were needed. Then on release day, I submitted it for posting, which was done soon afterwards. I quickly noticed lots of sites mentioning Narrator, that normally don't pick up my press releases (such as Macworld). The prMac service definitely has a much wider range of publishers than my self-created list.

I'm not sure if it's related or not, but I was very pleasantly surprised to discover that the Apple Downloads software listing site selected Narrator as a "Staff Pick"... not only showing it as the "Featured Download" on the Home & Learning category page, but as the "Featured Download" at the top of the All Categories page, for a couple of days (it's been bumped now, though). Quite the honor! I'm willing to give prMac the credit for gaining Apple's attention like that. You can't buy publicity like such a prominent spot on Apple's site, but for a few dollars you can buy an excellent press release distribution. I plan on using prMac again for future releases.

Narrator as Apple's Featured Download

Migrating from disk images to ZIP archives

For many years, I've been releasing my Mac OS X software on disk images. For a long time, they seemed the most elegant way to provide software to people: they provide a single downloadable container that can be saved in a compressed state, and they can include a pretty background image that explains how to install the app.

A more recent innovation was to include an alias (actually a symbolic link) of the Applications folder, with an arrow in the background indicating to drag the application to that folder to install:

Simon disk image

But all that complicates the release process for me, and for my customers.

For me, when I do a release build I need to copy the build to a standard location then run FileStorm, an application that builds the disk image, then upload the resulting disk image. Doesn't sound too hard, except that FileStorm tended to misbehave for me all too often, resulting in incorrectly laid-out disk images and other problems, requiring several attempts to get it right. It also had compatibility issues with Leopard, forcing me to run it on a Tiger machine, which had other complications.

For my customers, disk images have more hassles. After downloading, they are usually mounted automatically by the OS, though sometimes that didn't work for some people. The images are "internet enabled", so people downloading via Safari get only the contents of the disk images, while people using other browsers get the disk image window as above. Then they need to find it and drag the application to install it... but some people run it directly off the disk image, then wonder where it went after they've dismounted the disk image (or restarted their computer). Plus the disk image has to be dismounted, another hassle.

There's got to be a better way... and there is. The humble ZIP archive.

A ZIP archive is a simple compressed file. They can be created and expanded using built-in commands in the Finder. So for me, creating one is a trivial operation; no more messing around with FileStorm. And they are more convenient for my customers too. After downloading, the archive is automatically expanded, with the application appearing in the download folder. They can then easily install it by dragging to the Applications folder, or try it directly from the downloads folder if preferred — without it mysteriously vanishing after a restart.

So, as I release new versions of my apps, I have been switching to the ZIP archive format. Downloads work exactly the same from my site, but the result is much more convenient for everyone.

As always, I welcome feedback on this. So far, I haven't had any complaints, though one potential problem has come up: one person with an incorrectly installed copy of StuffIt had difficulty expanding the archive by double-clicking on it. The solution was simply to tell the Finder to use the built-in archive expander instead (which is called "BOMArchiveHelper" on Tiger or "Archive Utility" on Leopard).

Cocoa: custom attachment in a text view

I recently posted a query to CocoaDev asking for help with inserting a custom attachment cell into a text view. I had spent quite some time investigating and experimenting, and searches showed several people who had the same question, but no satisfactory answers. Some comments I read suggested subclassing NSTextView, which I tried, but not very satisfactorily.

Fortunately, Douglas Davidson was kind enough to point me in the right direction, combined with helpful off-list discussion with another developer who was working on much the same problem.

I thought I'd share my solution here, in case it helps anyone else. It turned out to be easier than I'd expected. Note: this is written for Leopard with Garbage Collection, so doesn't have releases etc.

Firstly, a file wrapper is used to create a placeholder TIFF with a special "sub-extension" to identify it. I could store any kind of data, but I use a placeholder image so if the user pastes the text into another app, it shows up as something nicer than a generic document icon. The identifier is a unique reference to the data that the attachment represents:

- (NSFileWrapper *)fileWrapperWithIdentifier:(NSString *)identifier;
{
    NSString *wrapName = [[identifier stringByAppendingPathExtension:@"myspecialmarker"] stringByAppendingPathExtension:@"tiff"];
    NSSize size = {100, 18};
    NSData *data = [[NSImage imageNamed:@"AttachmentPlaceholder"] TIFFRepresentation];
    NSFileWrapper *wrapper = [[NSFileWrapper alloc] initRegularFileWithContents:data];
   
    [wrapper setFilename:wrapName];
    [wrapper setPreferredFilename:wrapName];
   
    return wrapper;
}

This method inserts the attachment with the above wrapper and my custom cell:

- (void)insertMarkerWithIdentifier:(NSString *)identifier;
{
   NSFileWrapper *wrapper = [self fileWrapperWithIdentifier:identifier];
   NSTextAttachment *attachment = [[NSTextAttachment alloc] initWithFileWrapper:wrapper];
   MyAttachmentCell *cell = [MyAttachmentCell new];

   cell.identifier = identifier;
   [attachment setAttachmentCell:cell];

   [[myTextView textStorage] appendAttributedString:[NSAttributedString attributedStringWithAttachment:attachment]];
}

I use a couple of text view delegate methods to write the cell's file wrapper to the pasteboard as file contents, allowing it to be copied, dragged, and saved to disk with the text:

- (NSArray *)textView:(NSTextView *)aTextView writablePasteboardTypesForCell:(id <NSTextAttachmentCell>)cell
             atIndex:(NSUInteger)charIndex;
{
   return [NSArray arrayWithObject:NSFileContentsPboardType];
}

- (BOOL)textView:(NSTextView *)aTextView
       writeCell:(id <NSTextAttachmentCell>)cell
         atIndex:(NSUInteger)charIndex
    toPasteboard:(NSPasteboard *)pboard type:(NSString *)type;
{
   if (type == NSFileContentsPboardType)
       [pboard writeFileWrapper:[[cell attachment] fileWrapper]];

   return YES;
}

And to convert the attachment back to my custom cell after pasting/dragging/loading it, the text storage delegate (which is set via [[myTextView textStorage] setDelegate:self];). It looks for TIFF attachments that have my special marker "sub-extension" but aren't using my custom cell, and replaces their cell with my custom one:

- (void)textStorageWillProcessEditing:(NSNotification *)note;
{
   NSAttributedString *text = [myTextView textStorage];

   if ([note object] != text)
       return;

   NSUInteger length = [text length];
   NSRange effectiveRange = NSMakeRange(0, 0);
   id attachment;

   while (NSMaxRange(effectiveRange) < length)
   {
       attachment = [text attribute:NSAttachmentAttributeName atIndex:NSMaxRange(effectiveRange) effectiveRange:&effectiveRange];

       if (attachment)
       {
           if ([attachment isKindOfClass:[NSTextAttachment class]] &&
               ![[attachment attachmentCell] isKindOfClass:[MyAttachmentCell class]] &&
               [[[[attachment fileWrapper] preferredFilename] pathExtension] isEqualToString:@"tiff"] &&
               [[[[[attachment fileWrapper] preferredFilename] stringByDeletingPathExtension] pathExtension] isEqualToString:@"myspecialmarker"])
           {
               MyAttachmentCell *cell = [MyAttachmentCell new];
               [cell setIdentifier:[[[[attachment fileWrapper] preferredFilename] stringByDeletingPathExtension] stringByDeletingPathExtension]];
               [attachment setAttachmentCell:cell];
           }
       }
   }
}

The custom attachment cell is a subclass of NSTextAttachmentCell, to simply draw in a custom way.

I hope this is helpful to others.

I'm the MacTech Spotlight for October 2007

MacTech magazine has a monthly feature at the back of each recent issue called "MacTech Spotlight", where they devote a page to Q&As with a developer or other personality in the Mac community. They've talked with Paul Kafasis of Rogue Amoeba, Wolf Rentszch of Red Shed, and several others.

This month was my turn.

I haven't seen how the article turned out yet (my copy's in the mail), but hopefully it'll be okay. :) I talked about how I originally got into computers and programming, how Dejal got started, what I like about Apple and Mac OS X, how I come up with product ideas, and more.

For those interested, MacTech offers discounts on subscriptions - 60% off the cover price, or a limited-time offer of a six-month sub for only $9.95.

Syndicate content