8 common iOS app development mistakes to avoid

9 min read
March 16, 2023

iOS apps are known for being exceptionally secure, thanks to Apple’s closed ecosystem, which allows it to control device and app quality tightly.

That ecosystem also provides developers with many tools and resources to help them create stable apps with great user experiences.

However, it doesn’t mean that you can let your guard down when creating one.

Despite iOS’s strict standards, making critical mistakes in your code is still possible, unless you’re careful.

Here are eight development mistakes that you should take note of and avoid.

Not understanding asynchronous processes

A common mistake, especially among new programmers, is mixing asynchronous and synchronous operations improperly in code.

Briefly, synchronous code runs sequentially—that is, the runtime environment waits for the current line to execute before moving to the next one.

An asynchronous operation doesn’t do this. It allows the code to run in the background and moves on to the next line.

You’ll see this most commonly with operations that retrieve data from a file or database.


Source: Research Gate

Due to minimal wait times, asynchronous operations are used because they execute much faster than synchronous code.

However, they could also introduce unexpected results when not approached properly.

One common problem is when you gather data asynchronously that will be used immediately in the next line of code.

The issue is that the program might execute that code before it gets the data, leading to an error.

Here’s an example:

DECODE sceenshot 1

Source: DECODE

Here, the ‘fetchDataFromAPI’ function attempts to get data asynchronously from a JSON object using the ‘dataTask’ function.

But because the next lines of code are executed immediately before the actual data is retrieved, the method returns an empty string.

To fix these issues, you can use a completion handler. This is a function that executes only after an asynchronous task is completed.

Here’s how the function will look like if we use a completion handler:

DECODE sceenshot

Source: DECODE

Here, the result array is passed to the completion handler, which passes the data when the asynchronous operation completes.

Completion handlers are just one way to fix asynchronous code issues. Sometimes, a simple rearrangement of the code will do the trick.

Running UI-related code outside the main queue

A good programming practice is only to execute UI-related code, such as updating the user interface and handling touch events inside the main IOS queue.

The main reason is that UIKit—the library that handles iOS UI components—isn’t thread-safe.

Executing any of its functions on a background thread can introduce unexpected behavior and even cause a crash.

main thread

Source: Fluffy

To ensure that all UI updates are executed on the main queue, it’s best to use the ‘DispatchQueue’ function.

Here’s an example of its usage:

DispatchQueue function

Source: DECODE

Here, you tell Swift to explicitly execute the myLabel.text update to the main queue.

This is a good practice to get in even when using libraries, such as Alamofire or AFNetworking, that call a completion block on the main queue because you can’t rely on this happening all the time.

Misunderstanding concurrency and multithreading

Concurrency is a programming capability that allows you to execute multiple codes simultaneously by placing them on separate threads.

Think of a thread as a separate process, each executing some part of your program in the background.

In short, concurrency is a powerful tool as it allows your apps to run faster and more efficiently.

For instance, you can assign background tasks like retrieving data from a database to another thread, so that the app doesn’t freeze on the main thread until the background task is completed.

Using concurrency and multithreading in iOS development is powerful but also dangerous if misunderstood.

One common pitfall is when two concurrent processes try to access the same resource. Here’s an example:

DispatchQueue label

Source: DECODE

In this code, one queue tries to add a new item to the myArray array while the other attempts to read it.

This leads to a race condition, a situation where multiple changes are attempted on the same object, thus producing unexpected results. At worst, it could corrupt the data.

To solve this, the programmer should synchronize access to a resource via the sync method. Here’s the modified code:


Source: DECODE

In this scenario, the array manipulations are done within a serial queue, thus ensuring that only one queue can access it at any given time.

Not knowing the drawbacks of mutable objects

In programming, mutable objects are those that can be changed or modified after they’re created.

For example, they give the developers added flexibility by transforming a class from one state to another.

The problem is that mutable objects can be unpredictable during execution. This makes your code difficult to read, test, debug, and maintain.

More problematic is that mutable objects are not thread-safe, which means asynchronous processes can’t use synchronization methods to lock exclusive access to them.

This leads to the race condition problem we discussed in the concurrency section.

A good practice is to use immutable objects as much as possible, since that makes your code much safer and more manageable.

If you want to modify something in a class, creating an instance of it is better.

One exception is if you have an exceptionally large object. Creating instances of it might cause your app to slow down. In this case, it might be better to make it mutable.

In short, knowing when to use mutable and immutable objects, and using them prudently, is crucial.

Using storyboards instead of XIBs

Apple gives you multiple ways to design your app UI in XCode, the official iOS IDE. One that’s recommended for most beginners is the storyboard.


Source: Matteo Manferdini

In storyboard mode, you create app screens and then connect them by dragging and dropping. It’s a visually intuitive way to assemble your app UI, which is why it’s so accessible.

However, storyboards have several disadvantages that make them unsuitable for larger projects.

One of the biggest is that storyboards are ill-equipped to handle complex navigation flows, such as multi-step forms, wizard-style interfaces, or custom transitions.

You can’t customize storyboards as much nor reuse elements. It thus forces you into a generic, non-modular design.

In those cases, the better alternative is to use XIBs or XML Interface Builders:

XML Interface Builder

Source: Medium

An XIB represents only a single view or user interface element rather than an entire flow or series of screens.

It contains all the necessary information to configure it at runtime, including layout, content, and connections.

The biggest advantage of XIBs is that you can customize them more freely, including adding features through code.

Each element is also reusable, allowing you to achieve modular UI designs.

Thus, XIBs are recommended for creating larger, more complex apps with more seamless navigation.

Relying on hardcoded values

A common and potentially problematic habit is to use exact numbers or values in your code.

Here’s an example of what we mean:

DECODE screenshot 2

Source: DECODE

The issue here is that it’s not clear what the number 756 represents. Why is it a specific number, and what’s the logic behind it?

Furthermore, you don’t even know if 756 is in minutes, hours, or seconds.

As you can see, hardcoded values make your code difficult to comprehend.

Another issue is if you need to reuse that value multiple times in the code. In that situation, you would need to retype it each time, increasing the chances of making an error.

The better approach is to define the value as a constant using the define statement.

idle time in mins

Source: DECODE

The code now makes sense since you know that 756 means the number of idle minutes.

Plus, if you need to change the idle time to another value, you can change it in your define declaration, and it will reflect in all instances.

Using the default keyword in a switch statement

In programming, a switch statement is a conditional statement that executes code based on a series of conditions.

Here’s an example:

switch statement

Source: DECODE

In the above code, “print(“Excellent!”)” will execute since the grade variable has a value of “A.”

The last case, default, is a catch-all when none of the options are applicable. However, sometimes, using it can lead to unexpected results.

For example, if we were to introduce a new grade level, “A+” and forgot to revise the switch statement, the program would return “Invalid grade” even if the “A+” value is legitimate.

To get around this, a good practice is to list the switch options in an enum object instead of the default keyword. That way, oversights can be identified and fixed at compile time.

Not writing secure code

Security should be a top priority during coding. Unfortunately, this might not be the case, as developers might be focused more on making their code work by whatever means necessary.

Unfortunately, insecure code can introduce vulnerabilities that hackers can exploit.

For examples of these threats, check out OWASP’s (Open Web Application Security Project) Top Vulnerabilities for 2022:

OWASP Vulnerabilities

Source: Spiceworks

The thing is, most of these threats can be tackled by following Apple’s secure coding guidelines.

The document explores common vulnerabilities, such as buffer overflows, unvalidated inputs, and insecure file operations.

You’ll then discover techniques and best practices to help you avoid them.

The best way to avoid these mistakes

We hope that this overview of eight of the most critical mistakes in iOS development proves useful to you and the other developers from your team.

There are more useful insights that you can find on our website.

Want to learn more about iOS development? We’ve written an iOS roadmap that can give you a good grasp of the fundamentals.

Or, you can discover some iOS development best practices in this article.

Enjoy reading them!

Written by

Toni Vujevic

React Native Team Lead

Skilled in React Native, iOS and backend, Toni has a demonstrated knowledge of the information technology and services industry, with plenty of hands-on experience to back it up. He’s also an experienced Cloud engineer in Amazon Web Services (AWS), passionate about leveraging cloud technologies to improve the agility and efficiency of businesses. One of Toni’s most special traits is his talent for online shopping. In fact, our delivery guy is convinced that ‘Toni Vujević’ is a pseudonym for all DECODErs.

Related articles