listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

FPSOverlay.swift (2025B)


      1 import SwiftUI
      2 import UIKit
      3 
      4 struct FPSOverlay: View {
      5     @State private var fps: Int = 0
      6     @State private var targetFPS: Int = 0
      7     @State private var monitor = FPSMonitor()
      8 
      9     var body: some View {
     10         Text("\(fps) / \(targetFPS) FPS")
     11             .font(.system(size: 12, weight: .bold, design: .monospaced))
     12             .foregroundStyle(fps >= 55 ? .green : fps >= 45 ? .yellow : .red)
     13             .padding(.horizontal, 8)
     14             .padding(.vertical, 4)
     15             .background(.black.opacity(0.7), in: Capsule())
     16             .onAppear {
     17                 monitor.start { actual, target in
     18                     fps = actual
     19                     targetFPS = target
     20                 }
     21             }
     22             .onDisappear {
     23                 monitor.stop()
     24             }
     25     }
     26 }
     27 
     28 @MainActor
     29 private class FPSMonitor {
     30     private var displayLink: CADisplayLink?
     31     private var lastTimestamp: CFTimeInterval = 0
     32     private var frameCount: Int = 0
     33     private var lastTargetFPS: Int = 0
     34     private var onUpdate: ((Int, Int) -> Void)?
     35 
     36     func start(onUpdate: @escaping (Int, Int) -> Void) {
     37         self.onUpdate = onUpdate
     38         let link = CADisplayLink(target: self, selector: #selector(tick))
     39         link.preferredFrameRateRange = CAFrameRateRange(minimum: 80, maximum: 120, preferred: 120)
     40         link.add(to: .main, forMode: .common)
     41         displayLink = link
     42     }
     43 
     44     func stop() {
     45         displayLink?.invalidate()
     46         displayLink = nil
     47     }
     48 
     49     @objc private func tick(_ link: CADisplayLink) {
     50         let interval = link.targetTimestamp - link.timestamp
     51         if interval > 0 {
     52             lastTargetFPS = Int(round(1.0 / interval))
     53         }
     54         if lastTimestamp == 0 {
     55             lastTimestamp = link.timestamp
     56             return
     57         }
     58         frameCount += 1
     59         let elapsed = link.timestamp - lastTimestamp
     60         if elapsed >= 1.0 {
     61             onUpdate?(frameCount, lastTargetFPS)
     62             frameCount = 0
     63             lastTimestamp = link.timestamp
     64         }
     65     }
     66 }