Göksel Köksal
Göksel Köksal iOS developer. Serious gamer.

Lightning Read #2: Optional String Pitfalls

Lightning Read #2: Optional String Pitfalls

Photo by Max Bender on Unsplash

I was trying to build a URL the other day. Check the following code block:

init?(userId: String?) {
  guard userId != nil else { return nil }

  self.path = "/user/\(userId)"
  self.url.appendPathComponent(self.path)
}

Seems legit, right?

Assume that we’re passing 23940 as userId to this initializer. Depending on the Swift version, path value would be:

Swift 2.x | path = "/user/23940"
Swift 3.x | path = "/user/Optional("23940")"
Swift 4.x | path = "/user/Optional("23940")"

Subtle, but heart-breaking.

Firstly, beware of this issue if you’re working on a super-old project which still uses Swift 2.x for some reason, and planning to migrate.

Secondly, we could use a guard-let instead of a guard-check statement here to fix this issue. However, it’s not convenient to have guard statements everywhere, especially when you simply want to print stuff.

I created the following struct to overcome this problem:

public struct Printable<T>: CustomStringConvertible {

    public let value: T?

    public init(_ value: T?) {
        self.value = value
    }

    public var description: String {
        if let value = value {
            return String(describing: value)
        } else {
            return "(null)"
        }
    }
}

Note: Yes, we could also print “nil” instead of “(null)” but I think the latter is more expressive. You can always choose this one or the other.

Whenever you have optionals, you simply wrap them in a Printable struct as below.

self.path = "/user/\(Printable(userId))" // "/user/23940"
print(Printable(userId))                 // "23940"

Or, in a more complex example:

struct User: CustomStringConvertible {
  let id: String
  let firstName: String?
  let lastName: String?

  var description: String {
    var string = id
    string += ", \(Printable(firstName))"
    string += ", \(Printable(lastName))"
    return string
  }
}
let user1 = User(
  id: "23940",
  firstName: "Göksel",
  lastName: "Köksal"
)
let user2 = User(
  id: "23941",
  firstName: "Sıla",
  lastName: nil
)
print(user1)
print(user2)

Without Printable struct, these objects would print below;

"23940, Optional("Göksel"), Optional("Köksal")"
"23941, Optional("Sıla"), nil"

With Printable, it prints;

"23940, Göksel, Köksal"
"23941, Sıla, (null)"

The latter seems much cleaner.

Alternatives

Global “describe” Function

func describe<T>(_ value: Optional<T>) -> String {
  switch value {
    case .some(let wrapped):
      return String(describing: wrapped)
    case .none:
      return "(null)"
  }
}

Optional Extension

extension Optional {

  var stringValue: String {
    switch self {
      case .some(let wrapped):
        return String(describing: wrapped)
      case .none:
        return "(null)"
    }
  }
}

Credit: Alp Avanoğlu

Comparison

var url1: URL? = URL(string: "www.google.com")
var url2: URL? = nil

// `Printable` struct:

print(Printable(url1))  // Prints "www.google.com"
print(Printable(url2))  // Prints "(null)"

// `describe` global function:

print(describe(url1))   // Prints "www.google.com"
print(describe(url2))   // Prints "(null)"

// `stringValue` extension on `Optional`:

print(url1.stringValue) // Prints "www.google.com"
print(url2.stringValue) // Prints "(null)"

Moral of the Story

If you are annoyed by logs like Optional(www.google.com) on your console, you can start using one of the alternatives.


Thanks for dropping by! And, as usual, help spread the word. ❤️👏

comments powered by Disqus