Skip to content
Go back

Logging in Swift

Logging is one of the rare cases when you could probably justify having a singleton, but with Swift Protocol Extension you don’t need to.

Let’s integrate Logging in a way that:

Interface

Protocol oriented programming is a very useful technique for Swift source, we can create protocols and provide default implementations for whole or specific parts of it.

Let’s declare our interfaces first:

public enum LogTag: String {
    case Observable
    case Model
    case ViewModel
    case View
}

public enum LogLevel: Int {
    case Verbose = 0
    case Debug = 1
    case Info = 2
    case Warning = 3
    case Error = 4
}

public protocol Loggable {
    var defaultLoggingTag: LogTag { get }

    func log(level: LogLevel, @autoclosure _ message: () -> Any, _ path: String, _ function: String, line: Int)
    func log(level: LogLevel, tag: LogTag, @autoclosure message: () -> Any, _ path: String, _ function: String, line: Int)
}

Default implementation

We want to provide default implementation for our logging function, and only require the users of our protocol to provide defaultLoggingTag:

public extension Loggable {
    func log(level: LogLevel, @autoclosure _ message: () -> Any, _ path: String = #file, _ function: String = #function, line: Int = #line) {
        log(level, tag: defaultLoggingTag, message: message, path, function, line: line)
    }

    func log(level: LogLevel, tag: LogTag, @autoclosure message: () -> Any, _ path: String = #file, _ function: String = #function, line: Int = #line) {
        Logger.sharedInstance.log(level, tag: tag, className: String(self.dynamicType), message: message, path, function, line: line)
    }
}

Logger singleton

We need to provide implementation of Logger that our protocol extension is supposed to use:

protocol LoggerType {
    func log(level: LogLevel, tag: LogTag, className: String, @autoclosure message: () -> Any, _ path: String, _ function: String, line: Int)
}

final class Logger {
    internal var activeLogger: LoggerType?
    internal var disabledSymbols = Set<String>()
    private(set) static var sharedInstance = Logger()

    /// Overrides shared instance, useful for testing
    static func setSharedInstance(logger: Logger) {
        sharedInstance = logger
    }

    func setupLogger(logger: LoggerType) {
        assert(activeLogger == nil, "Changing logger is disallowed to maintain consistency")
        activeLogger = logger
    }

    func ignoreClass(type: AnyClass) {
        disabledSymbols.insert(String(type))
    }

    func ignoreTag(tag: LogTag) {
        disabledSymbols.insert(tag.rawValue)
    }

    func log(level: LogLevel, tag: LogTag, className: String, @autoclosure message: () -> Any, _ path: String, _ function: String, line: Int) {
        guard logAllowed(tag, className: className) else { return }
        activeLogger?.log(level, tag: tag, className: className, message: message, path, function, line: line)
    }

    private func logAllowed(tag: LogTag, className: String) -> Bool {
        return !disabledSymbols.contains(className) && !disabledSymbols.contains(tag.rawValue)
    }
}

Our Logger instance is only responsible for log suppressing symbols, as this is functionality often missing from external loggers.

It doesn’t log message anywhere directly, instead, it forwards it to specific LoggerType implementation:


    var prototype: LogMessagePrototype?
    var flushTimeout: Int64?

    func log(level: LogLevel, tag: LogTag, className: String, @autoclosure message: () -> Any, _ path: String, _ function: String, line: Int) {
        prototype = LogMessagePrototype(level: level, tag: tag, className: className, message: "\(message())", path: path, function: function, line: line)
    }

    func flush(secondTimeout: Int64) {
        flushTimeout = secondTimeout
    }
}

let setup: () -> (sut: Logger, stub: LoggerStub) = {
    let sut = Logger()
    let stub = LoggerStub()
    sut.setupLogger(stub)
    Logger.setSharedInstance(sut)
    return (sut, stub)
}

func testLoggableLogWithDefaultTagArgumentIsForwarded() {
    let (_, stub) = setup()
    let expected = LogMessagePrototype(level: .Warning, tag: defaultLoggingTag, className: String(self.dynamicType), message: "Test", path: "path", function: "function", line: 66)
    let actual: LogMessagePrototype? = {
        self.log(expected.level, expected.message, expected.path, expected.function, line: expected.line)
        return stub.prototype
    }()

    XCTAssertEqual(expected, actual)
}

func testIgnoredTagsAreHonored() {
    let (sut, stub) = setup()
    let expected: LogMessagePrototype? = nil
    let actual: LogMessagePrototype? = {
        sut.ignoreTag(.Model)
        sut.log(.Info, tag: .Model, className: "", message: "", "", "", line: 0)
        return stub.prototype
    }()

    XCTAssertEqual(expected, actual)
}

Note: LogMessagePrototype is just a struct containing all params the function accepts.

If you are working on a Model layer in your app, and you don’t care about UI logs, you can create a user breakpoint on the configuration point and do:

A simple implementation that fulfills all of our initial requirements. It’s also an example of how you can create testable protocol extensions.


Share this post on:

Previous Post
Setting up pre-commit hook for iOS
Next Post
iOS Architecture