10 Mistakes iOS Developers Make Without Realizing

The worst thing that can happen after submitting a buggy app to the App Store isn’t rejection, but acceptance. Once those one-star reviews start pouring in, recovering your app’s reputation becomes a monumental task, costing companies significant financial losses and developers their jobs.

With its impressive adoption rate exceeding 85% of users on the latest version, iOS holds the position of the world’s second-largest mobile operating system. This massive user base, known for its high engagement, also sets a high bar for app quality. Any flaw in your app or update will be noticed and critiqued.

The demand for iOS developers is soaring, prompting many engineers to pivot to mobile development, as evidenced by over 1,000 new app submissions to Apple daily. However, true iOS expertise goes beyond mere coding proficiency. Let’s delve into 10 frequent pitfalls that snare iOS developers and explore effective strategies to circumvent them.

85% of iOS users use the latest OS version. That means they expect your app or update to be flawless.

Common Mistake No. 1: Misinterpreting Asynchronous Processes

A frequent stumbling block for novice programmers is the mishandling of asynchronous code. Let’s illustrate this with a common scenario: A user opens a screen displaying a table view. The app fetches data from a server and populates the table view with it. A more formal representation of this process:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@property (nonatomic, strong) NSArray *dataFromServer;
- (void)viewDidLoad {
	__weak __typeof(self) weakSelf = self;
	[[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){
		weakSelf.dataFromServer = newData; 	// 1
	}];
	[self.tableView reloadData];			// 2
}
// and other data source delegate methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
	return self.dataFromServer.count;
}

At first sight, the code appears sound: Data is retrieved from the server, followed by UI updates. The problem lies in the asynchronous nature of data fetching, which doesn’t provide new data instantly. As a result, reloadData is triggered before receiving the data. To rectify this, line #2 should be moved within the block, right after line #1.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@property (nonatomic, strong) NSArray *dataFromServer;
- (void)viewDidLoad {
	__weak __typeof(self) weakSelf = self;
	[[ApiManager shared] latestDataWithCompletionBlock:^(NSArray *newData, NSError *error){
		weakSelf.dataFromServer = newData; 	// 1
		[weakSelf.tableView reloadData];	// 2
	}];
}
// and other data source delegate methods
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
	return self.dataFromServer.count;
}

However, this code might still exhibit unexpected behavior under certain circumstances, leading us to…

Imagine using the corrected code from the previous example, yet the table view remains stubbornly resistant to updating with new data, even after the asynchronous process concludes successfully. What could be amiss in such seemingly straightforward code? A breakpoint set inside the block could reveal the queue where it’s being called. The issue often arises from the call not residing in the main queue, the designated realm for all UI-related code execution.

Popular libraries like Alamofire, AFNetworking, and Haneke are engineered to execute completionBlock on the main queue after an asynchronous task. However, relying solely on this assumption can be risky, and dispatching your code to the correct queue can easily slip your mind.

To guarantee that your UI-related code operates on the main queue, remember to dispatch it accordingly:

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{
    [self.tableView reloadData];
});

Common Mistake No. 3: Misunderstanding Concurrency and Multithreading

Concurrency is akin to a double-edged sword: Extremely powerful when wielded correctly, but fraught with potential hazards if mishandled due to inexperience or carelessness.

While some developers attempt to steer clear of concurrency, it’s an almost unavoidable aspect of building robust applications, offering substantial benefits such as:

  • Fluid User Experience: Offloading tasks like web service calls (e.g., complex calculations or database reads) from the main queue prevents app freezing and unresponsiveness, ensuring a smoother user experience and avoiding potential app termination by iOS.
  • Leveraging Multi-Core Architectures: Modern iOS devices boast multi-core processors. Concurrency allows harnessing this power by executing tasks in parallel, significantly boosting app speed and responsiveness.

However, these advantages are accompanied by increased complexity and the risk of introducing subtle bugs, like race conditions that are notoriously difficult to reproduce.

Let’s examine real-world examples (some code omitted for brevity).

Case 1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
final class SpinLock {
    private var lock = OS_SPINLOCK_INIT

    func withLock<Return>(@noescape body: () -> Return) -> Return {
        OSSpinLockLock(&lock)
        defer { OSSpinLockUnlock(&lock) }
        return body()
    }
}

class ThreadSafeVar<Value> {

    private let lock: ReadWriteLock
    private var _value: Value
    var value: Value {
        get {
            return lock.withReadLock {
                return _value
            }
        }
        set {
            lock.withWriteLock {
                _value = newValue
            }
        }
    }
}

The multithreaded code:

1
2
3
4
5
6
7
8
9
let counter = ThreadSafeVar<Int>(value: 0)

// this code might be called from several threads 

counter.value += 1

if (counter.value == someValue) {
    // do something
}

At first glance, the code appears synchronized and should function as expected, given that ThreadSaveVar supposedly ensures thread safety for counter. Sadly, this isn’t the case. Two threads might simultaneously reach the increment line, rendering counter.value == someValue perpetually false. A workaround involves creating a ThreadSafeCounter that returns its value after incrementation:

1
2
3
4
5
6
7
8
class ThreadSafeCounter {
    
    private var value: Int32 = 0
    
    func increment() -> Int {
        return Int(OSAtomicIncrement32(&value))
    }
}

Case 2

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct SynchronizedDataArray {
    
    private let synchronizationQueue = dispatch_queue_create("queue_name", nil)
    private var _data = [DataType]()
    var data: [DataType] {
        var dataInternal = [DataType]()
        dispatch_sync(self.synchronizationQueue) {
            dataInternal = self._data
        }
        
        return dataInternal
    }

    mutating func append(item: DataType) {
        appendItems([item])
    }
    
    mutating func appendItems(items: [DataType]) {
        dispatch_barrier_sync(synchronizationQueue) {
            self._data += items
        }
    }
}

In this scenario, dispatch_barrier_sync was employed to synchronize array access – a common pattern for this purpose. However, the code overlooks the fact that structs create copies when appending items, resulting in a new synchronization queue for each append operation.

While seemingly correct initially, this code might not behave as anticipated and requires substantial effort to test and debug. Despite these challenges, mastering concurrency can dramatically enhance your app’s speed and responsiveness.

Common Mistake No. 4: Failing to Grasp the Dangers of Mutable Objects

Swift, with its emphasis on value types, helps mitigate errors related to mutable objects. However, many developers still work with Objective-C. Mutable objects are potential landmines, capable of causing elusive problems. While returning immutable objects from functions is a well-known principle, the underlying rationale often remains unclear. Let’s analyze the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Box.h
@interface Box: NSObject
@property (nonatomic, readonly, strong) NSArray <Box *> *boxes;
@end

// Box.m
@interface Box()
@property (nonatomic, strong) NSMutableArray <Box *> *m_boxes;
- (void)addBox:(Box *)box;
@end

@implementation Box
- (instancetype)init {
    self = [super init];
    if (self) {
        _m_boxes = [NSMutableArray array];
    }
    return self;
}
- (void)addBox:(Box *)box {
    [self.m_boxes addObject:box];
}
- (NSArray *)boxes {
    return self.m_boxes;
}
@end

This code functions correctly because NSMutableArray inherits from NSArray. So where does the potential for trouble lie?

The most straightforward issue arises when another developer modifies the returned array:

1
2
3
4
NSArray<Box *> *childBoxes = [box boxes];
if ([childBoxes isKindOfClass:[NSMutableArray class]]) {
	// add more boxes to childBoxes
}

This action disrupts the class’s internal state, creating a code smell and leaving the responsibility of fixing it on the developer who introduced the modification.

However, a more insidious problem showcases genuinely unexpected behavior:

1
2
3
4
5
Box *box = [[Box alloc] init];
NSArray<Box *> *childBoxes = [box boxes];

[box addBox:[[Box alloc] init]];
NSArray<Box *> *newChildBoxes = [box boxes];

The expectation is that [newChildBoxes count] > [childBoxes count]. What if it’s not? This points to a poorly designed class that mutates an already returned value. If you believe this inequality should always hold true, consider experimenting with UIView and [view subviews].

Fortunately, a simple fix exists by rewriting the getter from the first example:

1
2
3
- (NSArray *)boxes {
    return [self.m_boxes copy];
}

Common Mistake No. 5: Misunderstanding the Internal Workings of iOS NSDictionary

Developers working with custom classes and NSDictionary might have encountered the requirement for their classes to conform to NSCopying when used as dictionary keys. The reason behind Apple’s decision to impose this restriction often remains unexplored. Why copy the key instead of using the original object directly?

The key to unraveling this mystery lies in understanding NSDictionary’s internal mechanism. In essence, it’s a hash table. Let’s break down the high-level process of adding an object with a specific key (table resizing and performance optimizations are omitted for simplicity):

Step 1: Calculate hash(Key). Step 2: Based on the hash value, determine the object’s storage location. Typically, this involves calculating the modulus of the hash value with the dictionary’s length. The resulting index is then used to store the Key/Value pair. Step 3: If the location is empty, create a linked list and store the record (object and key). Otherwise, append the record to the existing linked list at that location.

Fetching a record from the dictionary follows a similar process:

Step 1: Calculate hash(Key). Step 2: Search for the Key using its hash value. If no data is found, return nil. Step 3: If a linked list exists at the calculated location, iterate through the objects until [storedkey isEqual:Key] returns true.

With this understanding, two critical conclusions emerge:

  1. Any change in the key’s hash necessitates moving the record to a different linked list.
  2. Keys must be unique.

Let’s illustrate these points with a simple class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface Person
@property NSMutableString *name;
@end

@implementation Person

- (BOOL)isEqual:(id)object {
  if (self == object) {
    return YES;
  }

  if (![object isKindOfClass:[Person class]]) {
    return NO;
  }

  return [self.name isEqualToSting:((Person *)object).name];
}

- (NSUInteger)hash {
  return [self.name hash];
}

@end

Now, envision a scenario where NSDictionary doesn’t copy keys:

1
2
3
4
5
NSMutableDictionary *gotCharactersRating = [[NSMutableDictionary alloc] init];
Person *p = [[Person alloc] init];
p.name = @"Job Snow";

gotCharactersRating[p] = @10;

Oops! A typo has slipped in. Let’s correct it:

1
p.name = @"Jon Snow";

What impact does this have on our dictionary? Mutating the name results in a different hash value. Consequently, our object is misplaced (still associated with the old hash, as the dictionary remains oblivious to the data change). Determining the correct hash for looking up data in the dictionary becomes ambiguous.

The situation could worsen if our dictionary already contained “Jon Snow” with a rating of 5. This mutation would lead to two different values for the same key.

As evident, mutable keys in NSDictionary open a Pandora’s box of potential problems. The best practice to avert such issues is to copy the object before storing it and mark properties as copy. This practice also contributes to maintaining class consistency.

Common Mistake No. 6: Opting for Storyboards Over XIBs

Following Apple’s guidance, many new iOS developers gravitate towards storyboards as the default choice for UI design. However, this approach comes with a slew of drawbacks and only a handful of (debatable) advantages.

Storyboard drawbacks include:

  1. Collaboration Challenges: Storyboards are notoriously difficult to modify concurrently by multiple team members. While using multiple storyboards is technically possible, its only real benefit lies in enabling segues between controllers within the same storyboard.
  2. String-Based Identifiers: Storyboards rely on strings for controller and segue names, forcing developers to either re-enter these strings throughout their code (increasing the risk of errors) or maintain an unwieldy list of storyboard constants. While SBConstants can offer some relief, renaming elements within a storyboard remains cumbersome.
  3. Hindered Modularity: Storyboards discourage modular design by providing little incentive to create reusable views. This might suffice for MVPs or rapid UI prototyping, but real-world applications often require reusing the same view across multiple screens, a task that storyboards don’t facilitate well.

Storyboard (debatable) advantages:

  1. Visualizing Navigation Flow: Storyboards offer a visual overview of the app’s navigation flow. However, this becomes unwieldy with complex applications involving numerous controllers and intricate connections, resembling a tangled web rather than providing a clear high-level understanding.
  2. Static Tables: This represents the most compelling argument for storyboards. However, in practice, most static tables evolve into dynamic ones as the app matures, a scenario where XIBs offer greater flexibility.

Common Mistake No. 7: Confusing Object and Pointer Comparison

When comparing objects, it’s crucial to differentiate between pointer equality and object equality.

Pointer equality occurs when two pointers reference the same memory location, indicating they point to the same object. In Objective-C, the == operator checks for pointer equality. Object equality, on the other hand, signifies that two objects represent the same logical entity, like two instances representing the same user from a database. Objective-C uses isEqual or, preferably, type-specific methods like isEqualToString and isEqualToDate to compare objects.

Consider the following code:

1
2
3
4
5
6
7
NSString *a = @"a";                         // 1
NSString *b = @"a";                         // 2
if (a == b) {                               // 3
    NSLog(@"%@ is equal to %@", a, b);
} else {
    NSLog(@"%@ is NOT equal to %@", a, b);
}

Running this code outputs “a is equal to b” because both a and b point to the same object in memory.

However, modifying line 2 to:

1
NSString *b = [[@"a" mutableCopy] copy];

results in “a is NOT equal to b”. Even though both objects hold the same values, they now reside at different memory locations, hence their pointers differ.

This pitfall can be avoided by using isEqual or its type-specific counterparts. In our example, replacing line 3 with:

1
if ([a isEqual:b]) { 

ensures accurate comparison regardless of the objects’ memory locations.

Common Mistake No. 8: Relying on Hardcoded Values

Hardcoded values introduce two primary problems:

  1. Lack of Clarity: Hardcoded values often lack context, making their purpose obscure.
  2. Code Duplication: Using the same hardcoded value in multiple places necessitates re-entering it (or copying and pasting), increasing the potential for errors and inconsistencies.

Consider this example:

1
2
3
4
5
6
7
if ([[NSDate date] timeIntervalSinceDate:self.lastAppLaunch] < 172800) {
    // do something
}
or
    [self.tableView registerNib:nib forCellReuseIdentifier:@"SimpleCell"];
    ...
    [self.tableView dequeueReusableCellWithIdentifier:@"SimpleCell"];

The meaning of 172800 remains unclear at first glance. Deducing that it represents the number of seconds in two days (24 hours/day * 60 minutes/hour * 60 seconds/minute = 86400 seconds/day, multiplied by 2) requires mental gymnastics.

Using #define statements can improve readability and maintainability:

1
2
#define SECONDS_PER_DAY 86400
#define SIMPLE_CELL_IDENTIFIER @"SimpleCell"

The preprocessor replaces occurrences of SECONDS_IN_TWO_DAYS with its defined value (172800) during compilation. While this enhances readability, a crucial aspect remains unaddressed.

Consider this code snippet:

1
2
3
#define X = 3 
...
CGFloat y = X / 2;  

What would be the value of y after execution? Contrary to the intuitive answer (1.5), y will be 1. The reason lies in #define’s lack of type information. In this case, the division of two integers (3 and 2) yields another integer (1), which is then cast to a Float.

This issue can be circumvented by utilizing typed constants:

1
2
3
static const CGFloat X = 3;
...
CGFloat y = X / 2;  // y will now equal 1.5, as expected 

Common Mistake No. 9: Overusing the Default Keyword in Switch Statements

While the default keyword in switch statements can be useful, its overuse can lead to bugs and unforeseen consequences. Consider this Objective-C code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
typedef NS_ENUM(NSUInteger, UserType) {
    UserTypeAdmin,
    UserTypeRegular
};

- (BOOL)canEditUserWithType:(UserType)userType {
    
    switch (userType) {
        case UserTypeAdmin:
            return YES;
        default:
            return NO;
    }
    
}

The Swift equivalent:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
enum UserType {
    case Admin, Regular
}

func canEditUserWithType(type: UserType) -> Bool {
    switch(type) {
        case .Admin: return true
        default: return false
    }
}

This code functions as intended, restricting editing permissions to admin users. However, introducing a new user type, “manager,” with similar editing rights, requires updating this switch statement. Failure to do so results in code that compiles without errors but fails to enforce the intended logic.

Using enum values instead of relying on default from the outset would have caught this oversight during compilation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
typedef NS_ENUM(NSUInteger, UserType) {
    UserTypeAdmin,
    UserTypeRegular,
    UserTypeManager
};

- (BOOL)canEditUserWithType:(UserType)userType {
    
    switch (userType) {
        case UserTypeAdmin:
        case UserTypeManager:
            return YES;
        case UserTypeRegular:
            return NO;
    }
    
}

The Swift equivalent:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
enum UserType {
    case Admin, Regular, Manager
}

func canEditUserWithType(type: UserType) -> Bool {
    switch(type) {
        case .Manager: fallthrough
        case .Admin: return true
        case .Regular: return false
    }
}

Common Mistake No. 10: Employing NSLog for Logging

Despite its widespread use for logging in iOS apps, NSLog often proves to be a poor choice. Examining Apple’s documentation for NSLog function description reveals its simplicity:

1
void NSLog(NSString *format, ...);

While seemingly harmless, NSLog displays debug messages in the Xcode organizer when a device is connected. This alone should discourage its use for logging, as it risks exposing sensitive internal data and appears unprofessional.

A more robust approach involves replacing NSLog with configurable CocoaLumberjack or dedicated other logging framework.

Wrap Up

iOS, with its potent capabilities and rapid evolution, offers a fertile ground for innovation. Apple continuously pushes the boundaries by introducing new hardware, enhancing iOS features, and expanding the Swift language.

Honing your Objective-C and Swift skills not only elevates you as an iOS developer but also opens doors to exciting projects at the forefront of technology.

Licensed under CC BY-NC-SA 4.0