Eavesdropping on Swift’s Print Statements

I have been developing against hardware that needs to be plugged in directly to the device, which means I am unable to debug and step through my code. At the very least, I want to be able to see my print statements in a UITextView inside the application.

Now I know I can use 3rd party libraries such as Cocoa Lumberjack & Antenna, but I do not want to replace all my print statements with Cocoa Lumberjack’s print statements (e.g. DDLogVerbose). The ideal solution would be to “listen in” on Swift’s print statements.

To do this, you can listen into STDOUT (standard out) and STDERR (standard error) by creating a new Pipe and replacing STDOUT and STDERR’s file descriptors to your pipe’s file descriptors. This forward the items being printed to STDOUT to your Pipe. You also need to pipe back to the original STDOUT’s file descriptor to ensure your logs continue to be printed in the Xcode console.

func openConsolePipe() {
//open a new Pipe to consume the messages on STDOUT and STDERR
inputPipe = Pipe()

//open another Pipe to output messages back to STDOUT
outputPipe = Pipe()

guard let inputPipe = inputPipe, let outputPipe = outputPipe else {
return
}

let pipeReadHandle = inputPipe.fileHandleForReading

//from documentation
//dup2() makes newfd (new file descriptor) be the copy of oldfd (old file descriptor), closing newfd first if necessary.

//here we are copying the STDOUT file descriptor into our output pipe's file descriptor
//this is so we can write the strings back to STDOUT, so it can show up on the xcode console
dup2(STDOUT_FILENO, outputPipe.fileHandleForWriting.fileDescriptor)

//In this case, the newFileDescriptor is the pipe's file descriptor and the old file descriptor is STDOUT_FILENO and STDERR_FILENO

dup2(inputPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO)
dup2(inputPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO)

//listen in to the readHandle notification
NotificationCenter.default.addObserver(self, selector: #selector(self.handlePipeNotification), name: FileHandle.readCompletionNotification, object: pipeReadHandle)

//state that you want to be notified of any data coming across the pipe
pipeReadHandle.readInBackgroundAndNotify()
}

To get access to the print statements and write back to STDOUT, you must listen in on the notification from your pipeReadHandle

func handlePipeNotification(notification: Notification) {
//note you have to continuously call this when you get a message
//see this from documentation:
//Note that this method does not cause a continuous stream of notifications to be sent. If you wish to keep getting notified, you’ll also need to call readInBackgroundAndNotify() in your observer method.
inputPipe?.fileHandleForReading.readInBackgroundAndNotify()

if let data = notification.userInfo[NSFileHandleNotificationDataItem] as? Data,
let str = String(data: data, encoding: String.Encoding.ascii) {

//write the data back into the output pipe. the output pipe's write file descriptor points to STDOUT. this allows the logs to show up on the xcode console
outputPipe?.fileHandleForWriting.write(data)

// `str` here is the log/contents of the print statement
//if you would like to route your print statements to the UI: make
//sure to subscribe to this notification in your VC and update the UITextView.
//Or if you wanted to send your print statements to the server, then
//you could do this in your notification handler in the app delegate.
}
}

That’s the gist of it. You have to listen in on STDOUT and STDERR by replacing the file descriptor with your pipe’s file descriptor. This allows you to capture all print statements which can then be forwarded to a server or displayed in a UITextView.

To ensure that you capture all logs, you can call openConsolePipe() in didFinishLaunchingWithOptions in the AppDelegate

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

// if not simulator, open console pipe
#if !((arch(i386) || arch(x86_64)) && os(iOS))
openConsolePipe()
#endif

return true
}