Common Mistakes Swift Developers Make Without Realizing

Having a background in Objective-C, I initially perceived Swift as a hindrance to my progress due to its strong typing, which often led to frustration.

In contrast to Objective-C, Swift enforces numerous requirements during compilation. Concepts loosely implemented in Objective-C, such as the id type and implicit conversions, are absent in Swift. For instance, adding an Int and a Double necessitates explicit conversion to a common type.

Optionals, while conceptually straightforward, are a fundamental aspect of the language that takes time to master. Initially, one might be inclined to force-unwrap everything, ultimately resulting in crashes. As familiarity with Swift grows, the scarcity of runtime errors becomes appreciated, as many mistakes are caught during compilation.

Swift programmers often have substantial prior experience with Objective-C, leading them to apply familiar practices that can result in errors.

This article aims to highlight and provide solutions for common pitfalls encountered by Swift developers.

Make no mistake - Objective-C best practices are not Swift best practices.

1. Force-Unwrapping Optionals

Optional type variables (e.g., String?) may or may not contain a value. When empty, they are equivalent to nil. Accessing the value of an optional requires unwrapping, achievable in two ways.

Firstly, through optional binding using if let or guard let:

1
2
3
4
5
6
7
8
9
  var optionalString: String?
  //...
  if let s = optionalString {
      // if optionalString is not nil, the test evaluates to
      // true and s now contains the value of optionalString
  }
  else {
      // otherwise optionalString is nil and the if condition evaluates to false
  }

Secondly, by force-unwrapping using the ! operator or implicitly unwrapped optional types (e.g., String!). Force-unwrapping a nil optional or accessing the value of an implicitly unwrapped nil optional leads to a runtime error, halting the application.

Variables not initialized during class/struct initialization must be declared as optionals. Assuming non-nullability in specific code segments might tempt developers to force-unwrap or use implicitly unwrapped optionals for convenience, but caution is advised.

This mirrors working with IBOutlets, variables referencing objects in interface files. Uninitialized during the parent object’s initialization (typically a view controller or custom UIView), they are guaranteed to be non-nil by viewDidLoad (view controllers) or awakeFromNib (views), allowing safe access.

Generally, it’s best to avoid force-unwrapping and implicitly unwrapped optionals. Always consider potential nil values and handle them gracefully via optional binding, nil checks before force-unwrapping, or careful access in the case of implicitly unwrapped optionals.

2. Overlooking Strong Reference Cycle Pitfalls

Strong reference cycles occur when two objects maintain strong references to each other. Familiar to Objective-C developers, this issue requires careful reference management in Swift as well. The Swift documentation offers a comprehensive section dedicated to this topic.

Closures demand particular attention. By default, they capture strong references to all referenced objects. If any of these objects strongly reference the closure, a cycle emerges. Capture lists are crucial for managing reference capture.

If a captured instance might be deallocated before the closure executes, capture it as a weak reference, which is optional due to potential nil values. If the instance is guaranteed to persist throughout the closure’s lifetime, capture it as an unowned reference, a non-optional type allowing direct access without unwrapping.

Consider the following Xcode Playground example: a Container class with an array and an optional closure invoked upon array changes (using property observers). The Whatever class, containing a Container instance, assigns a closure referencing self to arrayDidChange within its initializer, creating a strong relationship between the Whatever instance and the closure.

 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
28
29
30
31
32
33
34
    struct Container<T> {
        var array: [T] = [] {
            didSet {
                arrayDidChange?(array: array)
            }
        }

        var arrayDidChange: ((array: [T]) -> Void)?
    }

    class Whatever {
        var container: Container<String>

        init() {
            container = Container<String>()


            container.arrayDidChange = { array in
                self.f(array)
            }
        }

        deinit {
            print("deinit whatever")
        }

        func f(s: [String]) {
            print(s)
        }
    }

    var w: Whatever! = Whatever()
    // ...
    w = nil

Running this code reveals that deinit whatever is never printed, indicating the w instance remains in memory. Utilizing a capture list to avoid strongly capturing self rectifies this:

 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
28
29
30
31
32
33
34
    struct Container<T> {
        var array: [T] = [] {
            didSet {
                arrayDidChange?(array: array)
            }
        }

        var arrayDidChange: ((array: [T]) -> Void)?
    }


    class Whatever {
        var container: Container<String>

        init() {
            container = Container<String>()

            container.arrayDidChange = { [unowned self] array in
                self.f(array)
            }
        }

        deinit {
            print("deinit whatever")
        }

        func f(s: [String]) {
            print(s)
        }
    }

    var w: Whatever! = Whatever()
    // ...
    w = nil

Here, unowned is appropriate because self will always be valid during the closure’s lifetime.

Employing capture lists to prevent reference cycles is generally recommended, promoting reduced memory leaks and safer code.

3. Excessive Use of self

Unlike Objective-C, Swift doesn’t mandate self for accessing class or struct properties within methods, except within closures for capturing self. While not erroneous, using self unnecessarily adds verbosity. Consistency and code conciseness are key.

4. Misunderstanding Type Categories

Swift employs value types and reference types, each exhibiting distinct instance behavior. Unfamiliarity with these categories can lead to inaccurate code behavior assumptions.

In most object-oriented languages, passing around a class instance creates references to the same data, ensuring changes are reflected universally. These are reference types, declared as class in Swift.

Value types, declared with struct or enum, are copied during assignment or parameter passing. Modifying a copy doesn’t affect the original, as value types are immutable. Assigning new values to properties of value type instances like CGPoint or CGSize creates new instances with the changes. This explains property observer functionality for arrays (as in the Container example). Changes create new arrays assigned to the property, triggering didSet.

Confusing reference and value types can lead to unexpected outcomes.

5. Underutilizing Enums

Enums are often perceived as basic C enums: integer-based constant lists. Swift empowers enums with greater capabilities, such as attaching values to cases and defining methods and computed properties, enriching each case.

The official Swift documentation provides an accessible documentation on enums and practical error handling documentation. An in-depth exploration of enums in Swift explores the full spectrum of enum capabilities.

6. Neglecting Functional Features

The Swift Standard Library offers functional programming staples like map, reduce, and filter, facilitating concise and expressive code.

Consider calculating a table view’s height given a UITableViewCell subclass:

1
2
3
4
5
6
  class CustomCell: UITableViewCell {
      // Sets up the cell with the given model object (to be used in tableView:cellForRowAtIndexPath:)
      func configureWithModel(model: Model)
      // Returns the height of a cell for the given model object (to be used in tableView:heightForRowAtIndexPath:)
      class func heightForModel(model: Model) -> CGFloat
  }

With a modelArray containing model instances, the table view height can be computed in one line:

1
  let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +)

map generates a CGFloat array with each cell’s height, while reduce sums them.

Removing elements from an array can be streamlined. Instead of:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"]

  func isSupercar(s: String) -> Bool {
      return s.characters.count > 7
  }

  for s in supercars {
      if !isSupercar(s), let i = supercars.indexOf(s) {
          supercars.removeAtIndex(i)
      }
  }

which inefficiently calls indexOf repeatedly, consider:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
  var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"]

  func isSupercar(s: String) -> Bool {
      return s.characters.count > 7
  }

  for (i, s) in supercars.enumerate().reverse() { // reverse to remove from end to beginning
      if !isSupercar(s) {
          supercars.removeAtIndex(i)
      }
  }

Further improvement is achieved using filter:

1
2
3
4
5
6
7
  var supercars = ["Lamborghini", "Bugatti", "AMG", "Alfa Romeo", "Koenigsegg", "Porsche", "Ferrari", "McLaren", "Abarth", "Morgan", "Caterham", "Rolls Royce", "Audi"]

  func isSupercar(s: String) -> Bool {
      return s.characters.count > 7
  }

  supercars = supercars.filter(isSupercar)

Removing subviews from a UIView based on criteria like frame intersection with a rectangle, traditionally done with:

1
2
3
4
5
  for v in view.subviews {
    if CGRectIntersectsRect(v.frame, rect) {
      v.removeFromSuperview()
    }
  }

can be condensed using filter:

1
  view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() }

Exercise caution, as excessive chaining of these methods, while powerful, can hinder readability.

7. Avoiding Protocol-Oriented Programming

Swift is touted as the first protocol-oriented programming language, as emphasized in the WWDC Protocol-Oriented Programming in Swift session. This paradigm encourages modeling programs around protocols, extending types by conforming to and extending protocols.

For instance, with a Shape protocol, CollectionType (conformed by Array, Set, Dictionary) can be extended with a method calculating total area accounting for intersections:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  protocol Shape {
      var area: Float { get }
      func intersect(shape: Shape) -> Shape?
  }

  extension CollectionType where Generator.Element: Shape {
      func totalArea() -> Float {
          let area = self.reduce(0) { (a: Float, e: Shape) -> Float in
              return a + e.area
          }

          return area - intersectionArea()
      }

      func intersectionArea() -> Float {
          /*___*/
      }
  }

The where Generator.Element: Shape constraint limits method availability to types conforming to CollectionType with elements conforming to Shape. This allows invocation on Array<Shape> but not Array<String>. If Polygon conforms to Shape, the methods become available for Array<Polygon> as well.

Protocol extensions enable default method implementations, automatically available to conforming types (classes, structs, or enums) without modification. This is prevalent in the Swift Standard Library. For example, map and reduce are defined as CollectionType extensions, shared by Array and Dictionary without additional code.

This behavior resembles mixins in languages like Ruby and Python. Conforming to a protocol with default implementations adds functionality to types.

While protocol-oriented programming might initially appear awkward or impractical, This post provides insights into its real-world applications.

Swift: A Language to Be Reckoned With

Swift’s initial reception was met with skepticism, perceived as a replacement for Objective-C with a simplistic or non-programmer-oriented language. However, Swift has established itself as a robust and powerful language that makes programming enjoyable. Its strong typing minimizes errors, making it difficult to compile a list of potential pitfalls.

Returning to Objective-C after using Swift highlights the differences. Swift’s elegant features are missed, replaced by verbose Objective-C code, and runtime errors Swift would have caught during compilation are encountered. Swift is a significant advancement for Apple developers, with further growth anticipated as the language matures.

Licensed under CC BY-NC-SA 4.0