Been using a lot of print() when working on iOS projects. Hoping to see more information printed to aid in debugging, I did up a simple logging class in Swift which infers the file, function and line number where the logging is done. Not using the stack trace as the output from Thread.callStackSymbols is not informational enough.
// Z.swift
import Foundation
public class Z {
public static func log(
_ message: String,
_ extra: String = "",
file: String = #file,
function: String = #function,
line: Int = #line
) {
printLog(message, extra, file:file, function:function, line:line)
}
public static func log(
_ message: Int,
_ extra: String = "",
file: String = #file,
function: String = #function,
line: Int = #line
) {
printLog(String(message), extra, file:file, function:function, line:line)
}
public static func log(
_ message: Any?,
_ extra: String = "",
file: String = #file,
function: String = #function,
line: Int = #line
) {
// Passing [MyClass] to log() will end up coming here, hence a check
// to see if it is an array
if let array = message as? [Any] {
logList(array, extra, file:file, function:function, line:line)
} else {
printLog(
getObjectAsString(message),
extra,
file:file,
function:function,
line:line
)
}
}
static func logList(
_ message: [Any]?,
_ extra: String = "",
file: String = #file,
function: String = #function,
line: Int = #line
) {
if extra != "" {
printLog(extra, "", file:file, function:function, line:line)
}
if message != nil {
message!.forEach({ (item) in
printLog(
getObjectAsString(item),
"",
file:file,
function:function,
line:line
)
})
}
}
static func printLog(
_ message: String,
_ extra: String = "",
file: String = #file,
function: String = #function,
line: Int = #line
) {
// Extract the filename from the full path
let filename: String = (file as NSString).lastPathComponent
// Extract the method name without the parameter list
var methodName = function
if let regex = try? NSRegularExpression(pattern: "\\([^\\)]*\\)") {
methodName = regex.stringByReplacingMatches(
in: function,
options: [],
range: NSRange(location: 0, length: function.count),
withTemplate: ""
)
}
print("\(filename):\(methodName):L\(line): \(message)\(extra)")
}
static func getObjectAsString(_ object: Any?) -> String {
var output: String = ""
if object != nil {
let mirror = Mirror(reflecting: object!)
for (property, value) in mirror.children {
guard let property = property else { continue }
if ["id", "name"].contains(property) {
output += "\(property):\(value); "
}
}
}
output += String(describing: object)
return output
}
}
To use, just call Z.log(). The user does not need to cast the arguments to String first. The 2nd parameter, extras, is optional but useful when logging numbers. Line numbers included in the output are useful for tracing output from callbacks. For objects, public properties id/name are used if they exist. Below is an example of how it is used.
// ViewController.swift
import UIKit
class ViewController: UIViewController {
func test() {
// ViewController:test:L8: Hello World
Z.log("Hello World") // this is line 8 of the code, hence L8 in output
// ViewController:test:L11: 10 items
Z.log(10, " items")
// ViewController:test:L16: id:1; name:A; Optional(<Person: 0x2826d528>)
// Assume Person class with id and name properties.
// This works on [Person] as well.
Z.log(Person(id:1, name:"A"))
// ViewController:test:L22: [Strings]
// ViewController:test:L22: Optional("alpha")
// ViewController:test:L22: Optional("beta")
let items: [String] = ["alpha", "beta"]
Z.log(items, "[Strings]") // not sure why log output has Optional
}
}

