Swift Grab Bag

Episode #251 | 9 minutes | published on 01/13/2017 | Uses swift-3.0
Subscribers Only
In this episode we cover some lesser-known features of Swift, including @discardableResult, escaping closures, defer, and using dump versus print for better debugging output.

Episode Links

@discardableResult

When you have a function that returns a value purely for informational reasons (i.e. the return value is often ignored), then it can be annoying that the swift compiler gives you a warning if you don't assign it to a variable.

To squelch this warning, you can add the @discardableResult flag to the method name:

func @discardableResult save(value: String, forKey key: String) -> Bool) {
  // ...
  return true
}

save(value: "night", forKey: "theme")

// no more warning for unused return values!

This is particularly handy for some APIs where the return value is almost always ignored and is only useful in some narrow circumstances.

@escaping closures

Say we have a class with a method that takes a block as a parameter. Inside, we just run the block directly:

class Updater
  func run(block: () -> Void) {
     block()
  }
}

Then we use this from another class:

class Foo {
  var bar = ""
  func baz() {
     Updater().run {
       bar = "I was modified"
     }
  }
}

Note that we didn't have to capture self. inside the block. This is because the block was marked (by default) as non-escaping. The compiler knows that the block never escapes the scope in which it was defined, so external variables that are used inside the block don't need to be captured with a strong reference. This is an optimization that the compiler can make for us by default.

Conversely, if the block does need to escape the scope (for instance being assigned to an instance variable or called asynchronously), then the values that are used inside the block do need to be strongly referenced. If we try to do this, we'll get a compiler error:

class Updater
  var block: (() -> Void)?
  func run(block: () -> Void) {
     self.block = block // error!
  }
}

Why? Because, now that our block escapes the scope, we need to mark the block parameter as escaping so that callers know this, and thus the compiler can enforce that when the block is created that it captures its variables:

class Updater
  var block: (() -> Void)?
  func run(block: @escaping () -> Void) {
     self.block = block
  }
}

But this causes our block creation code to no longer compile. It complains that we need to capture self inside the block:

func baz() {
     Updater().run {
       self.bar = "I was modified"
     }
  }

Adding self. is required so that it becomes obvious that self is retained inside the block, and that's your queue to be on the lookout for retain-cycles.

Print vs. Dump

Often, when debugging, we use print to output the result of some object to verify things are working as intended. If we're using a struct, we get pretty useful results:

struct Person {
  let name: String
  let age: Int
}
let joe = Person(name: "Joe", age: 44)
print(joe) 
// outputs Person(name: "Joe", age: 44)

But if we're using a class, we don't get this output.

class Address {
  let streetAddress: String
  init(streetAddress: String) {
    self.streetAddress = streetAddress
  }
}

let addr = Address(streetAddress: "123 Any St.")
print(addr)
//outputs "Address"

Instead, we can use dump to leverage swift's limited introspection capability and give us a much better raw output for an object:

dump(addr)
//outputs:
//  Address:
//    streetAddress: 123 Any St.

This even works for nested objects.

defer

When you want to execute some code at the end of a method, but you have some early returns, your only option is essentially to copy/paste the code in multiple places. With defer, we can clean that up.

Say we're writing a table view controller method:

tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: IndexPath) {

}

Inside this method we want to ignore the selection if the section is 0, otherwise we'll push a view controller. In either case we need to deselect the row.

defer {
  tableView.deselectRow(at: indexPath)
}

if section == 0 {
  return
}

// push vc

This can be used anywhere you have multiple escape-points for a method.

blog comments powered by Disqus