Swift: Logging

beech-1446300-m

Leaping from tree to tree! As they float down the mighty rivers of British Columbia! With my best girl by my side!
The Larch! The Pine! The Giant Redwood tree! The Sequoia!
The Little Whopping Rule Tree! We’d sing! Sing! Sing!

The Lumberjack Song

Swift logging is a delightfully odd creature. Did you know, for instance, that there are separate standard and debugging logging features? Or that you could log directly to a string?

Here are a bunch of logging tips for you to enjoy.

Debug Print

Unlike normal print commands, debug print requests show instance representations that are “most suitable for debugging”. For example, debug printing includes quotes when regular printing does not:

Screen Shot 2015-05-22 at 11.45.02 AM

That extra information can be critical. The following code breaks a string into an array consisting of the first two words (Lorem and ipsum) and then lumps all the remaining values into the third entry.

var lipsum = "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
var words = split(lipsum, maxSplit:2, isSeparator:{$0 == " "})

When you println these results, it’s hard to see what the function has accomplished.

[Lorem, ipsum, dolor sit amet, consectetur adipiscing elit.]

The comma after amet looks as if there are four elements in this array, not three. Debug printing ensures that each element is clearly delineated. The quote marks highlight each item in the resulting array.

["Lorem", "ipsum", "dolor sit amet, consectetur adipiscing elit."]

It’s not just about quotes. With debug printing, 1..<6 becomes Range(1..<6) and ? becomes “\u{0001F600}”. Each result offers additional utility that supports your development.

The debugPrintln() and debugPrint() functions use the DebugPrintable representation when it has been implemented. This protocol requires a debugDescription variable from conforming instances.

Screen Shot 2015-05-22 at 12.05.52 PM

If you create the Point struct shown here and do not conform to either of the Printable or DebugPrintable protocols, you’ll print out some value like “__lldb_expr_44.Point”.  If you implement one and not the other, each one falls back to the other. So if you print and there’s only a debug description, it will print the debug description in favor of the default output.

Here’s how the debug print fallback cascade works:

  • If `T` conforms to `DebugPrintable`, write `x.debugDescription`
  • Otherwise, if `T` conforms to `Printable`, write `x.description`
  • Otherwise, if `T` conforms to `Streamable`, write `x`

For print, reverse the order. If an item does not support any of these protocols, Swift prints a default textual representation (like “__lldb_expr_44.Point”).

The Streamable protocol mentioned here requires you to implement a writeTo() function that writes data to an output stream:

func writeTo<Target : OutputStreamType>(inout target: Target)

Regular and debug printing both optionally write to an output stream that you specify.

Printing to Strings

Print() and friends also print to arbitrary output streams that conform to the `OutputStreamType` protocol.  Interestingly, this protocol is adopted by String, meaning you can log directly to strings instances. e.g.

var s = ""
print("Hello world", &s)
s // is now "Hello World"

Printing to Stderr

By default, all printing (regular and debug versions) uses the standard output stream. You may redirect output by providing an optional second argument to println.

For example, the following struct stores an output stream for stderr:

public struct StderrOutputStream: OutputStreamType {
    public static let stream = StderrOutputStream()
    public func write(string: String) {fputs(string, stderr)}
}
public var errStream = StderrOutputStream.stream

println("This prints to stderr", &errStream)
println("This prints to stdout")

Test this by building a Swift command line utility using this code. Pipe its output through open -f. Only the second line, which uses stdout, appears in TextEdit.

Note: You can use FileOutputStream to create a path and open it with fopen(). You don’t really have to mess with freopen and stderr. Even though it’s cool. And fun.

Printing Using Formats

While support for NSLog() is baked into Swift, you can easily create a Swift-specific alternative that doesn’t include date/time output (and won’t mirror output to the system console):

func swlog(format : String, args : CVarArgType...) {
    println(String(format: format, arguments: args), &errStream)
}
swlog("Hello world") // no args
swlog("Hello world: %2.3f", 5.2) // one arg
swlog("Hello world: %2.3f, %@", 5.2, "yikes") // multiple args

This implementation uses the errStream stream and String initialization to create a minimal stderr log-alike.

Printing to Files

If you’d rather redirect your logging to files, use freopen.

import XCPlayground
let outputPath = XCPSharedDataDirectoryPath.\
    stringByAppendingPathComponent("testOutput.txt")
freopen(outputPath, "a", stderr)
swlog("Testing output to file: \(NSDate())")

Better yet, add support directly to the StderrOutputStream:

This implementation enables you to control whether you appendToFile (default) or rewrite and whether the output stream should echo to stdout when set to rewrite. Pass nil to redirectOutputToPath() to switch redirection back off.

Using System Logging

Screen Shot 2015-05-22 at 7.46.00 AM

Apple System Logging enables you to hook into OS X’s log facility, replacing the older syslog API. To use this logging, follow these steps.

1. Create a new custom Cocoa framework.

2. Add the following import statement to your primary framework header.

#import “asl.h”

3. Select Target > Build Phases. Open Headers, click +, click Add Other, navigate to /usr/include, select asl.h. Click Finish.

4. Move the new asl.h reference file into your framework group if needed. Select it and open the File Inspector. Set Location to Absolute Path and make sure it points to /usr/include/asl.h.

5. Under the Identity and Type group is Target membership. Make sure it says Public and not Private for your Logging Framework.

6. In Project > Build Settings, switch Allow Non-modular Includes in Framework Modules from No to Yes.

7. Build the framework.

You should now be able to import the framework into any files or playgrounds in the same workspace. If you cannot quit and restart Xcode.

Thanks, as always, Lily Ballard

5 Comments