Detecting retain cycles and improved logging

:  ~ 1 min read

I think adding everywhere a deinit method, with a print statement inside, is a decent first barrier against retain cycles:

deinit {
  print("Object X has been deinitialized.")
}

This way, if you expect object x to deinit at some point but it doesn't, you at least know you need to start searching, and where.

Now, for the improved printing, to make this a bit cleaner and easier:

func customPrint<T>(
  _ object: T,
  _ function: String = #function, // 1
  _ file: String = #file, // 2
  _ line: UInt = #line) { // 3
  
  #if DEBUG // 4
    let filename = URL(string: file)?
      .lastPathComponent
      .stringByReplacingOccurrencesOfString(".swift", withString: "")
      ?? ""
    
    if object is EmptyPrintFlagger { // 5
      print("-- \(filename).\(function) [\(line)]") // 6
    }
    else {
      print("-- \(filename).\(function) [\(line)] - \(object)") // 7
    }
  #endif
}

func customPrint( // 8
  _ function: String = #function,
  _ file: String = #file,
  _ line: UInt = #line) {
  customPrint(EmptyPrintFlagger(), function, file, line) // 9
}

fileprivate struct EmptyPrintFlagger { } // 10

First, a few output examples, then we'll break down everything:

// ObjectX.swift
class ObjectX {

  deinit {
    customPrint() // 11 => ObjectX.deinit [60]
    customPrint("Example") // => ObjectX.deinit [61] - Example
    
    let i: Int? = nil
    let j: Int? = 5
    
    customPrint(i) // => ObjectX.deinit [66] - nil
    customPrint(j) // => ObjectX.deinit [67] - Optional(5)
  }
  
}

First, the default parameters: they are built-in keywords that return the current function (1), file (2) and line number (3). You can pass in any String, but the purpose of these is to print the current location of customPrint.

Next, we make sure no printing happens in production (2), because it (slightly) slows down the app, and clutters the system console with our logs.

Finally, we will do a bit of extra work now, so we can be lazy at later times: we create a dummy, fileprivate struct (10), that acts as a flag inside our main customPrint (5). This way we are able to call customPrint without any parameters (8, 11), but still get the correct location (5, 6, 9, 10).

Sadly, I didn't find any better way to differentiate between customPrint() and customPrint(x). While it wouldn't have been hard to just call customPrint(""), I hated the dangling - (7 with an empty String), and I'm also that lazy sometimes ¯\_(ツ)_/¯.