Swift conditional logging & compiler flags

During development it’s important to figure out what in the world is happening in all that code. That’s where logging comes in. The alternative is to add break points (and crash Xcode by using the console print output statement in a closure 😐).

Release versions of code should never contain print statements used for debugging. Printing to the console is actually an operation that has a cost and can effect performance. This is especially true during high occurrence operations (loops, observers or callbacks for example). Also when printing large chunks of information.

Logging in frameworks

The same is true for frameworks. Many developers rely on third party frameworks as well as our own frameworks in production applications. Thus, frameworks should not print to the console.

But logs are useful… so now what?

“To log or not to log . . . that is the question” ~ A wise programmer, probably

Frameworks should include useful and informational logging. Then allow a project owner to opt-in to printing logs. One way to do this is to use a variable and an internal logging function.

public var enableLogging = false
internal func logIfEnabled(string: String) {
if enableLogging { print(string) }
}
// Call instead of print(string)
logIfEnabled(string)

Cool now we only log if this variable is set. If the framework is one class the enableLogging variable can be an instance variable. If there are many classes a singleton implementation would be better. Then the value is set once and the framework can use it throughout.

// In project
MyFramework.shared.enableLogging = true
// In framework logIfEnabled(_:)
if MyFramework.shared.enableLogging { print(string) }

Conditional compiling with flags

It’s fine to set the enableLogging variable to true during development then when ready to release an app switch it to false. But that’s just one more thing to remember so instead use a compiler flag.

Locate Swift compiler flags in the target build settings under Swift Compiler — Custom Flags → Other Swift Flags

Any symbols defined here can be used for conditional compiling. For now we set -DDEBUG and -DRELEASE. Then wrap the enableLogging call in conditional compilation. This enables logging only during debug and never in a release build.

#if DEBUG
MyFramework.shared.enableLogging = true
#endif

Can’t we just set the compiler flag in the framework target then?

Sure can. Then the log function becomes the following.

internal func logIfEnabled(string: String) {
#if DEBUG
print(string)
#endif
}

This eliminates the need to set any variables in actual project code.

How about ‘print’

That’s actually possible as well. We can override the Swift print function globally in the framework.

internal func print(items: Any..., separator: String = " ", terminator: String = "\n") {
    #if DEBUG
var i = items.startIndex
repeat {
Swift.print(items[i], separator: separator, terminator: i == (items.endIndex - 1) ? terminator : separator)
i += 1
} while i < items.endIndex
#endif
}

Now we can call print as normal and it will only print in debug and not in release.

Note: This will work in an application target as well, I’m just using a framework as an example.

Cocoapods frameworks support

Compiler flags can’t be set in advance on frameworks installed with CocoaPods. The flags should be added on the target after install.

I prefer to use the neat post_install feature of Cocoapods instead of setting the flag manually. In the podfile:

post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == 'FrameworkToBeFlagged'
target.build_configurations.each do |config|
if config.name == 'Debug'
config.build_settings['OTHER_SWIFT_FLAGS'] = '-DDEBUG'
else
config.build_settings['OTHER_SWIFT_FLAGS'] = ''
end
end
end
end
end

print(“Thanks for reading!”)