I’ve designed literally dozens of component APIs over the years, including for clients like Apple, and I’ve learned quite a bit about the process. I periodically release open source components too, and the feedback I’ve had has helped me put together a set of guidelines for API design that I’d like to share with you.
This is an important topic, whether you’re an open source contributor, or working as part of a team on a large app, or just creating your own software. Just like the first launch experience of an app, your API is part of the first impression that a developer will have with your code, and will have a huge impact on whether they use it or throw it away.
APIs are UX for developers. I’ve always been surprised that there isn’t more material written about this aspect of our work, in a way that’s specific to the popular platforms.
As we go through some guidelines, I’m going to use my most recently released open source GUI component, MGTileMenu, as an example where necessary. You can read all about MGTileMenu here first, if you like.
Desirable qualities
API design is very much like user interface and user experience design. Your target audience has different needs and characteristics, but they’re still humans who are looking to get a job done. As with a friendly, usable app UI, you’re still trying to make your API:
- Intuitive
- Forgiving
- Frictionless
As with any piece of software designed for use by humans, we have to think about the use cases. We have to make the most commonly-needed stuff easy, without undue configuration. Default behaviours should be useful as-is, and should be sensibly chosen. The software should be discoverable, and should allow the user to generalise from known paradigms. It’s all exactly the same set of principles as when we’re creating UIs.
The developer interface
Components are interacted with by the developer using four primary explicit means:
- The class interface: its exposed properties and methods.
- The delegate protocol, where relevant.
- The data-source protocol, where appropriate.
- Any provided notifications.
We have to design each of those, judiciously and deliberately, for human use. There are two key questions when you’re thinking about the API:
-
What is the control?
This influences the interface and convenience methods. Is it a button? A slider? Your interface is obvious. Your convenience methods will follow the standard semantics of the control. -
What is the control like?
This influences the delegate and/or data-source model and notifications. If it’s a new type of control, is it essentially very similar to something else? An outline view is a linear table. A calendar widget is a date-picker. A collection of commands presented with a unified presentation is a menu.
Our core goal is consistency with existing components and models, so that we can turn an unfamiliar control into something the developer already understands. Use standard APIs, models, and patterns wherever possible (and that’s almost always). Familiarity and intuitiveness are just as important at code level as they are for the end user.
Let’s look at the four components of the component API mentioned above.
Class interface
Here’s the interface file for MGTileMenu.
Before we even start talking about the specifics of the interface, we have a couple of over-arching rules:
Rule 1: Use the local dialect
One the most common mistakes I see in API design is the use of foreign conventions. APIs belong to a platform and a developer ecosystem. You simply can’t use whatever idioms and architectures you’re used to from a different platform; to do so is to pollute your current codebase and to damage the productivity of your fellow developers.
Learn your target platform’s conventions before coding. For example, on iOS or OS X, don’t use exceptions for control flow. Name your methods in an appropriate manner (which usually means sufficiently verbose, but should also of course be sufficiently succinct).
Learn what a protocol is, and a delegate, and a category. Use that terminology throughout your code. Learn the relevant naming schemes for constructors and destructors. Obey native memory management rules. The vocabulary and the grammar are indivisible, and you’re either developing for a given platform or you’re not.
Rule 2: Design decoupled
Any component should be designed such that it’s not coupled to the project you created it for, and if it’s a GUI control or view, it should at least display something by default. Use the existing framework classes as a guide, and maintain loose coupling with delegate protocols, well-designed/named API methods and notifications where appropriate.
An obvious but very effective way to do this is to create a new project for each component, and develop the component literally in isolation. Force yourself to use your own API. Stay away from the temptation of tying unrelated classes together. Start as you mean to go on.
With that said, let’s talk about the class interface proper. Initialisation methods are one of the most important parts of the interface, because they’re how people get started with your component. Your class will have certain required settings for initial configuration. So, an obvious rule:
Rule 3: Required settings should be initializer parameters
If something needs to be set, don’t wait for it – require it up-front, immediately, and return nil if you don’t get something acceptable.
1
|
|
Rule 4: Allow access to initializer parameters
This is a corollary to the previous rule: remember not to just swallow those parameters. Give access to them via properties, and note if they might have been massaged in any way (sanitised, or otherwise modified).
1
|
|
These previous two examples raise a further general point.
Rule 5: Comment your header files (including defaults)
Realistically, you won’t always provide separate, standalone documentation for a component. If you don’t provide documentation, your .h files (and demo app) are your docs. They should be suitably written, and by ‘suitably’ I mean:
- Sufficiently detailed, but no more so. Be succinct.
- For professionals. Assume things that are safe to assume. Don’t waffle.
Particularly, you should briefly note default values beside properties or accessors; it’s much easier to scan those in the header file than to try to locate your initialisation code in the implementation.
1 2 3 |
|
Rule 6: Get up and running in 3 lines
Your class should be designed so that it requires minimal code to integrate (delegate/data-source protocol included, about which more later). Excluding delegate methods, you should aim to make it usable at least for testing purposes with only 3 lines of code.
Those lines are:
- Instantiate it.
- Basically configure, so it will show and/or do something.
- Display or otherwise activate it.
That should be it. Anything substantially more onerous is a code smell. Here are the relevant lines from MGTileMenu’s demo app:
1 2 3 4 5 6 7 8 |
|
Rule 7: A fat demo usually means a broken component
Another corollary: the size of your demo harness is a quality metric for your component, where smaller is better. Demo harnesses/code should be as small and thin as possible (making suitable allowances for demos that aim to explore all of a component’s customisation or functionality).
The core required code to turn an empty Xcode app template into a demo of your app should be minimised. It’s not OK to required copy-pasted boilerplate to get your component working, and having an example of it in your demo isn’t an excuse.
Rule 8: Anticipate customisation scenarios
My standard rule for apps is don’t give the user options. Choose sensible defaults to fit the majority, and skip the Preferences window. Good software, after all, is opinionated.
The situation is a bit different with components, because the scenarios of use aren’t as clear-cut. You can certainly make a component that only fits one specific situation, but usually we want some flexibility. You never know exactly how another developer is going to use your component, so you have to build in some generality.
It’s important to choose your customisation points carefully. It’s particularly important to consider dependencies – not in the compiling/linking sense, but rather the logical relationships between types of customisation. I approach this by trying not to think of customisation at the instance-variable level, but rather at the “aspect” level. What aspects of your component do you want to allow customisation of? Then you work out what specific properties to expose.
It’s easy to cripple a certain type of customisation by not exposing sufficient configuration points. Some examples:
- Don’t expose width and height without considering corner radius too.
- Don’t expose background colour without highlighted background colour.
- Don’t expose size without spacing.
The specifics depend on the component, but just try to consider the relationships between properties, from the point of view of either appearance or functionality. Empathise with the developer. Be flexible, without abandoning the identity of the component.
1 2 3 4 5 6 |
|
Let common sense be your guide. Decide what options will serve 70% or so of the usage situations you can think of, and provide those options. Let your delegate methods and code structure serve the rest.
Rule 9: More properties, fewer actions
There’s a particular pattern that keeps cropping up in components that I like – some of which are from standard frameworks, some open source from third parties, and some even my own. It’s a ratio of the number of properties (or accessors, or customisation points) on a component, to the number of “do stuff” methods (i.e. all the other stuff, from initializers to state-updating).
It’s pretty much always more properties, and fewer ‘actions’ (again, that’s not actions in the Interface Builder sense). MGTileMenu has an initializer, and four actual for-public-use methods (one of which is a convenience that calls another). In terms of customisation points, it has four times as many. I think that’s a good ratio, and leads to components that are both concise in actual functionality, but also flexible in customisation.
1 2 3 4 |
|
Rule 10: Use controls in your controls
A great way to simplify both the API and implementation of your component is to use existing controls in your implementation. A unified presentation doesn’t meant that you can’t build something out of pre-existing components (indeed, that’s one of the basic principles of good software engineering).
Consider how UITableViewCell and UIButton have simple APIs because they use sub-controls such as UIImageViews and UILabels. You can, and should, do that too – and if appropriate, expose the corresponding sub-controls to keep your class interface concise and consistent.
In MGTileMenu, for example, the tiles are regular UIButtons (not even subclasses). This drastically simplified the implementation compared to drawing the tiles within a single custom view, tracking input events, and supporting accessibility.
Rule 11: Convenient for you is convenient for me
You’ll naturally add convenience methods during implementation, and the instinct is to keep them private. Instead, consider whether you can expose them for use by those who integrate your component into their own apps.
Whatever made it more convenient for you to add a method or function may apply to those developers too.
For example, in MGTileMenu I created these convenience functions:
1 2 3 |
|
The first helps me shift a tile menu so that it’s fully visible within its parent view (which might be handy for another developer, if they’re providing ancillary UI related to the menu), and the second returns a Core Graphics gradient from two UIColors, which I used when setting a default background for the tiles (and another developer may find handy when implementing MGTileMenu’s delegate protocol, to give tiles custom gradients).
Rule 12: Magic is OK. Numbers aren’t.
Sooner or later, you’ll put magic into your component. Hopefully there’ll be plenty of the Steve Jobs type of intuitive, delightful, empowering magic, but what I’m talking about is things like numbers and other values that have special meaning in your code. A common example is -1, to indicate a unique thing in a set, or a special situation.
It’s fine. It’s genuinely OK to do that. What’s not OK, though, is needlessly putting mysterious raw values throughout your code, and it’sespecially not OK to expose that in the API. If you’re exposing magic, dress them up for consumption. Use #defines or a constant or something. Just make them presentable and understandable.
1 2 |
|
Delegate and data-source protocols
Delegate protocols are fantastic. They’re an easy, familiar and flexible way to embrace the MVC pattern, and they reinforce good habits of loose coupling and judicious API design.
Here’s MGTileMenu’s delegate protocol.
There are classic delegate and data-source protocols that we can draw on for almost any component. If you’re displaying data, the One True Data-Source Protocol is likely to be something very close to:
- How many things do I have?
- What’s the value for property Y of thing X?
Similarly, in almost any situation, the One True Delegate Protocol is likely to take the form:
- Should this thing do that?
- This thing is about to do that.
- This thing just did that.
This is also known as the Should, Will, Did protocol pattern, and it ties neatly in with the Will-Did notification pattern too, about which more later.
Let me mention something you might find controversial: I find it perfectly acceptable to conflate the delegate with the data-source (i.e. combine them into a single protocol). I do it with MGTileMenu and several other components, for example.
I fully accept the principle of separating them, and I can think of many cases where you’d want to keep them separate. Apple keeps them separate too, generally. That’s fine.
In my experience, though, in most cases it’s fine to combine them. Most people handle data-source methods and delegate methods in the same place. I’ve never had a complaint about unifying those protocols, and I can scarcely remember a situation where even existing separate protocols were handled in different places.
If you care about purity, or have a need to separate delegate from data-source, then obviously you should do so. I just don’t think you need to feel bad if you combine them.
Rule 13: Limit ‘required’ delegate methods
Be very careful when choosing which of your delegate methods arerequired. Too many required methods tends to indicate:
- Poor choice of default behaviour.
- Too much of your own politics are in your code.
A well-designed component should need very, very few required delegate methods – just the bare minimum to do whatever it does. Choose carefully. Equally, remember that it’s easy to add optional methods later, but it’s hard to turn optional ones into required ones (people will complain, and rightly so).
MGTileMenu has five required methods, four of which are data-source methods:
1 2 3 4 |
|
The first two follow the One True Data-Source Protocol. The third and fourth do too, but they also expose my politics: I think that software should be accessible, and I’m forcing you to supply a label and hint for each tile for VoiceOver to read. I’m comfortable with it.
There’s also one delegate method proper:
1
|
|
That one is required because it’s how you find out that a tile was activated. If you’re not willing to pay attention to that, MGTileMenu will do nothing useful, and you might as well not be using it at all. So, it’s required.
Rule 14: Design for accessibility
Following immediately on from the last rule: make things accessible. Don’t tack it on at the end, either: design for accessibility from the start. If you follow the “use controls in your controls” rule, you probably get this almost for free.
Delegate (or rather, data-source) methods, as shown above, are a great place to twist the arm of another developer to make them at least provide something for VoiceOver. And if you can automatically repurpose something visual (like a displayed text label) as a VoiceOver label, so much the better (again, in most cases VoiceOver already handles this for you).
Be socially conscious. Make it hard not to support accessibility. I also wrote an article about supporting VoiceOver in iOS apps, which Apple recommends to companies who contact them about accessibility programming. I recommend it too, but then I wrote it, so you’d expect that.
Rule 15: Use semantic objects for parameters
This doesn’t just apply to protocols, but protocols are where it’s particularly important. Use actual, first-class, semantically-appropriate objects for data, even if it’s more hassle for you to work with in your implementation.
If you’re asking for a date, don’t accept numbers – get an actual NSDate object. There are objects or structures for just about everything, and you should use them as intended. Create a class if you need to (you probably won’t need to).
The one standard exception, of course, is indices – there’s no reason for them to be anything but primitives, since NSNumber adds nothing that’s semantically important enough to offset the bundling/unbundling inconvenience.
Rule 16: Enhance the API if semantics don’t fit
I see this all the time. I mentioned earlier how you can think of almost any new, custom control as being substantially like something that already exists (often, it’s like the already-existing thing that you’re using behind the scenes for your implementation).
That’s great, and you’re very clever, but semantics trump similarity. It’s absolutely fine (and wonderful) to layer a new API on top of an existing one, in order to make the semantics fit. For example:
- A contact list implemented with a table should have a contacts-related API
- A month-view calendar implemented with a grid should have a date-related API
And so forth. Don’t force yourself (or other developers) to constantly be mentally converting between an abstract implementation API and the actual semantics of the component – make the API reflect the actual purpose of the component instead.
MGTileMenu’s delegate protocol does that by treating the menu not as a collection of UIButtons (the implementation), but rather as a unified menu, with numbered tiles each of which have relevant display properties.
Rule 17: Highlighting is interesting
I learned this one by having to go back and add new delegate methods and notifications to APIs I thought were finished. For interactive controls, highlighting is interesting. By ‘interesting’, I mean of potential significance to the surrounding app.
Any control will inform the app (in one sense or another, perhaps just by calling an action method) when it has been fully triggered, but comparatively few will notify when they’ve been visually highlighted (selected, pressed) or unhighlighted without being triggered. It turns out that that’s actually pretty important. The app might want to:
- Add, remove or reposition ancillary UI.
- Update some other part of its display.
- Offer some contextual help.
- Some other thing you can’t possibly foresee.
Highlighting is certainly an example of an optional set of delegate methods, but they’re important to have, and almost always trivial to implement.
1 2 |
|
Rule 18: Optional methods aren’t a commitment
Many of us approach optional delegate methods as an either-or situation: if you don’t implement them, you get the default behaviour, and if you do, then you’re totally responsible for what happens. That’s not ideal.
In any implementation which provides an optional delegate method, you should still fall back on the default behaviour even if the method is implemented, but doesn’t return something sensible. It sounds obvious, but it’s amazing how many components will blithely let delegate objects return any kind of craziness without sanity-checking, just because the delegate has somehow promised to behave itself by implementing the method.
I’m talking particularly about visual customisations, such as background colours or images. Consider very, very carefully whether you shouldn’t intervene in that case, and fall back upon your default appearance. Did they really want to show nothing? Does that even make sense? Will it make the control look broken? If so, step in, and serve up the default just as if the delegate method was never implemented in the first place.
Relatedly, have a documented, standard, unsurprising way todeliberately invoke the default behaviour via returning something like nil from each optional delegate method.
MGTileMenu, for example, has a relatively complex hierarchy of ways you can customise tile backgrounds. You can implement any (or all, or none) of three optional delegate methods to provide a background image, gradient or colour for each tile, in that priority order. You can also opt into the default behaviour for any tile at any time, by returning nil or NULL as is appropriate to the type.
You’ll have to try fairly hard (by returning clearColor
, or an empty UIImage object) to really, really make a tile’s background completely transparent.
Rule 19: Always say who’s talking
This is a simple rule, and an equally simple mistake to make. In your delegate methods, always pass the sender as a parameter. Always. Even for singletons. Even for things you cannot conceive would ever be used more than once simultaneously. No exceptions.
This:
1
|
|
Not this:
1 2 |
|
Rule 20: Put distinguishing params first in query methods
The One True Data-Source Protocol should always have query methods such that the most interesting thing goes first. The specific quality or property you’re requesting a value for. Like this:
1
|
|
Not like this:
1
|
|
The return type should flow naturally into the first part of the method name, without causing surprise. Data-source protocols often have many similarly-named methods, so keep the unique and interesting parts at the very start. Easier to read, and easier to autocomplete.
Some people have pointed out that Apple’s UITableViewDataSource protocol doesn’t do it that way, and instead puts the sender first, for example:
1
|
|
All I can say is: I’m aware of the difference. I stand by my argument.
Rule 21: Put the sender first in notification methods
The One True Delegate Protocol, however, isn’t for queries but rather for notifications. In this situation, you put the sender first (following our “say who’s talking” rule above).
1
|
|
This follows how an interaction would go between two people having a conversation. You wouldn’t just jump in and say “She’s going to be late,” because the other person would have to ask “Who?”
Instead, you start by saying who’s talking. It’s a convention, and handily distinguishes query (data-source) from notification (delegate) methods.
Rule 22: If a convention is broken, throw it away
Having said all of the above, remember that convention and consistency must at some point bow to superior judgement – in this case, yours. If a convention is broken, skip it without worrying. Rename things, if yours is truly better.
As an example, there’s a pre-existing convention for menu controls whereby you can enable or disable menu-items via the delegate, using a method called validateMenuItem:
. For the sake of consistency, I was tempted to use that same method name as part of my delegate protocol. I decided not to, because:
- It has a horrible, horrible name. “Validate”? That doesn’t say “enable” to me.
- It’s imperative, where in my case I’m really asking a question.
- It broke the naming scheme of my other delegate methods.
Instead, I went for something simpler and more understandable, if unconventionally-named:
1
|
|
We can debate the specific wording, but if you encountered that method you’d know what it was for and how to use it right away. To me, that’s better.
Notifications
Notifications are the other half of delegate protocols. My position is that, if you’re using a delegate protocol (you should, if it’s at all appropriate), then it’s incomplete until you add the notifications that naturally follow from it.
In MGTileMenu, you can find the notifications in the interface file for MGTileMenuController.
Rule 23: Notifications follow delegate methods
There’s a natural correspondence between delegate methods (proper; not data-source methods) and notifications. You use them in the same places in your code, and for exactly the same purpose.
If you have a delegate method that tells the delegate about something happening, you should usually provide a notification for that same purpose. Take your notification-like delegate methods, remove the interrogatory ones (the should
methods), and you have your list of notifications to implement.
The delegate methods’ parameters should match up with the notifications’ userInfo contents, with the obvious exception that you pass the sender as the notification’s object
, rather than bundled up in the info dictionary.
Delegate methods:
1 2 |
|
And corresponding notifications:
1 2 |
|
Rule 24: Be generous with notifications’ userInfo
Give a notification the information it requires in order to be useful. Remember that notification receivers may (and almost always will) not have anything to do with the delegate or data-source chain for your component.
Ask yourself what would be useful, and provide that information. At the very least, you must ensure that all arguments provided to the corresponding delegate method are wrapped up in the userInfo object.
Delegate methods:
1 2 |
|
And corresponding notifications:
1 2 3 4 |
|
Rule 25: Test the hell out of it
Finally, something we all already know. Software engineering and professionalism 101: make sure it actually works.
Whether testing means formal TDD is up to you, but testing itself isn’t optional. Every optional delegate method. Every posted notification. Every point of customisation, in every possible combination. Components provide a thousand opportunities for subtle issues.
There will be bugs. Find them and fix them first. If you’re pushed for time, cut a feature and debug instead. Thou shalt suffer no bugs to ship.
Final thoughts
I’ve formulated the above rules by learning the hard way, through years of making mistakes while creating components and their APIs. I do try to practise what I preach, though inevitably there will be a hundred examples of where I haven’t.
Whilst not all rules apply to all situations, and no rule applies in everycase, following as many of these as you can will give you a better chance of producing flexible, well-designed, reusable components for yourself and others to enjoy.
You may want to grab a quick summary of the rules, as shown below; I have the full-size version hosted on Flickr.
If you’re interested in releasing your components for others to use, as I did with MGTileMenu, you may also want to read my article on releasing open source code, which touches on some of these points and also talks about your README file, license choice, and related matters.
I hope you’ve enjoyed this article. If you did, perhaps you’d consider buying a non-attribution license for some of my open source components, or supporting future articles on this blog (and future source code releases) via a donation.
I’d truly appreciate it.
For more about software and user experience design, along with a multitude of other topics, you should follow me (@mattgemmell) on Twitter. You can also hire me for your own projects.
Now go and make great software.