Strategies to support the new iOS7 UI

Now that Apple has revealed iOS 7, there's no doubt that iOS developers around the world have a huge work ahead, to support the new user interface.

Every iOS major version has many changes in the SDK.
New concepts, new classes/frameworks to deal with, obsolete features, etc.

So there's usually always some work to do in order to fully support a new iOS major version.

Till now, if the app was made correctly, with compatibility in mind, it was usually not a big deal.
But with iOS7, things are completely different.

The problem

The iOS UI haven't changed (or only very slightly) from the first iOS version to iOS6.
So the modifications required to support a new iOS versions were mostly in the code logic.

Now we've got a completely different UI, and supporting this in our apps will require much more work.

When thinking about the UI, existing iOS apps can usually be categorized in three main categories.

  1. Applications that use only SDK components, without any modification of UI candy.
  2. Applications that uses mainly SDK component, with a few custom ones or with a few modifications.
  3. Applications with completely custom components.

The first category should be quite OK with iOS7. When you deal only with SDK features, without any kind of modification (custom drawing code, custom layout, etc), you're not really concerned about a change in the UI, as your app will immediately reflect all changes in the SDK.
But I think most of the AppStore apps don't fit in this category.

The second category of applications will obviously need modifications.
While some parts may adapt well to the new UI, all custom parts will need to be adapted.
Depending on the amount of custom parts, this may actually be a huge work.

The third category may be safe, as long as it's really completely custom, and unless the changes in the SDK breaks the custom parts.
But then, there's something else.

The problem with most applications with a custom UI is that the UI, even if custom, was somehow related to the original iPhone UI.
So even if the custom components will work with iOS7, there's a strong risk that the app will quickly look outdated.

I think users will get used of the new flat UI. So if an application does not use a flat interface, even if it's a killer app, it may finally get less users, only because it will feel less integrated with the system.
I guess that's how users will react, and I doubt we can change this mentality.

So to resume, most of the existing applications will need to be modified to support the flat UI of iOS7.

Solutions

Now what's the best way to achieve this, in a developer's perspective?

Of course, we could just drop support to iOS versions lesser than 7. But I don't think this is an option.
While some new apps may just do that, and even if the average iPhone user usually updates his device quickly, I think iOS6 (and also iOS5) needs to be supported.

So most of the existing applications will have to deal with two completely different kind of user interfaces. Headache ahead…

1 - iOS version detection

In Objective-C, we can of course query the system for the running iOS version.
This is done through the UIDevice class.

I won't fall in the debate of version checking versus feature checking.
While the second is obviously better, in such a specific and general case, version checking is perfectly OK.

So the iOS version number can be retrieved with:

NSString * version = [ [ UIDevice currentDevice ] systemVersion ];

This is not very usefull, as it returns a string with the full version number.
In our case, we're only interested by the major version number. And an integer value would be much more practical than a string.

So let's add a method for this in UIDevice, using a category:

/* UIDevice+VersionCheck.h */
@interface UIDevice( VersionCheck )

- ( NSUInteger )systemMajorVersion;

@end

/* UIDevice+VersionCheck.m */
@implementation UIDevice( VersionCheck )

- ( NSUInteger )systemMajorVersion
{
    NSString * versionString;
    
    versionString = [ self systemVersion ];
    
    return ( NSUInteger )[ versionString doubleValue ];
}

@end

It simply asks for the version string, and converts it to an integer.
As we cast a double to an integer, we'll get only the major system version.
This way, as we've augmented the UIDevice class, we can use:

NSUInteger version = [ [ UIDevice currentDevice ] systemMajorVersion ];

And of course, as we now got an integer, we can use it quite simply in conditional statements:

if( [ [ UIDevice currentDevice ] systemMajorVersion ] < 7 )
{
    /* iOS 6 and previous versions */
}
else
{
    /* iOS 7 and above */
}

But obviously, this will lead to very crappy code, because you'll have to put this kind of statement at many places.
Your code will then become hardly maintanable.

So how can we improve this?

2 - Class clusters to the rescue!

We can solve this using the class cluster approach.
First of all, what's a class cluster?

You may have noticed when using instances of NSArray, for instance, that the resulting object is not really a NSArray.

For instance, you may expect the following code to print NSArray to the console:

NSLog( @"%@", NSStringFromClass( [ [ [ NSArray alloc ] init ] class ] ) );

But in fact, it will print a different class name.
This is because NSArray is a class cluster.

Unlike other object-oriented languages, Objective-C does not have constructors.
So creating an instance of a particular class is usually done by sending the alloc message to a class.

The difference is that alloc is a standard static method. So unlike classic constructors, it has the ability to return a value.
In that case, a new instance of the requested class.

But as it's a standard method, it can actually return something else, like an instance of another class.

This is, for short, the main concept behind a class cluster.
The basic idea is to have a public base class, which exposes all the required and common methods.

Then, depending on the software's needs, we can have many other private classes, extending that base class and overriding behaviours.
This way, we can achieve separate specialization, while keeping a standard and unique interface.

For instance, think again about the NSArray class.
It obvisouly has to deal with many specializations, like mutable or immutable instances, toll-free bridging (for communicating with the CoreFoundation API), etc.

Dealing with all these cases in one unique class would mean lots of code inside a class, leading to maintanability issues.

So this problem is solved by having many specialized classes extending NSArray.
All of those classes aren't exposed in the public API.
The only public one is still NSArray, but it will just act as a frontend to the other ones.

This can be done by overriding the alloc method, and returning an instance of another class, depending on the context.

To give a practical example, let's imagine the following scenario: in your app, you have a subclass of UIView in which, amongs other things, you override the drawRect: method.

This is usually the case for custom UI components.

If you want to support both iOS6 and iOS7, you may need to code different drawing code, to adapt to the general system UI.
We dont want if/else statements in the drawRect: method. This is crappy.

So we'll simply make a base class, containing all the common code, and we'll override the alloc method in order to have a class cluster:

/* TestView.h */
@interface TestView: UIView

/* Common method */
- ( void )test;

@end

/* TestView.m */
@implementation TestView

+ ( id )alloc
{
    if( [ self class ] == [ TestView class ] )
    {
        if( [ [ UIDevice currentDevice ] systemMajorVersion ] < 7 )
        {
            return [ TestViewIOS6 alloc ];
        }
        else
        {
            return [ TestViewIOS7 alloc ];
        }
    }
    else
    {
        return [ super alloc ];
    }
}

- ( void )test
{}

@end

Look at the alloc method implementation.
When called, it detects the iOS version and actually instanciates a specialized subclass, depending on the iOS version.

So when calling:

[ TestView alloc ]

You'll actually get an instance of the TestViewIOS6 prior to iOS 7, and an instance of TestViewIOS7 on iOS7 and later.

This way, you can have your common and version independant code in the base class, and specialized code in the the different subclasses, while keeping a unique interface when using your custom view.
For instance:

/* TestViewIOS6.m */
@implementation TestViewIOS6: TestView

- ( void )drawRect: ( CGRect )rect
{
    /* Custom iOS6 drawing code */
}

@end

/* TestViewIOS7.m */
@implementation TestViewIOS7

- ( void )drawRect: ( CGRect )rect
{
    /* Custom iOS7 drawing code */
}

@end

This way, all of your specific code will be in specialized subclasses.
It means your code will be cleaner and more maintanable.

Also, when you'll decide to drop support for iOS6, the only thing you'll have to do is to remove the specialized iOS6 subclass.
Much simpler than removing if/else statements…

Enjoy, and have fun with iOS7 : )

Comments

Author
Vinay C
Date
08/06/2013 05:40
Indeed it is much better to do this than having crappy if-else statements everywhere in the code. Keeps code clean, readable and easier to manage for app running across version.
Author
Marius Landwehr
Date
08/07/2013 12:54
This seems not to work if you subclass TestView again :/ too bad
Author
Jean-David Gadina
Date
08/07/2013 18:10
Yes, such a class cluster is not ready for subclassing. And usually, you shouldn't subclass an existing cluster.
That being said, it's perfectly possible. You may add additional checks in the base class' alloc method.
Author
Using class clusters to support iOS 7 « [iOS developer:tips];
Date
08/12/2013 13:55
[...] Jean-David Gadina on class clusters: [...]
Author
Mike Lee
Date
08/21/2013 04:32
Such a great strategy to prepare the iOS7 and I'll apply this to all my sources.
Author
The Socialize Blog » Updating Socialize iOS SDK UI to iOS 7
Date
02/03/2014 00:32
[...] For more information on class clusters and OS versioning, check out this article. [...]