iOS Development Isn’t in a Good place

I’ve programmed on iOS for years: first in 2010–11, then in 2016 with ARC, and from then on in Swift. Along the way, I’ve noticed several small things that, when put together, lead me to the conclusion that native iOS development experience isn’t in a good place. This covers documentation, frameworks, languages, error-handling, Xcode…

Let’s start with the documentation.

First, a lot of it has been marked as “no longer being updated”. And this info isn’t available in any other place — it’s not a question of clicking a redirect link and reading another document instead. Apple has deprecated the old documentation without bothering to write new ones. Maybe they think of documentation as a nuisance.

Second, important APIs like DispatchQueue’s initialiser has no documentation at all. And dispatch queues are a fundamental iOS framework used by many apps, which Apple has pushed for years. There is documentation about dispatch queues in general — oh, wait, “this document is no longer being updated”—but not about this specific initialiser, and even that documentation isn’t linked from the initialiser’s documentation page. Unless you already that there’s a document named so and so, you won’t find it. I’ve come across documentation that could have saved me days of time, after wasting the time. This happened on multiple occasions.

Third, a lot of knowledge is in WWDC videos, some from years ago. Apple expects us to watch years of videos to learn what we need to learn, and then watch newer videos to correct things from earlier videos that are no longer supported or recommend. You learn this trick only with experience developing for iOS. There’s a site where you type your keywords in and hope that a video comes up. iOS developers have had to resort to hacks like these.

Fourth, moving on from documentation, some framework classes like UIPageViewController are buggy and crash or otherwise misbehave [1]. Since it’s closed-source, you can’t even read the code to figure out the bug and how to work around it. You apply a workaround, and it seems to work, but a week or two later you realise that it still crashes, then apply a second workaround, and repeat the cycle again. And a third time. You’ve wasted multiple days to a week. Apple doesn’t seem to bother to fix these bugs, probably because they don’t care about our productivity.

Fifth, common abstractions like a photo gallery are missing. It took us one person-quarter to implement, because there are many cases to handle: swiping to move back and forth, deleting the current photo, pinching to zoom, double-tapping to zoom, single-tapping to hide or show the navigation bar, taking care that a double tap doesn’t also trigger the single-tap callback (because a double tap starts with a single tap), taking care not to zoom the whitespace around the photo as we zoom the photo, swiping down to close the gallery, calculating the zoom level based on the dimensions, deciding whether to leave the minimum zoom level at 1 and set the maximum zoom level or vice-versa (only one of which works, and it’s the counter-intuitive choice), and so on…

Sixth, some APIs come only in an asynchronous form. But if you’re invoking it from a background queue, it may be simpler to call it synchronously. But iOS doesn’t offer one. You have to build a wrapper yourself, learning how to use DispatchSemaphores for this. The opposite problem happens too, in the form of APIs that are available only in a blocking form [2]. Ideally, every API should be available in both a blocking and an async form, so that you can use whichever is convenient. To be fair, this is a common problem across development environments, not specific to iOS, so it wouldn’t be fair to blame iOS for it.

Seventh, Swift doesn’t have promises, to let you do async operations one after another. Suppose you want to ask for camera, photos and location permission, each in turn after the async call for the previous one completes. This becomes easy with promises. You can also also set up dependencies like, “When the camera permission is granted, start showing the camera live preview” and “When both camera and photos permissions are granted, do this”. The overall structure becomes clear, with promises, as opposed to obfuscated, which is a problem with async code in general. But iOS doesn’t support promises, because it comes from an earlier era and doesn’t feel like it has been updated for 2018. There are third-party promise libraries, but it has to be part of the OS if other third-party libraries are to adopt it. If one library uses one promise library, and another library uses an incompatible promise library, you can’t integrate them both into your app.

I’ve been waiting for promise support for multiple versions of Swift, but the Swift team hasn’t delivered. Swift has been taken over by theorists who debate academic things or solve yet another corner case in their type system, making a complex language even more complex, rather than deliver what their users (developers) actually need to build real-world apps.

Eighth, some async APIs invoke your callback in the queue you called them from, and some do so in an arbitrary queue. A few don’t even document their behavior [3]. This can cause race conditions that have to be caught in code review by a more experienced engineer, or the hard way by debugging a crash report. I filed a radar to unify these, but Apple marked it as Won’t Fix.

Ninth, moving on from threading, and talking about errors, some iOS APIs return non-descriptive error codes like -16405. iOS still hasn’t learnt from Java, which would use something like InvalidFilePathException, which immediately communicates to the programmer what went wrong.

Swift does have an object-oriented error handling mechanism, in the form of an Error protocol, with concrete implementations for specific errors, similar to Java’s Exception and concrete subclasses like IndexOutOfBoundsException and FileNotFoundException. But that’s all in theory. In practice, Apple hasn’t modernised their legacy APIs to use the modern object-oriented error-handling mechanism. We’re still stuck with -16405. Yes, this would be a big effort, but that’s what it takes to keep a development environment productive for developers to use.

While an object-oriented system is best [4], it’s not needed. Apple can stick with ints, but document all error codes in one place, and impose an internal rule on their engineers every code used anywhere in the iOS codebase should be documented. When you look up the error, you should get guidance as to exactly what caused the problem (or common causes if there can be multiple), and what to do to fix it.

Another alternative is string codes, rather than integer ones. “INVALID_FILE_PATH” would immediately convey the message. It’s not 1980 anymore. We can and should use a few more bytes to save someone hours of work.

Tenth, some errors, like NSInternalInconsistencyException can’t even be caught in Swift. You have to write an Objective-C wrapper that catches this exception and returns it as the return value.

The integration between Swift and Objective-C is well done, much better than say Java/C++ or Python/C, but there are still some warts that give a feeling of two systems glued together, requiring you to understand how each of them works and where things get lost in translation, as opposed to one coherent, productive system.

Eleventh, things sometimes fail without an error. I was trying to record a video, using AVCaptureMovieFileOutput, which is supposed to save to a file, but it didn’t. There’s no error code (not even -16405, which I can Google), no exception, no log message telling me what went wrong, nothing. I wasted a day or two trying various possibilities but nothing worked. Only days later did yet another possibility strike me, which turned out to be the right one. Nobody wants to waste days because some Apple engineers didn’t follow basic programming practices like not failing silently.

Twelfth, Swift code can still crash in a way that the caller can’t recover from, thanks to things like nil unwrap failures. Swift still hasn’t learnt from what Java got right in 1996, which is that Java code can’t crash. It can only throw an exception that the caller decides how to handle. For example, if you’re building a camera app and you want to geotag photos by using a third-party library, the library can crash your app. In Java, you’d just catch all exceptions, log them to your analytics server for fixing, and continue running. As a user, I’d much rather have a photo that wasn’t geotagged over losing my photo. As an engineer, a good programming language lets me build software from components in a robust way, not where one component can bring the whole thing down like a stack of dominoes.

Thirteenth, Swift is not dynamic, like Objective-C, and doesn’t have anything like Objective-C’s id, which is dynamically typed. You can invoke any method on id, and it either succeeds or fails at runtime, like Python. I wouldn’t use id 99% of the time, but when I need it, it’s invaluable. In that sense, Swift is three steps forward, one step back.

Fourteenth, Swift is needlessly complex a language, in the same realm as C++, as opposed to simpler languages like Java or Python. The type system has all kinds of gotchas that can crash your app in production. As a second example, I was trying to compare if two strings are equal, and it crashed. When I investigated, it turned out that both strings were nil, and implicitly unwrapped optionals, which the compiler unwrapped before comparing. As a third example, I tried to override a function that took a string with one that took an optional string, and the compiler rejected it [5]. Why? The implementation can handle all the cases callers might give it, so it should logically work.

As a fourth example, I found that it’s impossible to write a generic function in Swift that takes two floating-point numbers of any type (like two floats or two doubles or two CGFloats) and checks if they’re equal within a margin of 0.1. This is trivial in C++:

template<typename S, typename T>
bool isRoughlyEq(S a, T b) {
return fabs(a — b) < 0.1;
}

This also handles heterogeneous types like a float and a double. Swift can’t even handle two numbers of the same type.

Swift is a needless complex language, complex in ways that don’t often translate into better code in day-to-day usage.

Fifteenth, Apple doesn’t use cross-platform APIs like Vulkan, instead reinventing the wheel with things like Metal. They’ve also deprecated OpenGL, which was the cross-platform option. I’ve found that rewriting my simple Core Image kernel in Metal didn’t improve performance, despite what Apple says. Metal doesn’t support old phones like the 5s. It doesn’t support the simulator, which is important to test my algorithm with dozens of images, which don’t fit in the 4GB memory of the iPhone. I don’t want to invest too much in Metal since that won’t help me when I port my app to Android or Windows. I’ll stick with the deprecated OpenGL option till it stops working.

Sixteenth, iOS doesn’t support reactive programming like ReactJS. You have to manually track dependencies — which UI element should be updated when a given model layer property changes. It’s needless overhead for a human to do what the computer can, and you’ll invariably miss some, causing bugs. There are a lot of dependencies even for an app that looks trivial on the surface, like an iceberg. It’s fine for people who don’t want reactive programming (if that’s the right term for this) to be able to opt out of it, but the rest of us should have a smoother programming experience.

Seventeenth, Xcode is buggy. It sometimes crashes, or needs a clean rebuild. Or you have to delete your Derived Data folder. Refactoring sometimes fails with an exception saying the ranges didn’t match or something. Apple’s declining software quality with user-visible software like iOS and macOS is also manifesting in developer-visible software like Xcode. Xcode can’t even do simple code analysis that Eclipse used to be able to do a decade back, like identifying dead code.

What does all this mean?

You may think that some of these complaints are small ones, or that they’re common to most programming environments. You’d be right about any individual complaint, or a few of them together. I hesitated to write this blog post because it can come across as being unduly critical.

But when you put together all the above problems, they add up, and it leads to the conclusion that developer experience is not a good place. It’s death by a thousand cuts. It’s an exaggeration, to be clear, but I hope it gets the point across [6].

So what? Some people will complain about it and go back to Xcode, right?

The problem is that the iOS SDK has competition, in the form of hybrid app frameworks. Historically, they haven’t been competitive if you want to build a polished consumer-quality app. But promising frameworks like Flutter are being built, which use C++, bypassing the problems of the HTML/CSS/JS stack. Other companies like Basecamp have found that the sweet spot is 90% web content with native navigation, and that there was no discernible difference with a fully native app, except that the web version is much faster to iterate on. Basecamp says that “the majority of information-based apps today [2014] can be successfully implemented through this approach with varying levels of the native/HTML split”. As time goes by, more developers will find one of these approaches to work for their app. There will always be apps like NoctaCam that integrate closely with the hardware and do computational photography on the GPU, but most apps just fetch JSON from a server and display it, and these will soon be viable with hybrid apps. Since innovation in mobile has slowed down for the past few years, apps in 2018 are doing the same things as they did in 2016, so hybrid app frameworks will be able to catch up.

To be clear, native vs hybrid is a decision involving many aspects like:

  • How likely are we to find that hybrid doesn’t work for us and have to rewrite it in native code, which is the worst outcome possible [7]?
  • Do we care about Android or are we okay with Android users getting what they get?
  • Do we even have the resources to build two apps?
  • Shouldn’t we validate that this product meets users’ needs and has a large enough market before we commit to two native apps?
  • Do we prioritise UX, or are we a customer-hostile organisation like a bank?
  • Do we need to support people accessing us in a mobile browser? If so, we should consider building the core as a web app, plus extra functionality when it’s installed as a native app.
  • For us, time to iterate is more important than eliminating small UX annoyances, so we should use hybrid.

As you can see, this decision doesn’t come down a single factor. It’s not “iOS development is broken so people will switch to hybrid”. It’s more like “As we evaluate whether to use hybrid, the friction of native iOS development is a factor to be taken into account”. I know that if I build a mobile app that doesn’t require close integration with the hardware, I’ll prototype it in Flutter to see if Flutter works before building in native [8].

It’s not wise for Apple to rest on their laurels and have an attitude of, “Our users (developers) can’t do anything about it anyway” or “There’s no exodus, so there’s nothing to be concerned about”. When competition comes viable, people will switch. And when they do, they won’t come back. Fixing these issues takes time, so Apple should start now, not wait till developers start leaving and then take 2 years to fix, which would cause more damage to the iOS ecosystem.

Maybe this won’t happen and I’m being too negative, but if I were Apple, I wouldn’t be cavalier, given the important of native apps to the iPhone.

[1] UIPageViewController maintains a cache of which page is before or after a given page. But if that changes, UIPageViewController doesn’t know, causing the crash. It doesn’t offer a way to disable this cache, or at least invalidate it. There’s a workaround — set the current page to the one that’s already the current page, which seems to invalidate the cache. But you later realise your app still crashes. Is it the same bug or a different one? Who knows.

[2] iOS should’ve offered an async static method in NSData that creates the NSData asynchronously and gives it a callback, like:

NSData.from(filePath) { data in
// do something with the data
}

This problem is best solved at the programming language level, with language support for sync and async versions of the same function. You invoke whichever you want, and it just works. Behind the scenes, the person writing the code can implement whichever form he wants to, and the language will synthesise the other one. Or the API implementor can provide both forms if they’re more efficient than the language-synthesised ones. All that becomes an implementation detail. Whether a function is synchronous is up to the caller. This is like property getters and setters. When you set a property on an object, you don’t have to think about whether the setter was provided by the programmer or by the compiler. It just works. Async should be like that.

[3] Ideally, all async APIs would take an additional argument, which is the queue to invoke the callback on. This would be an optional argument, defaulting to the current queue, but you’ll be able to pass nil in case your callback can run on any thread without race conditions. This would be safe by default, and performant when needed, which is the right way of handling it.

[4] Because you can have subtypes of errors, like a CouldntRecordVideoException having subclasses like InvalidFrameRateException and InvalidResolutionException. That way, unless you want to treat the latter two cases differently from each other, you can ignore this detail and just think in terms of CouldntRecordVideoException.

It’s not necessary to actually throw exceptions. Functions can also return an Exception along with the actual return. That would still be object-oriented, with the aforementioned advantages.

[5] Or was I trying to override a function that took an optional with one that took an implicitly unwrapped option or vice-versa? I don’t remember. And that is again a problem with Swift’s complexity. Many programmers don’t remember all the edge cases in an overly complex language.

[6] I’ve worked on far worse development environments like J2ME and Symbian. By comparison, iOS development is a joy. But expectations are higher in 2018 than in 2010. In any case, we should look forward rather than backward, and ask how iOS is positioned relative to its future competitors, namely hybrid app frameworks.

[7] This is the biggest risk I see with hybrid apps, and one that my team suffered from: we spent 2 quarters building a hybrid app, only to find that it didn’t work, so we had to rewrite it in native, taking 3 more quarters. By trying to save time, we wasted a lot of it.

Decisions become hard when you don’t know how long one of the options takes. If you did know, like “Option A takes 2 quarters, and option B takes 3 quarters”, then it becomes much easier to decide.

[8] once it becomes production-ready.