How to Hire a Great iOS Developer

Sep 18, 2022

The population of iOS app developers has grown significantly in the years since the iPhone was released for 3rd party development in 2008. There are now well over a million iOS mobile apps available for download on the App Store, supplied by an ever-expanding global community of developers.

For those looking to hire a dedicated iOS developer, this newfound popularity is both a blessing and a curse. While it makes locating iOS programmers easier than ever, it also makes it that much more difficult to sort through the many available candidates to locate those who are really iPhone app development experts.

iOS developers

Part of the challenge lies in the fact that true expertise extends far beyond coding knowledge. It entails being aware of the various options that exist to implement a solution, as well as the tradeoffs that come with these different choices in terms of productivity, scalability, performance, and maintainability.

With that in mind, this article offers a sampling of questions that can be used to evaluate the depth and breadth of a candidate’s overall iOS expertise.

With regard to coding, this hiring guide attempts to be as language agnostic as possible, while still addressing the majority of the iOS and iPhone developer community (i.e., those who write iOS apps in Objective-C or those who are in various stages of adopting Swift and Cocoa Touch). Yet it is important to recognize that iOS apps can also be written in Ruby, Python, C#/.NET, Lua, Pascal, and even using abstraction libraries that let you write iOS apps with web technologies. That said, since the frameworks for iOS development are by and large written in Objective-C, a basic knowledge of some Objective-C concepts are required to do iOS development and are therefore discussed in this guide.

One note of caution: These sample questions are intended merely as a guide. Not every “A” candidate worth hiring will be able to properly answer them all, nor does answering them all guarantee an “A” candidate. Rather, these questions are meant to serve as a valuable component of an overall effective recruiting process. At the end of the day, hiring remains as much of an art as it does a science.

Assessing the Foundation

There are some fundamentals of iOS programming (Objective-C in particular) that a candidate should be familiar with. Here are some examples.

Q: Describe the use of methods and messages in Objective-C. Discuss some of the advantages and disadvantages of this approach.

A method in Objective-C is associated with a class. Objective-C has instance methods requiring (in object-oriented parlance) an instance of a class, and class methods which can be called with just a class (similar to the notion of static methods in Java, for example).

Methods are invoked by sending them a message. Here’s an example of sending a message to a myUser object:

[myUser allowAuthorizationTo: secretChamber forDuration: oneHour]

In Objective-C, unlike many other languages (including Swift!), we can send messages to null objects with no negative side effects (the message is ignored and execution simply continues).

In earlier versions of the Cocoa toolchain you could also call a method that the compiler, at compile time, couldn’t determine the existence of. While this may sound like a powerful capability, it was more commonly a source of hair-pulling debugging scenarios.

It’s important to note here that in addition to the “strange” square braces to send a message, to the uninformed eye, Objective-C appears to have a concept of external parameter names (a term borrowed from Swift), but this is not the case. In the above example, the method being called must have forDuration: plus an accompanying parameter, and it must be in that same position in the method call.

(For more information about how parameter names work in Objective-C, see this blog entry by Bill Bumgarner, written in response to this post on PyObjC by Toptal’s Ryan Wilcox.)

Q: What is a selector and what is its purpose? Provide an example of where and how it might be used.

A selector is the mechanism used to select a method to execute for an object, or the unique identifier that replaces the name when the source code is compiled. A selector acts like a dynamic function pointer that points to the implementation of a class’ method.

As an example, suppose you had a selector for the method run, and three classes (Dog, Athlete, and ComputerSimulation) that each implemented a method run. The selector could be used with an instance of each of the classes to invoke its run method, even though the implementation might be very different for each.

Q: How can you check dynamically at runtime to see if a class supports a method that can respond to a particular message?

The respondsToSelector: method returns a Boolean value that indicates whether the receiver implements or inherits a method that can respond to a specified message.

The ability to check dynamically at runtime if a class supports a method can be particularly useful with delegates (see the next question in this guide) that can have optional methods a developer can choose to implement or not. A common pattern is to use respondsToSelector: to see if a delegate object implements an optional method before sending it that message. For example:

if [self.myDelegate respondsToSelector: @selector(allowAuthorizationTo:forDuration:)] {
    [self.myDelegate allowAuthorizationTo: theVault forDuration: oneHour]
}

As noted in Apple’s iOS documentation, though, you cannot test whether an object inherits a method (from its superclass) by sending respondsToSelector: to the object using the super keyword. Sending respondsToSelector: to super is equivalent to sending it to self. Instead, you should use the instancesRespondToSelector: method on the object’s superclass, for example:

if( [MySuperclass instancesRespondToSelector:@selector(aMethod)] ) {
    // invoke the inherited method
    [super aMethod];
}

Q: What is the delegate pattern and what is it used for? Describe the delegates used by a UITableView.

Delegation is where one object relies on another object to supply some well-defined functionality, state information, data, or events. As stated in Apple’s Cocoa Developer’s Guide, “the main value of delegation is that it allows you to easily customize the behavior of several objects in one central object”.

A great example of delegation is the UITableView object that is typically used when a table is rendered on the screen. The UITableView employs two different delegates:

  1. UITableViewDelegate: Invoked if the user has, for example, selected a cell, or to determine what the height of a row should be.
  2. UITableDataSource: Used to retrieve information relating to how each cell in the table should be rendered, how many rows there are, if rows can be edited or moved, and so on.

Q: What are categories in Objective-C and how do they differ from subclasses?

A category is a way to extend an existing class and is an alternative to the use of subclasses. The main distinction between subclassing and categories is that a category adds functionality to an existing class itself which is then available anywhere in your code that uses that class or its subclasses. In contrast, subclassing creates a new class with added (or altered) functionality. You must then use that new subclass, rather than the original class or any of its subclasses, to access this extended or revised functionality.

For example, the common string object in Objective-C is NSString and it has a mutable subclass NSMutableString. When you use a category to extend the NSString class, every NSString and NSMutableString object in your code will support these new methods. These new methods can then be invoked on any NSString or NSMutableString in any class that imports the category header file.

A potential pitfall with categories, though, is name clashes. If you implement a category method with the same name as an existing method in the referenced class, the behavior at runtime is undefined (i.e., it is unpredictable which of the versions of the method will be invoked). To avoid name collisions, Apple recommends using a three-letter prefix before any category methods.

Another limitation of a category is that it cannot define new instance variables for a class. However, an experienced developer will be aware that you can define new state objects associated with the class using Objective-C runtime methods, which provides a way for a category to store state changes.

Q: Describe error handling in Cocoa.

Most errors are handled in Cocoa via NSError. Two of the more common approaches are as follows:

  1. Pass a blank NSError object into a method and, when the method completes, check to see if that object is still blank or contains an error. (To be entirely precise, pass in a pointer to the NSError pointer, or NSError ** in Objective-C parlance.)
  2. Pass an NSError object to some failure delegate method or callback.

NSError objects have an error domain, an error code, and user info dictionary for additional information. This additional information might include, for example, a localized description and recovery suggestion, so later code can display that to the user.

In addition to NSError, iOS development also has NSException. Unlike exceptions in other languages, NSException is intended to be used for programming errors only. Cocoa frameworks in general are not exception-safe, so if you generate (or invoke code that generates) exceptions, care should be taken to use try/catch as close as possible to the place where the exception is occurring. In practice, NSException is rarely used in iOS codebases.

Q: What are some common causes of exc_bad_access errors and how might you go about debugging them?

exc_bad_access errors are a common occurrence that can frustrate beginner iOS developers due to the lack of useful debugging information that they provide. They often occur when trying to access an object that was never initialized or has already been released. These errors can also result from passing a parameter to a message that the message is not intended to receive (e.g., passing in an NSInteger when an NSString is expected).

Note that, in certain cases the compiler will provide a warning that, if heeded, can alert the developer to the problem so it can be resolved before any runtime error occurs. But if such warnings ignored, exc_bad_access errors may occur. Here, for example, is erroneous code that is attempting to format a string using an integer variable:

[NSString stringWithFormat:@"This is %@", 123];

The %@ in the string format specification indicates that an object, rather than an integer, is expected (an integer would be represented with %d). This statement will therefore result in a compiler warning and, if ignored, will result in an exc_bad_access error at runtime.

But not all causes of exc_bad_access errors will be identifiable by the compiler.

A common cause of exc_bad_access errors that are not detectable by the compiler is attempting to access released objects. In such cases, enabling NSZombies in Xcode can be an effective debugging technique. Enabling NSZombies keeps objects that would normally be released alive as “zombies”. After enabling NSZombies, you can then follow what is happening in your code and receive a message when the application tries to access an object that has been released, thereby identifying the problem in your code. A note of caution though: Be sure to remember to disable NSZombies before submitting the app to the App Store. When NSZombies is enabled, no objects are released and you will be leaking memory constantly until your app eventually is killed due to memory warnings.

Digging Deeper

As mentioned, true iOS expertise extends far beyond coding knowledge. Highly experienced iPhone application developers will have a thorough understanding of what’s going on “under the hood” on the iOS platform. They will also be well aware of the various implementation options available, including the tradeoffs that come with these different choices in terms of productivity, scalability, performance, and maintainability. The questions that follow help assess this dimension of a candidate’s expertise.

Q: Compare and contrast the MRR and ARC approaches to memory management in Objective-C.

Objective-C keeps track of what objects are in use, or not, by use of a retain count. When an object is in use by another object it’s retain count goes up; when it is not being used by any objects, its retain count reverts to zero.

In Objective-C there are two ways to manage this retain count:

1. Manual Retain-Release (MRR)
2. Automatic Reference Count (ARC)

With MRR, developers had to worry about autoreleasing an object, or putting it in an auto release pool in certain situations.

In contrast, ARC automatically takes care of many of these concerns. Most iOS applications and developers have therefore switched over to ARC. In fact, with Swift, ARC is required.

While ARC does require some thought (around object ownership for example) the requirements on developers are much less with MRR.

Q: How can you improve performance when drawing and animating a view with shadows?

If you need to apply a shadow to a view, many inexperienced developers will import the QuartzCore framework and apply a shadow to the layer property of the view; e.g.:

UIView *shadowedView= [[UIView alloc] init];
 
shadowedView.layer.shadowOffset = CGSizeMake(1.0, 1.0);
shadowedView.layer.shadowRadius = 2.0;
shadowedView.layer.shadowOpacity = 0.9;

While this is a simple way to add a slightly transparent shadow on a view, it can result in poor rendering performance. Unfortunately, the rendering engine usually needs to check each pixel of the view to determine the shadow’s outline.

However, you can define a shadow path to avoid this expensive calculation and optimize the rendering. For a standard rectangular view, you would define a shadow path as:

shadowedView.layer.shadowPath = [[UIBezierPath bezierPathWithRect: shadowedView.bounds] CGPath];

This provides much better performance, though you must keep in mind that whenever the view’s bounds change, the shadow path also needs to be updated.

By default, when animating a view with a shadow, the shadow is redrawn during each stage of the animation. You can optimize animations with shadows by rasterizing the layer of the view with the shadow. When the layer with the shadow is rasterized, it is rendered as a bitmap in its local coordinate space and then composited to the destination. Here’s an example of how you could rasterize your shadow effects:

shadowedView.layer.shouldRasterize = YES;
// and we need to ensure that it rasterizes for the pixel density of the screen
shadowedView.layer.rasterizationScale = [UIScreen mainScreen].scale;

Q: What are some common causes for poor performance in scrolling table views? How can they be avoided?

When the iPhone originally came out, smooth scrolling was one of the keys to the positive user experience. However, there are many mistakes a novice developer can make that can cause a lag to occur while scrolling. Examples include:

  • Not using a reuse identifier properly. The default behavior when a table is scrolled is that table cells that go offscreen are deallocated and new cells are created for the new content that is shown. When you use a reuse identifier, when a cell goes off screen it is not deallocated and instead cached for reuse. If a new cell is to appear with the same reuse identifier, the cached cell is used and the expensive operation of allocating and laying out an entirely new cell is avoided. Instead the cached cell must only be reconfigured to display the new content, such as updating the text and image that is displayed.
  • Synchronously loading web content. If loading is done synchronously, it will stall out the main thread while the data loads, which can cause a perceptible lag when scrolling. This is a particularly common problem when images are being downloaded for displaying in table cells. The solution here is to use a placeholder value or image and load the data asynchronously in the background. Once the data is loaded, a message can then be sent to the main thread to update the appropriate cells.
  • Applying expensive operations when setting up a cell for display. Expensive operations, such as converting a high-resolution image to a thumbnail or applying a complicated photo filter or transform to an image, can severely degrade display performance. If you only need a thumbnail size image, store a smaller size locally. Similarly, apply any complex transforms to the image in advance if possible. Alternatively, perform the necessary operations on the image off the main thread and then refresh the cell once the image is ready for display.
  • Overly complex view hierarchies. While you can use the various UIViews to create custom cells, this has the potential to degrade performance if it is overused or gets overly complex. Although this has become less of an issue with the increasing speed of supported iOS devices, it still can impact scrolling performance in some devices and scenarios. Although tedious, this problem can be addressed by implementing your own drawing of the cell’s content (i.e., replace the UILabels, UIImages, etc., with a UIView and then implement your own drawing in that view). If done properly this can significantly improve performance when rendering cells.

Q: Diagram out the Core Data stack.

Core Data is Apple’s model layer, persistent object store, and object graph manager. You might be tempted to call Core Data Apple’s Object Relational Mapper, but it’s really more than that.

Below is a diagram of the Core Data stack.

Note: It’s extremely rare to find a developer who can map out the entire Core Data stack from memory. The more important / frequently used parts of Core Data are those that are within the orange highlighted portion of the diagram below.

Core data stack - C&M Blog 


Q: Is Core Data thread safe? How can you deal with this in your app?

Core Data is not thread safe. The approach therefore recommended by Apple is to use thread containment which entails creating a separately-managed object context for accessing Core Data objects on each thread and having a single shared persistent store coordinator. If you need greater concurrency, you can also have separate persistent store coordinators, though this comes with added complexity and also increased memory usage.

Before iOS 5, the developer needed to manually manage the threads. However, the release of iOS 5 added new methods to the managed object context to eliminate the need for manual thread management; instead you can safely access Core Data objects through a block of code passed to the performBlock: or performBlockAndWait: methods.

Keep in mind, though, that changes to Core Data objects do not necessarily propagate immediately to other threads. In the standard case, Core Data objects that are loaded into memory are not updated on a different thread until those objects are fetched again from persistent storage. In order for a managed object context to take in changes from Core Data objects on another thread immediately, it needs to be observing the NSManagedObjectContextDidSaveNotification. Once this notification occurs, the object can merge the changes using the mergeChangesFromContextDidSaveNotification: method.

Q: Describe different ways to achieve concurrency on the iOS platform.

The following mechanisms are provided on the iOS platform for achieving concurrency:

  • NSThread. Cocoa provides support for creating threads and executing code by subclassing NSThread.
  • NSOperationQueue. NSOperationQueue is a simple API that determines how many things can run simultaneously, then goes and does them. It allows you limit the number of operations running concurrently (e.g., to limit the number of concurrent downloads). While simple and useful, it’s a layer of abstraction beyond manually managing NSThread and the complications that can involve. (It’s often sadly unused.)
  • Grand Central Dispatch (GCD). GCD is Apple’s approach to providing access to low-level concurrency. There’s a lot in GCD, but sometimes you really want a low level tool. GCD includes some useful tools, particularly the following:
  • dispatch_async: When given a task (the code in a block) it will run it at some point in the future.
  • dispatch_group_async: You can create a group of dispatched tasks using dispatch_group_create() and dispatch_group_async(). You can even wait for all the tasks in the queue to complete with dispatch_group_wait().

Bonus Section: Swift

Swift is a new programming language from Apple that is referred to as “Objective-C without the C”. Swift can work side-by-side with Objective-C, offering many improvements that enable developers to create apps that are more stable and have better performance in less time. Overall, it is the single biggest change to iOS development since the initial release of the iOS SDK. Unveiled at Apple’s WWDC in 2014, and usable in production with the release of iOS 8 in September 2014, Swift will also be compatible with iOS 7 and OS X Mavericks.

The Apple frameworks are used in the same manner in Swift, so an experienced iOS developer is able to transfer most of his or her knowledge to writing Swift code. That said, there are several issues an experienced iOS developer should be aware of regarding Swift and its interoperability with Objective-C.

Q: In what ways can Swift and Objective-C code be used together in the same project? What are some limitations in this regard? Provide an example.

Both languages can be used in the same project, thanks to bridging header files that Xcode automatically creates between the two languages.

An example of this can be seen in initializing a plain table view.

In Objective-C syntax, this would be specified as:

UITableView *tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; 

In contrast, the Swift bridge removes the initWith syntax and modifies the formatting automatically to the Swift syntax as follows:

let tableView: UITableView = UITableView(frame: CGRectZero, style: .Plain)

Fortunately, Swift is able to take advantage of anything written in Objective-C, so you can create a new Swift project and still utilize any existing code base.

However, this bridging only works perfectly in Objective-C to Swift (and not vice versa), so code that takes advantage of new features in Swift will not be accessible in an Objective-C file. Examples of this include generics, tuples, and structures defined in Swift, for which there are no Objective-C equivalents. Unfortunately, this can make it difficult or impossible to utilize some of the exciting third-party libraries that are being built in Swift with an existing Objective-C code base.

Apple provides detailed documentation on how to utilize Swift with Objective-C, which every experienced developer should become familiar with.

Q: Compare method calling and messages in Objective-C and Swift. Provide an example.

While Objective-C uses the unique bracket syntax to call methods on an object, Swift uses the more conventional dot notation.

So, a message like[myUser allowAuthorizationTo: secretChamber forDuration: oneHour] in Objective-C turns into myUser.allowAuthorizationTo(secretChamber, forDuration: oneHour) in Swift.

Q: Compare error handling in Objective-C and Swift.

Writing Swift code that interacts with Objective-C means dealing with NSError in the same way Objective-C does. In Swift, however, exceptions are no longer supported.

Using a method that takes an NSError means using an optional variable (a variable that may or may not exist) and the unwrapping the error if it exists.

As Swift is a new programming language, best practices around error handling, especially Swift code calling other Swift code, haven’t really emerged yet as of this writing (September 2014).

Wrap-up

With the iOS app development scene getting shaken up by the release of an entirely new programming language (Swift), it has become more important than ever to find elite iPad and iPhone app developers who have mastery of the application development platform and can transition between these two languages. While finding these experts for your development team on a full-time or part-time basis is always a challenge and no brief guide can cover the topic entirely, we hope that the questions in this post can provide a starting point for identifying developers with a solid mastery of the mobile app development environment.

Charvi Goyal

Building a global developer community to connect the best opportunities to the best talent.