+1

The Mistakes Most Swift Developers Don't Know They're Making

Coming from an Objective-C background, in the beginning, I felt like Swift was holding me back. Swift was not allowing me to make progress because of its strongly typed nature, which used to be infuriating at times.

Unlike Objective-C, Swift enforces many requirements at the compile time. Things that are relaxed in Objective-C, such as the id type and implicit conversions, are not a thing in Swift. Even if you have an Int and a Double, and you want to add them up, you will have to convert them to a single type explicitly.

Also, optionals are a fundamental part of the language, and even though they are a simple concept, it takes some time to get used to them.

In the beginning, you might want to force unwrap everything, but that will eventually lead to crashes. As you get acquainted with the language, you start to love how you hardly have runtime errors since many mistakes are caught at compile time.

Most Swift programmers have significant previous experience with Objective-C, which, among other things, might lead them to write Swift code using the same practices they are familiar with in other languages. And that can cause some bad mistakes.

In this article, we outline the most common mistakes that Swift developers make and ways to avoid them.

Top Swift Development Mistakes

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

  1. Force-Unwrapping Optionals

A variable of an optional type (e.g. String?) might or might not hold a value. When they don’t hold a value, they’re equal to nil. To obtain the value of an optional, first you have to unwrap them, and that can be made in two different ways.

One way is optional binding using an if let or a guard let, that is:

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 } Second is forcing unwrap using the ! operator, or using an implicitly unwrapped optional type (e.g. String!). If the optional is nil, forcing an unwrap will cause a runtime error and terminate the application. Further, attempting to access the value of an implicitly unwrapped optional will cause the same.

We sometimes have variables that we can’t (or don’t want to) initialize in the class/struct initializer. Thus, we have to declare them as optionals. In some cases, we assume they won’t be nil in certain parts of our code, so we force unwrap them or declare them as implicitly unwrapped optionals because that’s easier than having to do optional binding all the time. This should be done with care.

This is similar to working with IBOutlets, which are variables that reference an object in a nib or storyboard. They won’t be initialized upon the parent object’s initialization (usually a view controller or custom UIView), but we can be sure they won’t be nil when viewDidLoad (in a view controller) or awakeFromNib (in a view) is called, and so we can access them safely.

In general, the best practice is to avoid forcing unwrap and using implicitly unwrapped optionals. Always consider the optional could be nil and handle it appropriately either using optional binding, or checking if it’s not nil before forcing an unwrap, or accessing the variable in case of an implicitly unwrapped optional.

  1. Not Knowing Pitfalls of Strong Reference Cycles

A strong reference cycle exists when a pair of objects keeps a strong reference to each other. This is not something new to Swift, since Objective-C has the same issue, and seasoned Objective-C developers are expected to properly manage this. It’s important to pay attention to strong references and what references what. The Swift documentation has a section dedicated to this topic.

It’s particularly important to manage your references when using closures. By default, closures (or blocks), keep a strong reference to every object that is referenced inside of them. If any of these objects has a strong reference to the closure itself, we have a strong reference cycle. It’s necessary to make use of capture lists to properly manage how your references are captured.

If there’s the possibility that the instance captured by the block will be deallocated before the block gets called, you have to capture it as a weak reference, which will be optional since it can be nil. Now, if you’re sure the captured instance will not be deallocated during the lifetime of the block, you can capture it as an unowned reference. The advantage of using unowned instead of weak is that the reference won’t be an optional and you can use the value directly without the need to unwrap it.

In the following example, which you can run in the Xcode Playground, the Container class has an array and an optional closure that is invoked whenever its array changes (it uses property observers to do so). The Whatever class has a Container instance, and in its initializer, it assigns a closure to arrayDidChange and this closure references self, thus creating a strong relationship between the Whatever instance and the closure.

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

If you run this example, you’ll notice that deinit whatever never gets printed, which means our instance w doesn’t get deallocated from memory. To fix this, we have to use a capture list to not capture self strongly:

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

In this case, we can use unowned, because self will never be nil during the lifetime of the closure.

It’s a good practice to nearly always use capture lists to avoid reference cycles, which will reduce memory leaks, and a safer code in the end.

  1. Using self Everywhere

Unlike in Objective-C, with Swift, we are not required to use self to access a class’ or struct’s properties inside a method. We are only required to do so in a closure because it needs to capture self. Using self where it’s not required is not exactly a mistake, it works just fine, and there will be no errors and no warnings. However, why write more code than you have to? Also, it is important to keep your code consistent.

  1. Not Knowing the Type of Your Types

Swift uses value types and reference types. Moreover, instances of a value type exhibit a slightly different behavior of instances of reference types. Not knowing what category each of your instances fit in will cause false expectations on the behavior of the code.

In the most object oriented languages, when we create an instance of a class and pass it around to other instances and as an argument to methods, we expect this instance to be the same everywhere. That means any change to it will be reflected everywhere, because in fact, what we have are just a bunch of references to the exact same data. Objects that exhibit this behavior are reference types, and in Swift, all types declared as class are reference types.

Next, we have value types which are declared using struct or enum. Value types are copied when they’re assigned to a variable or passed as an argument to a function or method. If you change something in the copied instance, the original one will not be modified. Value types are immutable. If you assign a new value to a property of an instance of a value type, such as CGPoint or CGSize, a new instance is created with the changes. That’s why we can use property observers on an array (as in the example above in the Container class) to notify us of changes. What’s actually happening, is that a new array is created with the changes; it is assigned to the property, and then didSet gets invoked.

Thus, if you don’t know the object you’re dealing with is of a reference or value type, your expectations about what your code is going to do, might be entirely wrong.

  1. Not Using the Full Potential of Enums

When we talk about enums, we generally think of the basic C enum, which is just a list of related constants that are integers underneath. In Swift, enums are way more powerful. For example, you can attach a value to each enumeration case. Enums also have methods and read-only/computed properties that can be used to enrich each case with more information and details.

The official documentation on enums is very intuitive, and the error handling documentation presents a few use cases for enums’ extra power in Swift. Also, check out following extensive exploration of enums in Swift to learn pretty much everything you can do with them.

  1. Not Using Functional Features

The Swift Standard Library provides many methods that are fundamental in functional programming and allow us to do a lot with just one line of code, such as map, reduce, and filter, among others.

Let’s examine few examples.

Say, you have to calculate the height of a table view. Given you have a UITableViewCell subclass such as the following:

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 } Consider, we have an array of model instances modelArray; we can compute the height of the table view with one line of code:

let tableHeight = modelArray.map { CustomCell.heightForModel($0) }.reduce(0, combine: +) The map will output an array of CGFloat, containing the height of each cell, and the reduce will add them up.

If you want to remove elements from an array, you might end up doing the following:

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) } } This example doesn’t look elegant, nor very efficient since we’re calling indexOf for each item. Consider the following example:

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) } } Now, the code is more efficient, but it can be further improved by using the filter:

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) The next example illustrates how you can remove all subviews of a UIView that meet certain criteria, such as the frame intersecting a particular rectangle. You can use something like:

for v in view.subviews { if CGRectIntersectsRect(v.frame, rect) { v.removeFromSuperview() } }

  We can do that in one line using `filter`

view.subviews.filter { CGRectIntersectsRect($0.frame, rect) }.forEach { $0.removeFromSuperview() } We have to be careful, though, because you might be tempted to chain a couple of calls to these methods to create fancy filtering and transforming, which may end up with one line of unreadable spaghetti code.

  1. Staying in the Comfort-Zone and Not Trying Protocol-Oriented Programming

Swift is claimed to be the first protocol-oriented programming language, as mentioned in the WWDC Protocol-Oriented Programming in Swift session. Basically, that means we can model our programs around protocols and add behavior to types simply by conforming to protocols and extending them. For example, given we have a Shape protocol, we can extend CollectionType (which is conformed by types such as Array, Set, Dictionary), and add a method to it that calculates the total area accounting for intersections

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 statement where Generator.Element: Shape is a constraint that states the methods in the extension will only be available in instances of types that conform to CollectionType, which contains elements of types that conform to Shape. For example, these methods can be invoked on an instance of Array<Shape>, but not on an instance of Array<String>. If we have a class Polygon that conforms to the Shape protocol, then those methods will be available for an instance of Array

as well.

With protocol extensions, you can give a default implementation to methods declared in the protocol, which will then be available in all types that conform to that protocol without having to make any changes to those types (classes, structs or enums). This is done extensively throughout the Swift Standard Library, for example, the mapand reduce are defined in an extension of CollectionType, and this same implementation is shared by types such as Array and Dictionary without any extra code.

This behavior is similar to mixins from other languages, such as Ruby or Python. By simply conforming to a protocol with default method implementations, you add functionality to your type.

Protocol oriented programming might look quite awkward and not very useful at first sight, which could make you ignore it and not even give it a shot. This post gives a good grasp of using protocol-oriented programming in real applications.

As We Learned, Swift Is Not a Toy Language

Swift was initially received with a lot of skepticism; people seemed to think that Apple was going to replace Objective-C with a toy language for kids or with something for non-programmers. However, Swift has proven to be a serious and powerful language that makes programming very pleasant. Since it is strongly typed, it is hard to make mistakes, and as such, it’s difficult to list mistakes you can make with the language.

When you get used to Swift and go back to Objective-C, you will notice the difference. You’ll miss nice features Swift offers and will have to write tedious code in Objective-C to achieve the same effect. Other times, you’ll face runtime errors that Swift would have caught during compilation. It is a great upgrade for Apple programmers, and there’s still a lot more to come as the language matures.

Source: https://www.toptal.com/swift/top-swift-development-mistakes


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí