Developing iOS apps is fun and challenging. During this process, we sometimes make bad decisions and mistakes, that can have impact on the quality of the app (both technical and from the user’s perspective). Some of those pitfalls can cause crashes in our apps, making users angry. Others can make the maintenance of the app a nightmare, making developers angry. In this post, we will see some of the more common such errors, ranging from the most trivial ones that a careless developer can make, to the bigger impact ones, that a tech lead might not foresee and send the project to hell.
One of the most common bugs a junior developer can make is introduce memory leaks. Memory leak is a piece of memory which is not used anymore in your app, but it is not de-allocated. Having too much memory leaks can increase the memory usage of your app, causing the operating system to terminate it.
Since iOS apps now have automatic reference counting, the most common way you can introduce these leaks is through wrong references to the variables (strong vs. weak). The strong reference cycle between class instances is a well documented issue in Apple’s documentation and everyone should be aware of it.
*image taken from Apple’s documentation
What basically happens is that both the Person and Apartment hold strong references between each other and they can’t be released, leaking memory.
More advanced and sneaky error is memory leak with closures (or blocks in Objective-C). This happens if you have closure in a class instance. The body of the closure captures the instance if it accesses some property of that instance (self.someProperty). We fix this by using weak self in the closure. Here’s a good post on the topic.
When you try to fix leaks without deeply understanding ARC, you might as well introduce zombies. Zombies are deallocated objects which are still used in the app. When you try to access such object, the app immediately crashes.
The most common ways to access deallocated object are:
- improper use of weak references (when they should be strong)
- not clearing up some observation. For example, you subscribe to some notification through the NSNotificationCenter, but you don’t remove the subscription in the dealloc method. When that notification fires again, that object is deallocated.
Since zombies immediately crash your app, you should be very careful avoiding them. Apple’s Instruments app is a great tool to detect memory related issues, like zombies and leaks.
This is not iOS specific and it’s a general bad practice in software engineering. However, for many different reasons, such as not enough time, bad design, not adjusting to some changes in the project, the codebase might suffer from not respecting the DRY principle.
One classic such mistake is the following: You create two abstract view controllers, one inheriting from UITableViewController and one from UIViewController. Then, those two share some common functionality, like some gesture recognizers, keyboard notifications, UI (colors, texts). Then you start adding the same methods in both places and the two controllers grow with a lot of duplications. Then, you need to add a UICollectionViewController base class and copy everything there. When you need to change something, there is a lot of work for you.
Usually, I solve this by not using UITableViewController or UICollectionViewController at all. I tend to implement the required datasource and delegate methods instead. That gives me flexibility, avoiding unnecessary subclassing. Of course, you get some functionality for free in the UITable/CollectionViewControllers, but since customizing them can be a pain, I pass on that.
Adding up on the previous paragraph, unnecessary subclassing can introduce a lot of problems in your apps. You can easily fall into the trap of needing to inherit from more than one class to avoid duplication and then you start to ask yourself, why modern languages don’t allow multiple inheritance.
The answer is you should always prefer composition over inheritance. What this means is to have instances of other classes that implement the desired functionality, instead of subclassing them. The iOS SDK encourages you to do this, by enforcing the delegate pattern and protocols.
With Swift and its protocol oriented nature, you can avoid subclassing even more, by using types such as structs and enums, combined with protocols.
Inconsistent coding style
For some it might not be a big deal, but inconsistent coding style between team members is something that bothers a lot. The team should agree on certain coding conventions and follow them. If your team doesn’t have such conventions, you can always follow the ones from Apple.
This doesn’t apply to indentation only, but to general coding style. There should be short, concise, properly named methods, doing one thing only. The level of abstraction of a function should be respected. For more details about writing clean and maintainable code, pick up a copy of Uncle Bob’s Clean Code (if you haven’t read this classic yet).
Too much state
We need to check if that switch is on. We need to track whether that property is set. Let’s introduce some more booleans.
Flags everywhere. Too much state is one of the most common sources of bugs in software development. Why computers work properly again, after you’ve restarted them? Because the corrupted state is now lost and the machine starts with clean state. Having no state at all is impossible in apps, they would not do anything interesting without it.
However, we should look for ways to reduce state as much as possible. Boolean flags should be avoided when there is a better solution. Internal variables should be passed as arguments to a function, instead of directly being referenced in a method. Methods should be as stateless as possible – pure functions with no side effects. The benefits of functional programming should be used as much as possible, no matter what your app architecture is.
Where possible (though not always), you can use functional reactive programming, with data streams and bindings, which will greatly reduce state management in your app.
Breaking of patterns
When you’ve started a project, you (hopefully) agreed upon some general architecture and design patters that will be followed throughout the project. Usually, that’s MVC, MVVM, VIPER or something else. However, usually, again because of lack of time, bad software design decisions or not understanding the architecture, there are breaking of the established patterns.
Classical example is adding a button to a table view cell, which displays information about a person. When the button is clicked, we want to tell the controller which person was clicked. It’s easy for junior developers to store the model (the person object in our case) in the cell and then just send it to the delegate. However, model should not be stored in the view. Not only it breaks the MVC pattern, but it also introduces some unexpected behaviour, since cells are re-used and they must be as dumb as possible.
There are a lot of examples like this. The same happens with controllers and models. The data flow is not clear and you never know what comes from where. When you start breaking the pattern, you usually end up with spaghetti code all over the place and debugging looks more like a detective work.
Massive View Controllers
If you think this section is about criticising the MVC architectural pattern, you are wrong. I don’t think that the MVC pattern is bad for iOS apps. Check this great post regarding MVC. There are really complex apps written in MVC, which are not implemented in a bad way.
The pitfalls of MVC come when the implementation is not optimal. If you put everything in the controller, without using utility or service classes, of course your controllers would be massive. You can always create objects that implement, for example, UITableView datasource and delegate methods. MVC can easily deceive you to put everything in the controllers, but that’s the development team’s mistake, not MVC’s.
Going with the flow
Continuing on the MVC discussion, one common mistake is to go with the flow. If everyone talks how MVC stands for “Massive View Controllers” and how great MVVM or VIPER is, it’s easy to adopt something which is not suitable for your application and its business requirements.
Every architectural pattern has a reason why it exists and which problems it solves. Not understanding them and just adopting it, only because it’s trendy, is like playing a Russian roulette – you might get lucky, but you might also blow it up. So before adopting something in your project, you need to have clear understanding and arguments why you have chosen that particular architectural pattern.
No unit tests
Be a yardstick of quality. Some people aren’t used to an environment where excellence is expected – Steve Jobs.
Usually, teams have the lame excuse that they don’t have time for unit tests. Or that it was not agreed with the customer and therefore you are not paid to write tests. However, when the customer decides to give you the responsibility to develop their software, you have to obligation to provide the quality that is expected from you. At the end, it all depends on how much you care about software quality. For me, unit tests should be part of every task/feature and implicitly included in the estimations.
Tight dependencies on third-party frameworks
A good architect maximizes the number of decisions not made – Robert Martin.
Recently, I’ve read Uncle Bob’s Clean Architecture book, where I’ve found the great quote above. And that’s so true – you must not depend on a third party framework, deeply integrated in your solution. We know what happened with Parse and many other such frameworks. Your business logic should be isolated from implementation details such as which frameworks you use. It should be easy for you to throw away such framework and replace it with new one if needed. Using protocols/interfaces, dependency injection and other decoupling techniques helps a lot in that mission.
Also worth mentioning
- Force unwrapping optionals in Swift.
- Not having centralised place for style management (colours, fonts etc).
- Not using constants.
- Doing long operations on the main thread.
- Changing the user interface from threads other than the main.
- Not caring about security. Storing credentials in insecure storage, such as User defaults.
- Not having log levels for debug and release. It might happen that you log sensitive data goes in production.
These were some of the mistakes developers make while creating iOS apps. Of course, there are many more. What are your thoughts about the provided pitfalls? Do you have any other ones which are worth mentioning?