The ability to log values and visualize them in dataFude is quite powerful, but the core concept of the app really shines when a developer automates logs that drive metrics over time.

Such metrics could be any value that changes over time, such as the size of the app cache, the amount of RAM consumed, or the number of instances created of a given class.

This article will look at two quite different approaches to tracking automation, one with SwiftUI and another with UIKit. Let’s start with SwiftUI first.

SwiftUI modifier to track view rendering

In this article we’ll look at logging the number of times a SwiftUI view is re-created. This is useful when you’re trying to track down unexpected state changes that cause your SwiftUI views to be recreated.

In its essence the view modifier will be a counter that will increment any time SwiftUI calls it during creating the view.

Let’s start by creating a serial queue called debugRenderQueue to do the logging and a dictionary to keep all the counters:

1
2
3
4
private let debugRenderQueue = DispatchQueue(
  label: "debugRenderQueue"
)
private var debugRenderCounts = [String: Int]()

Next, let’s add a new method on View called debugRender(...):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
extension View {
  func debugRender(named id: String) -> some View {

    debugRenderQueue.async {
      debugRenderCounts[id, default: 0] += 1
      // Log the current count to the unified log
      os_log("\(id): \(debugRenderCounts[id]!)")
    }
    
    return self
  }
}

You can add this modifier to any view in code and it’ll keep count of redraws.

Let’s give that a try! Let’s look at a Timer-driven clock view in SwiftUI:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
struct ContentView: View {
  @State var timer = Timer.publish(every: 0.1, on: .main, in: .default)
    .autoconnect()

  @State var time = ""

  var body: some View {
    VStack {
      Text("Current time:")
      Text(time).bold()
    }
    .onReceive(timer) { d in
      time = d.formatted(date: .omitted, time: .standard)
    }
  }
}

The timer updates the time state property with the current time 10 times per second (the 0.1 cadence of the timer in the code above), and the view picks up the new state and displays the time:

Ticking clock

When it comes to improving your app’s performance, you might like to check whether the view is redrawn 10 times per second (as often as you update the time property) or just once when the time value actually changes.

This is where using the new modifier from above comes handy. If you add the debugRender() modifier you’ll see the updates in dataFude right away:

1
2
Text(time).bold()
  .debugRender(named: "Timer")
Ticking clock

And to make the data even easier to read — click the tile menu and select “View as chart” and then click on the tile subtitle to see how many logs per second it displays:

Ticking clock

And now you have your answer — the tile updates once per second, therefore SwiftUI only recreates the view when the state actually changes.

Automatically track view controllers’ state in UIKit

Now let’s switch gears and look at UIKit. UIKit classes are Objective-C types inheriting NSObject so logging automation is even easier thanks to the almost unlimited possibilities for swizzling.

Long story short, in a UIKit app you don’t need to add a modifier or a function call anywhere — you can, for example, automatically track the state of all view controllers.

You can write your own swizzling code but for this article I’ll use @steipete’s InterposeKit.

In the app’s AppDelegate.init() let’s add an IntreposeKit call to swizzle UIViewController.viewDidAppear(_) and UIViewController.viewDidDisappear(_). The code is a bit verbose but the two important lines are the ones getting the current controller’s name and logging true when presented and false when dismissed:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
_ = try! Interpose(UIViewController.self) {

  // Swizzle viewDidAppear
  try $0.prepareHook(
    #selector(UIViewController.viewDidAppear(_:)),
    methodSignature: (@convention(c) (AnyObject, Selector) -> Void).self,
    hookSignature: (@convention(block) (AnyObject) -> Void).self) {
      store in { `self` in

        // Here we write to the log
        let name = NSStringFromClass(self.classForCoder)
          .components(separatedBy: ".").last!
        os_log("\(name): true")
        store.original(`self`, store.selector)
      }
  }

  // Swizzle viewDidDisappear
  try $0.prepareHook(
    #selector(UIViewController.viewDidDisappear(_:)),
    methodSignature: (@convention(c) (AnyObject, Selector) -> Void).self,
    hookSignature: (@convention(block) (AnyObject) -> Void).self) {
      store in { `self` in
      
        // Here we write to the log
        let name = NSStringFromClass(self.classForCoder)
          .components(separatedBy: ".").last!
        os_log("\(name): false")
        store.original(`self`, store.selector)
      }
  }
}

The code injects couple of new lines into UIViewController.viewDidAppear(...) and UIViewController.viewDidDisappear(...) that get the current class name and write to the log something like:

MainViewController: true
MainViewController: false
DetailsViewController: true
DetailsViewController: false
...

Let’s give this code a try with a test app that presents two different view controllers — one for “details” and one for “more information”.

There’s no need to add any more code than the block above which, as said, tracks the presentation state of any of your view controllers:

Ticking clock

You can track as many view controllers as you’d like this way — you’re only limited by how many tiles fit on your screen.

This kind of automation can be very useful for example to verify that all instances of an object have been released from memory. Or when tracking the state of a whole class of objects like with the view controllers above.

What other SwiftUI view modifiers or UIKit extensions that talk to dataFude would you like to use? Get in touch at https://twitter.com/icanzilb or https://mastodon.social/@icanzilb.