1 minute read

How to access app logs from UI Tests on iOS

There is no secret that the UI tests are pretty limited in terms of accessing app internals. That’s why sometimes you have to craft some hackish workarounds to bypass these limitations.

The app logs are one of those things that would be quite useful to have access to from UI tests to observe any events that happen in the app or whatnot.

Even though there is nothing out of the box that could help us, let’s see how we can achieve this after all.

Sample app

As an example, I created a simple app that has an image view and logs a message when it’s tapped:

import SwiftUI

struct ContentView: View {

    var body: some View {
        VStack {
            VStack {
                Image(systemName: "photo")
                    .font(.system(size: 150))
                    .accessibilityIdentifier("imageView")
                    .onTapGesture {
                        print("Image was tapped!")
                    }
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.brown)
    }
}

@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

That’s how our picturesque sample app looks like:

Preview

LogView injection

To access the logs from UI tests, we need to inject a view that’ll display them for us. But don’t worry:

  • it will be there only in debug builds
  • it will be completely transparent
  • so basically, nothing changes in UI, the tree hierarchy is where the magic happens
struct ContentView: View {
    @State private var log: String = ""

    var body: some View {
        VStack {
            VStack {
                Image(systemName: "photo")
                    .font(.system(size: 150))
                    .accessibilityIdentifier("imageView")
                    .onTapGesture {
                        let event = "Image was tapped!"
                        print(event)
                        log += event
                    }
                    #if DEBUG
                    .overlay {
                        Text(log)
                            .accessibilityIdentifier("logView")
                            .foregroundColor(.clear)
                    }
                    #endif
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(Color.brown)
    }
}

Sample test

Now, let’s access the logs from the sample UI test and assert that the event was logged:

import XCTest

final class SampleUITests: XCTestCase {

    func testExample() throws {
        let app = XCUIApplication()
        app.launch()

        app.images["imageView"].tap()
        XCTAssertTrue(app.staticTexts["logView"].label.contains("Image was tapped!"))
    }
}

Finally, that’s how the tree hierarchy snapshot looks like:

Element subtree:
 Application, 0x153d0e440, pid: 61338, label: 'SampleApp'
    Window (Main), 0x153d10f20, ((0.0, 0.0), (393.0, 852.0))
      Other, 0x153d0aa00, ((0.0, 0.0), (393.0, 852.0))
        Image, 0x153d07220, ((110.0, 371.0), (173.0, 135.0)), identifier: 'imageView', label: 'Photo'
        StaticText, 0x153d07340, ((124.5, 417.3), (144.0, 42.3)), identifier: 'logView', label: 'Image was tapped!'

Afterword

Hope you got the idea. Making the LogView available across all app screens requires some boilerplate code or some smart architecture, so I’ll leave it up to you. Happy hacking! 🤠

Updated: