DEV Community

Bugfender
Bugfender

Posted on • Originally published at bugfender.com on

Swift Logging Techniques: A Complete Guide to iOS Logging

Logging plays a crucial role in app development. As well as debugging everyday issues with the product, a good logger will monitor application behavior and gain insights into user interactions. This is particularly crucial for iOS developers given the frequency of Apple’s official updates and the ferocious competition in the marketplace.

This article explores various logging tools, from traditional methods like NSLog to advanced options such as Apple’s Unified Logging System and third-party solutions like our very own Bugfender (yep, we’re going to flex a little here but, well, it’s our article right?! And we’ll be totally impartial, promise).

Swift Logging Frameworks and Tools

Swift developers have access to a variety of logging frameworks and tools, each offering different logger types and capabilities. Let’s explore some of the most popular options:

Apple’s Unified Logging System

This is Apple’s recommended approach for logging in modern iOS and macOS applications. It provides a centralized system for capturing, storing, and accessing iOS logs across Apple platforms.

NSLog

NSLog is the traditional method for logging in both, Objective-C, the original iOS programming language, and its successor, Swift. While it’s simple to use, NSLog has limitations in terms of performance and flexibility. For a deep dive into NSLog, check out our comprehensive guide to NSLog.

CocoaLumberjack

CocoaLumberjack is a popular third-party logging framework known for its flexibility and performance. It offers features like log levels, multiple loggers, and log filtering. Learn more about CocoaLumberjack in our detailed CocoaLumberjack tutorial.

SwiftyBeaver

SwiftyBeaver was a popular logging tool for Swift, known for its ease of use and colorful console output. However, it’s important to note that SwiftyBeaver is now deprecated (in other words, obsolete). For developers looking for a powerful alternative with remote logging capabilities, Bugfender is an excellent choice.

NSLogger

NSLogger is another third-party logging tool that offers features like remote logging and a desktop viewer application. For more information, check out our guide to NSLogger.

SwiftLog

SwiftLog is Apple’s open-source logging API for Swift. It aims to provide a common logging interface that can be used across different Swift platforms and frameworks.

Apple’s Unified Logging System

Let’s dive deeper into Apple’s recommended logging solution: the Unified Logging System.

Core Concepts of Apple’s Unified Logging System

The Unified Logging System introduces several key concepts:

  1. Subsystems and Categories : These help organize and filter logs.
  2. Log Levels : Different levels of importance for log messages.
  3. Privacy : Built-in privacy controls for sensitive information.

Getting Started with Apple’s Unified Logging System

Basic Setup

To use the Unified Logging System, which serves as a robust logging backend for your application, you first need to import the os framework, you first need to import the os framework like so:

import SwiftUI
import os

@main
struct demoApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Once you have imported the os framework, you can create a Logger instance. You can use this later to log your messages, which you can view in the Xcode console log section or retrieve from your iOS device log archive. The Logger class supersedes the old os_log and OSLog framework methods used in previous iOS versions and it’s the recommended Apple approach to logging. The new Logger API simplifies some of the complexities of os_log and automatically handles privacy and performance considerations more intuitively.


class AppDelegate: NSObject, UIApplicationDelegate {
    let logger = Logger(subsystem: "com.yourcompany.app", category: "main")

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // Logger usage will go here
        return true
    }
}

Enter fullscreen mode Exit fullscreen mode

Usage of Logger Object

Once you’ve created a logger, it’s pretty straightforward. You can use it to write log messages and you will get your log output in the console app when you run the code:

logger.log("This is a log message")

Enter fullscreen mode Exit fullscreen mode

As you can see, we are using the log function to send a log entry to the Xcode debug console.

Writing Log Messages

Log Levels

The Unified Logging System provides several log levels, also known as logging levels, which allow developers to indicate the importance or severity of a log message, from debug level information to critical errors:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
    logger.debug("Debug message")
    logger.info("Info message")
    logger.notice("Notice message")
    logger.error("Error message")
    logger.fault("Critical fault message")
    return true
}

Enter fullscreen mode Exit fullscreen mode

Data Values

You can include dynamic values in your log messages, making use of structured logging to ensure that data is organized and easy to analyze:

let userCount = 42
logger.info("Current user count: \(userCount)")

Enter fullscreen mode Exit fullscreen mode

Log Formatting

Colors

While the Unified Logging System doesn’t directly support colors, Xcode’s console will help you out by color-coding messages based on their log level.

Value Formatting

You can format values in your log messages:

 let price = 19.99
 let formattedPrice = String(format: "%.2f", price)
 logger.info("Item price: $\(formattedPrice)")

Enter fullscreen mode Exit fullscreen mode

Alignment

For tabular data, you can use string formatting to align values:

let userId = 12345
let userName = "John Doe"
logger.info("User ID: \(String(format: "%-10d", userId)) Name: \(userName)")

Enter fullscreen mode Exit fullscreen mode

Advanced Usage of Apple’s Unified Logging System

Privacy

The Unified Logging System provides built-in privacy controls, which are essential when implementing structured logging, especially in applications that handle sensitive user information.

import os.log

let email = "user@example.com"
logger.info("User email: \(email, privacy: .private)")

Enter fullscreen mode Exit fullscreen mode

Network Requests Logging

Here’s an advanced example of how you might log network requests with additional details:

    func logNetworkRequest(_ request: URLRequest, response: HTTPURLResponse?, data: Data?) {
        let networkLogger = Logger(subsystem: "com.bugfender.app", category: "network")

        networkLogger.info("Request: \(request.url?.absoluteString ?? "Unknown URL", privacy: .public)")
        networkLogger.info("Method: \(request.httpMethod ?? "Unknown method", privacy: .public)")
        networkLogger.info("Headers: \(request.allHTTPHeaderFields?.description ?? "No headers", privacy: .private)")

        if let httpBody = request.httpBody, let bodyString = String(data: httpBody, encoding: .utf8) {
            networkLogger.debug("Request Body: \(bodyString, privacy: .private)")
        }

        networkLogger.info("Status Code: \(response?.statusCode ?? 0, privacy: .public)")

        if let data = data {
            let responseSize = ByteCountFormatter.string(fromByteCount: Int64(data.count), countStyle: .file)
            networkLogger.info("Response Size: \(responseSize, privacy: .public)")

            if let responseString = String(data: data, encoding: .utf8) {
                networkLogger.debug("Response Body: \(responseString, privacy: .private)")
            }
        }
    }


    func exampleNetworkRequest() {
        let url = URL(string: "<https://api.example.com/data>")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")

        let bodyData = """
        {
            "username": "johndoe",
            "password": "secret123"
        }
        """.data(using: .utf8)
        request.httpBody = bodyData

        // Simulating a response
        let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)
        let responseData = """
        {
            "status": "success",
            "token": "abc123xyz789"
        }
        """.data(using: .utf8)

        // Log the request and response
        logNetworkRequest(request, response: response, data: responseData)
    }

Enter fullscreen mode Exit fullscreen mode

Screenshot 2024-07-15 at 3.13.05 PM.png

Bugfender and Remote Logging

While local logging is crucial for development, remote logging takes your debugging capabilities to the next level by serving as an external logging backend that aggregates logs from user devices. This means you can reproduce any error on any device, without requiring direct access or proximity to the device that’s having issues.

Bugfender serves just this purpose. It’s (we like to think) a powerful remote logging platform that allows you to collect logs from your users’ devices in real-time, helping you diagnose and fix issues that are difficult to reproduce locally.

What is Remote Logging?

Remote logging involves sending log data from a user’s device to a centralized server. This allows developers to:

  1. Monitor app behavior across multiple devices
  2. Diagnose issues in production environments
  3. Gather usage statistics and crash reports
  4. Provide better customer support

Getting Started with Bugfender

Installation

To add Bugfender to your project using Swift Package Manager:

  1. In Xcode, go to File > Swift Packages > Add Package Dependency
  2. Enter the Bugfender repository URL: https://github.com/bugfender/BugfenderSDK-iOS
  3. Follow the prompts to add the package to your project

Configuration

In your AppDelegate, import and configure Bugfender:

import BugfenderSDK

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        Bugfender.activateLogger("YOUR_APP_KEY")
        Bugfender.enableNSLogLogging() // Capture console logs
        Bugfender.enableUIEventLogging() // Log user interactions
        Bugfender.enableCrashReporting() // Log crashes automatically

        return true
    }
}

Enter fullscreen mode Exit fullscreen mode

Basic Logging with Bugfender

Bugfender provides simple methods for logging at different levels:

Bugfender.print("This is a regular log message")
Bugfender.warning("This is a warning message")
Bugfender.error("This is an error message")

Enter fullscreen mode Exit fullscreen mode

Advanced Bugfender Features

Device-Associated Data

Associate custom data with devices for easier filtering and analysis:

Bugfender.setDeviceString("Premium", forKey: "userTier")
Bugfender.setDeviceInteger(42, forKey: "userAge")
Bugfender.setDeviceBOOL(true, forKey: "isPremiumUser")

// Advanced: Logging user actions with associated data
func logUserAction(_ action: String, details: [String: Any]) {
    Bugfender.print("User Action: \(action)")
    for (key, value) in details {
        Bugfender.setDeviceString("\(value)", forKey: "action_\(action)_\(key)")
    }
}

// Usage
logUserAction("purchase", details: ["item": "Premium Subscription", "price": 9.99, "currency": "USD"])

Enter fullscreen mode Exit fullscreen mode

Sending Issues

For important events or errors, you can send issues with additional context:

func sendDetailedIssue(title: String, description: String, context: [String: Any]) {
    var fullDescription = description + "Context:\n"
    for (key, value) in context {
        fullDescription += "- \(key): \(value)\n"
    }

    let issueUrl = Bugfender.sendIssueReturningUrl(withTitle: title, text: fullDescription)
    if let url = issueUrl {
        print("Issue created. View it at: \(url.absoluteString)")
    }
}

// Usage
sendDetailedIssue(
    title: "Payment Failed",
    description: "User encountered an error during checkout process",
    context: [
        "User ID": 12345,
        "Payment Method": "Credit Card",
        "Amount": "$59.99",
        "Error Code": "PF001"
    ]
)

Enter fullscreen mode Exit fullscreen mode

User Feedback

It’s easy to implement a user feedback system with custom UI and handling:

class FeedbackViewController: UIViewController {
    @IBOutlet weak var subjectField: UITextField!
    @IBOutlet weak var messageTextView: UITextView!

    func sendFeedback() {
        guard let subject = subjectField.text, !subject.isEmpty,
              let message = messageTextView.text, !message.isEmpty else {
            // Show an alert to the user
            return
        }

        let feedbackUrl = Bugfender.sendUserFeedbackReturningUrl(withSubject: subject, message: message)
        if let url = feedbackUrl {
            print("Feedback sent! View it at: \(url.absoluteString)")
            // Show a success message to the user
        } else {
            // Show an error message to the user
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Customizing Log Collection

Here’s an advanced example of a custom log interceptor that filters logs based on multiple criteria:

class AdvancedLogInterceptor: NSObject, BFLogInterceptor {
    func intercept(_ interceptedLog: BFInterceptedLog) -> BFInterceptedLog? {
        // Only send logs that are errors, warnings, or contain specific keywords
        if interceptedLog.level == .error || interceptedLog.level == .warning {
            return interceptedLog
        }

        let importantKeywords = ["payment", "login", "logout", "crash"]
        if importantKeywords.contains(where: interceptedLog.text.lowercased().contains) {
            return interceptedLog
        }

        // Don't send logs from specific subsystems
        let ignoredSubsystems = ["com.thirdparty.analytics", "com.thirdparty.ads"]
        if let subsystem = interceptedLog.subsystem, ignoredSubsystems.contains(subsystem) {
            return nil
        }

        // Default: don't send the log
        return nil
    }
}

// In AppDelegate:
let interceptor = AdvancedLogInterceptor()
Bugfender.enableNSLogLogging(withInterceptor: interceptor)

Enter fullscreen mode Exit fullscreen mode

Integrating with Other Tools

Bugfender provides methods to get URLs for the current session and device, which can be useful for integrating with other tools:

if let sessionUrl = Bugfender.sessionIdentifierUrl() {
    print("Current session URL: \(sessionUrl.absoluteString)")
}

if let deviceUrl = Bugfender.deviceIdentifierUrl() {
    print("Current device URL: \(deviceUrl.absoluteString)")
}

Enter fullscreen mode Exit fullscreen mode

Handling Crashes

Bugfender can automatically collect crash reports, but you can also manually send crash information:

Bugfender.sendCrash(withTitle: "Unexpected Crash", text: "The app crashed while processing user data")

Enter fullscreen mode Exit fullscreen mode

Best Practices for Using Bugfender

Here are some general rules of thumb that our customers have found useful in the past.

  1. Use appropriate log levels (print, warning, error) to categorize your logs effectively.
  2. Leverage device-associated data for easier filtering and analysis.
  3. Implement user feedback to gather valuable insights directly from your users.
  4. Use custom log interceptors to control which logs are sent to Bugfender, especially in production environments.
  5. Regularly review your Bugfender dashboard to identify trends and issues.

Conclusions

Effective logging is a crucial skill for any Swift developer. Whether you’re using Apple’s Unified Logging System, a third-party framework like CocoaLumberjack, or a remote logging solution like Bugfender, understanding how to log effectively can significantly improve your development process.

By mastering logging techniques, you’ll be better equipped to debug issues, monitor your app’s performance, and perhaps most importantly, understand the people who are using your app. What they’re like, what they want, and where they’re encountering problems using your product.

As you continue to develop your Swift applications, remember that good logging practices are an investment in the quality and maintainability of your code. Logging isn’t just a tack-on; it’s part of the build process. And, when done properly, it can be one of the most rewarding parts, too.

Top comments (0)