swiftmacosswiftuicpu-usagefoundation

How can I retrieve the CPU usage of my Mac in the form of a percentage and put that into an updating variable?


I'm coding a Mac app in SwiftUI 6.0.3 and Xcode 16. My Mac is up to date with macOS Sequoia 15.3.1. I'm trying to have a menu bar item that updates at an interval with the percentage of the CPU that I am using. This code returns no errors, and as far as I can tell, should work, but I must be missing something. When I run the app, instead of giving me a percentage, it just says "Calculating..." which is the default value of the cpuUsage variable.

import SwiftUI
import Foundation

@main
struct MenuBarApp: App {
    @State private var cpuUsage: String = "Calculating..."
    
    var body: some Scene {
        // Menu bar item
        MenuBarExtra("icon \(cpuUsage)") {
            // Option to quit app
            Button("Quit") {
                NSApp.terminate(nil)
            }
        }
    }
    
    // Starts repeating CPU monitoring function at an interval of 1 second
    func startCPUMonitoring() {
        Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            if let usage = getCPUUsage() {
                cpuUsage = String(format: "%.1f%%", usage)
            } else {
                cpuUsage = "N/A"
            }
        }
    }

    // Retrieves CPU usage as a percentage of the total
    func getCPUUsage() -> Double? {
        var size = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info_data_t>.size / MemoryLayout<integer_t>.size)
        var cpuLoad = host_cpu_load_info()
        let result = withUnsafeMutablePointer(to: &cpuLoad) {
            $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) {
                host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
            }
        }
        
        guard result == KERN_SUCCESS else {
            print("Error retrieving CPU load: \(result)")
            return nil
        }
        
        let user = Double(cpuLoad.cpu_ticks.0)
        let system = Double(cpuLoad.cpu_ticks.1)
        let idle = Double(cpuLoad.cpu_ticks.2)
        let nice = Double(cpuLoad.cpu_ticks.3)
        
        let totalTicks = user + system + idle + nice
        let cpuUsage = (user + system + nice) / totalTicks * 100.0
        
        return cpuUsage
    }
}

I've asked ChatGPT and went through the Apple Developer documentation but the problem is so niche that I can't find a single relatively recent source that discusses anything remotely similar. I think it's probably an issue with what function is being called where or the order of events or something, but I can't figure it out. Please help. I'm just trying to make this work on the latest version of macOS, it doesn't matter if it works for older versions.


Solution

  • Assuming your calculations are correct, try this approach using a Button("Start") to start monitoring, and a more precise String(format: "%.6f%%", usage) to show any differences in the value, as shown in this example code.

    Note you need to click on the "Start" button to display the results.

    @main
    struct MenuBarApp: App {
        @State private var cpuUsage: String = "Calculating..."
        
        var body: some Scene {
            // Menu bar item
            MenuBarExtra("icon \(cpuUsage)") {
                Button("Start") {
                    startCPUMonitoring()  // <--- here to start
                }
                Button("Quit") {
                    NSApp.terminate(nil)
                }
            }
        }
        
        // Starts repeating CPU monitoring function at an interval of 1 second
        func startCPUMonitoring() {
            Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                if let usage = getCPUUsage() {
                    cpuUsage = String(format: "%.6f%%", usage)  // <--- here
                } else {
                    cpuUsage = "N/A"
                }
            }
        }
    
        // Retrieves CPU usage as a percentage of the total
        func getCPUUsage() -> Double? {
            var size = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info_data_t>.size / MemoryLayout<integer_t>.size)
            var cpuLoad = host_cpu_load_info()
            let result = withUnsafeMutablePointer(to: &cpuLoad) {
                $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) {
                    host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
                }
            }
            
            guard result == KERN_SUCCESS else {
                print("Error retrieving CPU load: \(result)")
                return nil
            }
            
            let user = Double(cpuLoad.cpu_ticks.0)
            let system = Double(cpuLoad.cpu_ticks.1)
            let idle = Double(cpuLoad.cpu_ticks.2)
            let nice = Double(cpuLoad.cpu_ticks.3)
            
            let totalTicks = user + system + idle + nice
            let cpuUsage = (user + system + nice) / totalTicks * 100.0
            
            return cpuUsage
        }
    }
    

    EDIT-1:

    If you want to start the CPU monitoring immediately without having to click "Start", then try this approach using a @Observable class CPUMonitor. This will observe any changes in the cpuUsage and update the View/MenuBar.

    @Observable class CPUMonitor {
        var cpuUsage: String = "Calculating..."
        
        init() {
            startCPUMonitoring()  // <--- here
        }
        
        // Starts repeating CPU monitoring function at an interval of 1 second
        func startCPUMonitoring() {
            Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                if let usage = self.getCPUUsage() {
                    self.cpuUsage = String(format: "%.6f%%", usage)  // <--- here
                } else {
                    self.cpuUsage = "N/A"
                }
            }
        }
    
        // Retrieves CPU usage as a percentage of the total
        func getCPUUsage() -> Double? {
            var size = mach_msg_type_number_t(MemoryLayout<host_cpu_load_info_data_t>.size / MemoryLayout<integer_t>.size)
            var cpuLoad = host_cpu_load_info()
            let result = withUnsafeMutablePointer(to: &cpuLoad) {
                $0.withMemoryRebound(to: integer_t.self, capacity: Int(size)) {
                    host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, $0, &size)
                }
            }
            
            guard result == KERN_SUCCESS else {
                print("Error retrieving CPU load: \(result)")
                return nil
            }
            
            let user = Double(cpuLoad.cpu_ticks.0)
            let system = Double(cpuLoad.cpu_ticks.1)
            let idle = Double(cpuLoad.cpu_ticks.2)
            let nice = Double(cpuLoad.cpu_ticks.3)
            
            let totalTicks = user + system + idle + nice
            let cpuUsage = (user + system + nice) / totalTicks * 100.0
            
            return cpuUsage
        }
    }
    
    @main
    struct MenuBarApp: App {
        let cpuMonitor = CPUMonitor() // <--- here
        
        var body: some Scene {
            MenuBarExtra("icon \(cpuMonitor.cpuUsage)") {  // <--- here
                Button("Quit") {
                    NSApp.terminate(nil)
                }
            }
        }
    }
    

    Works well for me, tested on macOS 15.3.1, using Xcode 16.2.