Swift and SwiftUI tutorials for Swift Developers

How to print in Xcode console with SwiftUI

If you ask any iOS developer what tool they use most in their daily life, you might expect answers like “Instruments”, “Core Data”, or “Combine”. However, the reality of Swift programming is much more pragmatic: the most used tool, for better or worse, is the humble print() function.

In classic imperative development (UIKit), printing to the console was trivial. You placed a print("Hello") in viewDidLoad and that was it. But with the arrival of SwiftUI and its declarative paradigm, the rules of the game have changed. You can no longer simply drop arbitrary code in the middle of building a view.

In this tutorial, we will explore in depth how to print to the Xcode console using SwiftUI, from the most basic tricks to advanced OSLog techniques for professional applications on iOS, macOS, and watchOS.


The Challenge of Printing in SwiftUI

To understand why it is sometimes confusing to print data in SwiftUI, we must first understand how the framework works.

In SwiftUI, the body property of a View is not a normal function; it is a computed property that returns some View. Inside this block, Xcode expects to receive a View structure, not code execution instructions.

var body: some View {
    // This will cause a compilation error:
    print("Trying to render the view") 
    
    return Text("Hello World")
}

The Swift compiler will yell at you because print returns Void (empty), and ViewBuilder expects visual components. So, how do we know what is happening inside our apps?


1. The Classic: .onAppear and .onDisappear

The most “official” and safe way to print to the console when a view enters the scene is by using lifecycle modifiers.

For an iOS developer, understanding the lifecycle is crucial. Although SwiftUI abstracts much of this, we still need to know when something is shown on the screen.

Implementation

struct DebugView: View {
    let name: String
    
    var body: some View {
        Text("Screen of: \(name)")
            .onAppear {
                print("🔵 [Lifecycle] View \(name) has appeared.")
            }
            .onDisappear {
                print("🔴 [Lifecycle] View \(name) has disappeared.")
            }
    }
}

Pros:

  • It is valid and safe Swift code.
  • It does not affect rendering performance.
  • Works perfectly on watchOS, macOS, and iOS.

Cons:

  • It only runs once when the view appears. If the state changes and the view redraws (re-renders), onAppear will not necessarily run again.

2. The “Side Effect” Trick: Printing inside the ViewBuilder

Sometimes, you need to know if the view body is being re-evaluated, not just when it appears. As expert Swift programming developers, we know that the compiler ignores assignments to anonymous or discarded variables within certain contexts.

We can “hack” the ViewBuilder by executing a function that returns an empty View or simply executing the print and returning control to the flow.

Option A: The let _ = extension

Inside a Swift block, you can make an assignment to _ (underscore) to execute code.

var body: some View {
    VStack {
        let _ = print("⚡️ The view body is being evaluated")
        
        Text("Main Content")
    }
}

Note: This works in recent versions of Swift, but it can sometimes be unstable if the compiler decides to optimize the code.

Option B: A Custom Modifier (The Elegant Way)

To keep our SwiftUI code clean and readable, it is best to create an extension. This demonstrates a senior iOS developer level.

extension View {
    func debugPrint(_ vars: Any...) -> some View {
        for v in vars { print(v) }
        return self
    }
}

// Usage:
Text("Hello")
    .debugPrint("The text is rendering")

However, this has a problem: it returns self, so it runs when building the view, but it doesn’t guarantee it happens on every state update in the way you might expect.


3. Monitoring State with onChange

The number one reason we want to print to the Xcode console is to see how our data changes. “Why isn’t my @State variable updating the UI?” is the million-dollar question.

Since iOS 14 (and improved in iOS 17), we have the .onChange modifier.

Modern Syntax (iOS 17+)

Swift programming evolves fast. The new syntax allows us to get the old value and the new value.

struct CounterView: View {
    @State private var count = 0
    
    var body: some View {
        Button("Increment: \(count)") {
            count += 1
        }
        .onChange(of: count) { oldValue, newValue in
            print("📈 Change detected: \(oldValue) -> \(newValue)")
        }
    }
}

This is the definitive method for debugging business logic at the view layer. It works excellently for @State, @Binding, and @Environment.


4. The Secret Weapon: Self._printChanges()

This is a hidden gem that Apple quietly introduced and that every iOS developer should know. It is a static method available on any SwiftUI view that tells you exactly which property caused the view to redraw.

Have you ever felt that your app is slow and suspect that SwiftUI is unnecessarily redrawing the entire screen?

Simply call this method inside the body:

struct PerformanceView: View {
    @State private var text = ""
    
    var body: some View {
        let _ = Self._printChanges()
        
        TextField("Type something", text: $text)
    }
}

Output in the Xcode console:

PerformanceView: @self, @identity, _text changed.

This will tell you if the change was triggered by a state variable (_text), by the environment, or by a change in view identity. It is vital for optimization on watchOS, where resources are limited.


5. Professional Debugging: OSLog and Logger

The print() command is useful, but it has serious issues:

  1. It is slow.
  2. It appears in the user’s device console if you aren’t careful.
  3. It cannot be easily filtered in the Xcode console.

The current standard for Swift programming in production environments is the Unified Logging System (OSLog).

Importing the framework

import OSLog

Creating a Logger

Ideally, create an extension to have global access to your logs. This allows you to categorize messages (e.g., “Network”, “UI”, “Database”).

extension Logger {
    private static var subsystem = Bundle.main.bundleIdentifier!
    
    static let ui = Logger(subsystem: subsystem, category: "UI")
    static let network = Logger(subsystem: subsystem, category: "Network")
}

Using it in SwiftUI

struct ProDebuggingView: View {
    var body: some View {
        Button("Download Data") {
            Logger.ui.info("Button pressed")
            
            // Network simulation
            Logger.network.notice("Starting API call...")
        }
    }
}

Why use Logger instead of Print?

  1. Severity Levels: You can mark messages as .debug, .info, .notice, .error, or .fault.
  2. Persistence: Errors (.fault) are saved on the device and can be retrieved later, even if you weren’t connected to Xcode.
  3. Privacy: OSLog automatically hides sensitive data (like passwords) in the console unless you specify otherwise.
  4. Filtering in Xcode: At the bottom of the console, you can type “category:Network” and you will see only your network logs, eliminating system noise.

6. Visual Debugging: When Text is Not Enough

Sometimes, printing to the console doesn’t give you the full picture, especially when working with complex layouts in SwiftUI.

Random Background Color

A classic iOS developer trick is to color backgrounds to see view frames.

extension View {
    func debugBackground() -> some View {
        self.background(Color(
            red: .random(in: 0...1),
            green: .random(in: 0...1),
            blue: .random(in: 0...1),
            opacity: 0.5
        ))
    }
}

Use it on your VStacks or HStacks to see exactly how much space they occupy.

Preview Canvas Debugging

In Xcode, don’t forget that you can use the “Play” button in the Canvas and right-click on preview elements to inspect their attributes. Although it’s not “printing to console,” it is a way of “printing visual information.”


7. Debugging on Different Platforms (iOS, macOS, watchOS)

The beauty of SwiftUI lies in its cross-platform capability. Everything we’ve seen (print, onChange, OSLog) works exactly the same on:

  • iOS: Standard development for iPhone and iPad.
  • macOS: When developing for Mac, the Xcode console is your best friend, but you can also use the native macOS “Console” app to see OSLog logs in real-time without having Xcode open.
  • watchOS: Debugging is critical here. print can have significant lag due to the wireless connection with the debugger. It is highly recommended to use OSLog on watchOS to avoid performance bottlenecks on the watch.

8. Breakpoints: Printing Without Writing Code

Did you know you don’t need to clutter your code with print() to print to the console? Xcode has a powerful feature called Breakpoints with Actions.

  1. Click on the line number where you want to debug.
  2. Right-click on the blue indicator (the breakpoint) and select “Edit Breakpoint”.
  3. Click “Add Action” and select “Log Message”.
  4. Write your message. You can interpolate values with @variable@.
  5. Check the box “Automatically continue after evaluating actions”.

Now, every time the code passes through there, it will print your message to the console and keep running without pausing the app. It’s magic! And the best part: you don’t run the risk of forgetting to delete print statements before uploading the app to the App Store.


Conclusion: You Are What You Debug

Mastering the Xcode console is what separates a student from a senior iOS developer. While print() is quick and easy, tools like _printChanges() and OSLog give you full control over what happens under the hood of your SwiftUI applications.

Next time your view doesn’t update or your app crashes, don’t just guess. Use these techniques to interrogate your code. Programming in Swift is logic, and the console is the window into that logic.

Tool Summary:

  1. print(): Quick, dirty, for temporary use.
  2. .onAppear / .onChange: To follow data flow and lifecycle.
  3. Self._printChanges(): To optimize view redrawing.
  4. OSLog / Logger: For professional systems, persistence, and filtering.
  5. Breakpoints: To debug without modifying source code.

If you have any questions about this article, please contact me and I will be happy to help you 🙂. You can contact me on my X profile or on my Instagram profile.

Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Article

Create a game with SpriteKit and SwiftUI

Next Article

How to add a Toolbar in SwiftUI

Related Posts