Nat! bio photo

Nat!

Senior Mull

Twitter Github Twitch

Dienstag Postmortem

Since my first commercial iOS App called Dienstag is now in the acceptance process of the App Store, I have some idle time to reflect on two years of development on iOS. I liked the postmortem column on Gamasutra, so that's the reason for the title of this entry. But this postmortem is almost completely technical :)

For the human/marketing angle, checkout our facebook page, where I am sure a "like" would be very much appreciated. Dienstag App

The development history in a nutshell

The project started out on iOS 5, which was quickly succeeded by iOS 6. I would have been extremely surprised at the beginning, if someone had told me, that at the time of iOS 8s release our app still wouldn't be done yet. But here is a recollection of all my faults: why it took way too long.

Dienstag App is a fairly big App, it contains 120K lines of core code, 30K of library code, that wasn't specifically written for Dienstag and about 2K lines of third party codes (Finch, Indenticon) as well as the opaque Crashlytics framework. It has 25 xibs but no storyboard and it is 100% ARC free ;) It was developed for iOS 6 and since it uses UICollectionView, it can't run on iOS 5.

The project started 2 years ago, so 120K lines isn't all that much. Unfortunately in this time, I probably wrote three times as much code and threw most of it away again.

I take the blame for the first rewrite, because what I designed, turned out to be unmanageable and too complicated. Basically I was still learning iOS and CoreData and the user interface was a bit too radical a departure from standard iOS fare. I made it much simpler in the next iteration, but when I thought I was nearing completion, more than a year had already gone by.

But then "marketing" wanted something else, and a second somewhat unintentional rewrite began. There were some shiny, competitor apps for the same segment. And the question was, why can't it more like this ? Yes, why can't it, because it's completely different... But after some discussion, it was mutually agreed that we would offer a second screen, that would add similar functionality. The old screen got laid to rest for a while.

Quite a few more months passed. Internally the new code was very much favored and the old screen got neglected and staled away. But then we tested the market with a prototype of the new screen and found, that everyone liked the looks of the new app, but functionally everyone wanted the old app. Unfortunately there were now already too many changes and too much effort invested in the new code, that resurrecting the old code, was not very attractive. So most of the old functionality had to be brought back, but coded very differently.

With hindsight I have to say, doing this third iteration was a huge mistake on my part. We should have just brought the second version to market. The rewrite would have been a nice v2.0. I was a little too happy coding new features for the "new" screen and underestimated how much time it would take.

Anecdote

When finally we were about done and through beta testing. iOS 8 happened and the app stopped working. Oh joy. I used up one of my precious DTS incidents to let Apple figure out, how to get around the stopping iOS bug, so that the app would run with iOS 6, 7 and 8. But DTS seemed to be more interested in criticizing the demo code I sent. That code ironically, was some old Apple demo code, where I just put in the two or three lines that triggered the bug.

I found the proper solution on openradar in the end...

What went right ?

Used Stackoverflow

I just learned a whole lot about iOS programming using stackoverflow. I would have wasted so much more time on iOS peculiarities, if the solution to 90% of my problems wasn't a quick search away.

Used Crashlytics for beta-testing

Crashlytics doesn't cost anything, it's from Twitter and it works well for me, to collect crash dumps from test devices. It's also a nice way to distribute your beta apps. Unfortunately the majority of testers have the attention span of a 3 year old, and if the app doesn't install immediately, they have forgotten it already. Installing an app via Crashlytics is a three step process, at least one step too many.

Previously I used a combination of Diawi for distribution and QuincyKit for crash reports, which are also nice projects, but a bit more limited and cumbersome. I haven't tried TestFlight yet.

Used mogenerator for CoreData class generation

I suspect everybody uses this as well. I started out trying not to use it, because I like to keep external dependencies down. But CoreData without mogenerator is a drag.

Used AppCode to find bugs

I bought AppCode solely to run "Inspect Code...". The results returned are quite a bit more helpful than what Xcode Analyzer returns.

Wrote many small test apps to try out pieces of code

I probably wrote a hundred little apps, that tested out some feature, or started coding a subview with it. When the code was complete I moved it into the main app, deleted the original files and then symlinked the files from the main app in the test app. This way, I could go back to the test app to tweak something, when it didn't work out in the main app. Needless to say being able to focus on just a small piece of code in a controlled environment is much more convenient. It's like a test, but it's not :)

Used assert and NSParameterAssert pervasively, did not write a single test

So when the app was run by me or someone else and something unexpected happened, it crashed and I got the stack-trace. I wrote over 1000 asserts, it crashed a lot. Currently there are no known crashers.

A test is something that I write, when I am writing a library and I need a test bed to actually debug the code. Then a test is something of value to me, rather than a drag. Otherwise a test is kind of an investment in the code. Changes to the main code now are bigger changes, as I need to update the tests or write new ones. Typically though, they just stale away. Gotta keep nimble...

The way I code is like a farmer, there is a set of "Bauernregeln", which I like to follow. And only if I run into a problem, I start to think it over.

Bauernregel
  • "If it's a library, write a test. If it's an app, write a test app"

Used Finch to play sounds

I wouldn't use anything else but Finch for doing sound effects on iOS. It's lightweight and it works.

Wrote my own template engine to customize output

Dienstag creates HTML output. To interface with the app I concocted a little template engine called MulleScion, which these key features:

  • it can be embedded into HTML and the code is still valid HTML code, which makes it possible to use it with HTML editors
  • it has some limited control features
  • it can deal with compiled and compressed templates
  • you can put ObjC-Code right into the template
  • has some security features, so that a template can't run amok

It was nice that I could offload the HTML coding and didn't have to redo the template, everytime something was tweaked in the HTML.


What went wrong ?

Choice of CALayer over UIView

I looked at what both classes do, decided that what UIView adds is fairly minimal and decided to do as much as possible with CALayers. This didn't work out so well for me, because CALayer isn't "really" Objective-C last time I looked, but rather C++ wrapped into Objective-C. That means that some coding assumptions, like "I can override a method on CALayer and do some custom stuff there" was not necessarily working as expected.

My perceived advantages of UIView over CALayer

  • UILabel does a lot more layouting than CATextLayer
  • gestures and touches need to be usefully handled by a UIView anyway
  • tie in with UIViewController
  • easy animations of groups of UIViews with +beginAnimations
  • an UIView is a pure Objective-C object

I especially reconsidered my approach, when I was actually thinking about doing my own text layout engine, as I was drawing texts in CALayers at the CoreText level.

Bauernregeln
  • "If a screen area should respond to touch, it's always an UIView"
  • "If it is a passive part of a composited view it's likely a CALayer"
  • "If it's text, it's probably an UIView"

Did not use UIViewController containment initially

For whatever reason I didn't latch onto UIViewController containment immediately. Probably from reading older documentation, I thought at first, that you could only have one UIViewController. Because the app is fairly complex, separating the code into separate UIViewControllers was very beneficial to reduce the complexity.

But it came at quite some cost later in the project, to rewrite everything, because a lot of controller code was now partly in an incompatible self written controller hierarchy and also partly in views.

Tried to adapt third party software instead of writing from scratch

In principle I really like the idea of collaboration, yet what I want from a software is apparently totally different, than what most other programmers want. First of all, everybody seems to be really fond of ARC code. Coz you know magic! And much faster than regular ObjC code... More code, and more dependencies are obvious indicators of a powerful code base. Everything should be rewritten to use as much blocks and async GCD queues as possible. So it's a bit of a one way street. Fork + convert = insular code only I care about.

I tried to base my main Calendar on PMCalendar, you can see the first rewrite on Github as MulleCalendar.

This failed, because even after rewriting the whole project to fit my style more, it didn't adapt well to broader changes.

In a way software is like a crystalline structure, you build up a core, and the rest of the code sort of crystallizes around it in a logical fashion. But if something in the core can't do it, it is usually much more complicated to build something around it, than just to tear everything down and start anew.

The main problems with the way this calendar worked was, that it's own somewhat crufty "Theme engine" was tightly coupled to the calendar. I didn't really like the view structure either and layouting it into a different looking view, was not very easy.

Unfortunately to reach this conclusion, I wasted quite a lot of time.

Bauernregel
  • "If third party software isn't doing what you want with some minor tweaks, write it yourself from scratch"

Use of UIAppearance

Probably the biggy. UIAppearance is noobshit and I fell for it. I would estimate, that because "skinning" and "theming" isn't very much pushed by Apple anymore, that this functionality will get a deprecation notice rather sooner than later. But theming was still big around iOS 5 and 6. As this app was a collaboration with another company, which is more graphically oriented, I was thinking that having theme support would be a good thing to share development resources.

Problems of UIAppearance

  • too slow during initial setup, if using XML (UISS) for deployment
  • too slow during actual view creation
  • too late in the view creation process
  • no support for CSS like "ids", only "class"

I have written some technical rants (#1 #2]) already, so no need for duplication. But every time it sickens me to look at the code that is still in the app that moves views quickly into and out of a UIWindow, just to let UIAppearance run, so that the view is fully set up for further processing.

I used the otherwise very fine project UISS Stylesheets for UIViews to setup the plethora of UIView appearance calls. Unfortunately the loading and parsing of that XML file, was just too slow for deployment use. Fortunately one can copy/paste the actual Objective-C UIAppearance calls from UISS into the project, obviating the need to link to UISS for the final product.

UISS itself was very helpful to get the layout right, since you don't have to recompile the project for every minor change.

Pretty much at the end of the project, I still got complaints that some operations felt too sluggish. It turned out, that views, that were appearing dynamically, were spending the majority of their sweet time in UIAppearance...

It took some time to get rid of UIAppearance in the affected UIView subclasses and hardcode the values instead.

Bauernregel
  • "UIAppearance, just say NO."

Getting lost in the layouting process with complex UIView subview topologies

Now this topic doesn't touch views layouted in xibs or storyboards. This is about compositing a complex user interface element out of a collection of views.

Layouting conceptually is a top-down and bottom-up problem. From the top, there is a view that has an area of 320x160 pixels. UIAppearance says, please inset these 320x160 pixels by 10 pixels all around, to make it look nicer and with a green background. So what is left is 300x140. Now -layoutSubviews runs recursively. In the bottom there is a view, that says. "I need to display this information in about 32x32 pixels minimally, although 48x48 pixels would be preferred, I can't deal with the 24x24 you're giving me." Can the view, that contains the bottom view, provide that much space, by squeezing another view ? Should the view size itself to the desired size and ask the superview to layout again ?

Don't overthink it, rather don't do it!

Apple now has a constraints system, available since iOS 7, that suggests that you should do something like this. To me it sounds like a path to misery -> HTML + CSS. It appears to be opaque, complicated and almost impossible to debug.

To bring myself into a mental state, where I had a sound perspective and grasp on how I wanted to do layouting took quite some time. It was hard for me let go and not to try get it "perfect", top/down and bottom/up.

Bauernregeln
  • "When you are creating a view manually you initialize it with initWithFrame:CGRectZero. It's not useful to think at this point of time about the frame of the view yet."
  • "In -layoutSubviews, just set the frames of your subviews. Resist the temptation to change anything else."
  • "View space is always propagated from the top down. If the view can't deal with the given space from the parent view, you're doing it wrong."
  • "If you are doing manual layouting, CGRectIntegral() is usually NOT what you want"
  • "Don't use UIAppearance to change implicitly or explicitly the frame size of a view"

Tried to use opaque Apple code to do something special

Gone are the days, where you could start with some opaque Apple code and then step by step mold it to your liking, with the use of categories, poseAs: and looking into undocumented private variables. Technically you can still do it, but Apple won't accept it in it's store. No sale.

This unfortunately means, that I will almost invariably hit a brick wall at some unforseeable point in time. For example, I spent way, way more time dicking around with UIScrollView than I eventually needed to code my own custom UIScrollView. The opacity of the iOS libraries means, that I always have to guess, how it's really implemented, guess how it could break in the next iOS version and also guess beforehand, if everything is exposed like I will eventually need it.

Bauernregeln
  • "Custom views subclass from UIView. Nothing else."
  • "Don't do nothing smart with stock views."

Used CoreData as if it is EOF

CoreData is like someone wrote EOF in Java and then ported it back to Objective-C. There are a few good ideas and simplifications, but in general it's a very brittle magic framework, where you don't want to code anything that "surprises" CoreData. And CoreData is easily surprised.

Subclassing CoreData classes or overriding CoreData accessors is a path to misery, where I am unfortunately still traveling on. I am not 100% sure, but I would probably have been better off, either just going sqlite-direct or to use a stripped down MulleEOF for Dienstag.

Bauernregel
  • "Do it like a brother, do it like a noob."

Where did I think different ?

Did not use ARC

I have to convert Dienstag to ARC one of these days and see how much "faster" it will perform. A year ago I made a little test with some code comparing different implementations of singletons, and compared the execution times. It was interesting, because "naive code" only suffered a factor 2 ARC penalty, whereas "clever code" suffered a factor 10 ARC penalty. So ARC seems to be a great programmer equalizer in that respect. I didn't investigate other "patterns", but I also continued not using ARC. Less magic, less pain.

Did not use PCH

In my opinion PCH makes me turn out sloppy code, that can't be easily moved to other projects as the header dependencies become unclear. My machine is fast enough to compile without PCH.

Did not use Key Value Observing

KVO to me is overly granular, and seems too much effort for too little result. KVO probably makes sense in conjunction with bindings, but they don't exists on iOS AFAIK. But I wouldn't use them either. Less opaque code, less pain. The way Dienstag is written, I didn't miss bindings or KVO at all.

Used blocks minimally

Now in an aesthetic sense, writing what amounts to a callback directly in the caller is appealing, if you can keep the code extremely small; Like one or two lines. The obscure semantics of blocks with regard to Objective-C objects (magic!), makes me avoid them as much as possible though. (When I was a total blocks noob, I used up one DTS incident on a blocks bug. I got some code critique and had to figure out the bug myself...)

Used multi-threading minimally

I try to make my code fast enough for single-threaded use. There is some network code, and some initialization code, which benefitted from multi-threading/queueing. I also would have liked to have CoreData run with a private shadow queue, but it turned out to take more effort, than I saw benefit coming out of it. I am sure it would have created a whole new slew of bugs.

Used ObjC 2.0 features sparingly

  • Writing a @property is quite a timesaver compared to writing accessors manually. I think it's more readable in the header also. I don't like it though, that I have no control over what the default accessor is really doing.

  • property syntax: You know view.alpha = 1.0 ? I don't write that I always write [view setAlpha:1.0]. Why ? Because view.alpha looks to me like a struct accessor or a union accessor. And I don't want to get rid of that.

  • @synchronized( whatever) {} is nicer to write, than all the old NSLock code. Although it seems a bit slower, for whatever reason.

  • @optional in @protocol is a very good addition to the language.


Was it fun ?

Was it enjoyable to write an iOS application ? All in all it's still better than writing Java or C# code. I wrote a little Android app and that was just horribly tedious. I hate Java. But coding to a simulator is also just not very exciting somehow. Coding to wildly different iOS versions, where you endlessly need to tweak layout changes is also very boring. There were times it felt more like web-coding than actual programming.

I still like ObjC and I love the combination of C-code with ObjC, which fortunately still is possible. But iOS has too many opaque frameworks with overlapping intents, full of magic and brittleness to be truely enjoyable. In general though, I favor the architecture of layering views in iOS more than the non-overlapping views in Mac OS X.

That's it. Did anyone make it till here ? :)