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 }