I’ve written SwiftUI code that uses Combine to display CPU usage metrics such as User
, System
, Nice
, and Idle
values for each core on the system. The functionality works initially in the sense that when the app launches, the usage data is correctly fetched and displayed.
However, I’ve encountered a problem: after the initial data is displayed, the values do not update in real-time as I expected. The Combine publisher seems to stop triggering updates after the first fetch, and no new data is reflected in the UI.
I'm looking for guidance on how to make the values refresh continuously or at regular intervals. Perhaps I'm missing something in the way Combine is set up, or maybe there's a more effective approach for keeping the UI updated with real-time data.
Any suggestions or insights would be greatly appreciated. Thanks in advance!
Here's my code:
import SwiftUI
import Combine
struct ContentView: View {
@StateObject private var cpuMonitor = CPUUsageMonitor()
var body: some View {
ScrollView {
VStack {
Text("Overall CPU Usage")
.font(.largeTitle)
.padding()
ForEach(0..<cpuMonitor.cpuUsages.count, id: \.self) { index in
VStack(alignment: .leading) {
Text("Core \(index):")
.font(.headline)
HStack {
Text("User: ")
Text("\(cpuMonitor.cpuUsages[index].user, specifier: "%.2f")%")
}
HStack {
Text("System: ")
Text("\(cpuMonitor.cpuUsages[index].system, specifier: "%.2f")%")
}
HStack {
Text("Nice: ")
Text("\(cpuMonitor.cpuUsages[index].nice, specifier: "%.2f")%")
}
HStack {
Text("Idle: ")
Text("\(cpuMonitor.cpuUsages[index].idle, specifier: "%.2f")%")
}
}
.padding()
}
}
}
.onAppear {
cpuMonitor.startMonitoring()
}
.onDisappear {
cpuMonitor.stopMonitoring()
}
}
}
class CPUUsageMonitor: ObservableObject {
@Published var cpuUsages: [CoreUsage] = []
private var cpuInfo: processor_info_array_t!
private var prevCpuInfo: processor_info_array_t?
private var numCpuInfo: mach_msg_type_number_t = 0
private var numPrevCpuInfo: mach_msg_type_number_t = 0
private var numCPUs: uint = 0
private var timerCancellable: AnyCancellable?
init() {
let mibKeys: [Int32] = [CTL_HW, HW_NCPU]
mibKeys.withUnsafeBufferPointer { mib in
var sizeOfNumCPUs: size_t = MemoryLayout<uint>.size
let status = sysctl(processor_info_array_t(mutating: mib.baseAddress), 2, &numCPUs, &sizeOfNumCPUs, nil, 0)
if status != 0 {
numCPUs = 1
}
}
cpuUsages = Array(repeating: CoreUsage(user: 0.0, system: 0.0, nice: 0.0, idle: 0.0), count: Int(numCPUs))
}
func startMonitoring() {
timerCancellable = Timer.publish(every: 1, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.updateInfo()
}
}
func stopMonitoring() {
timerCancellable?.cancel()
}
func updateInfo() {
var numCPUsU: natural_t = 0
var cpuLoadInfo: processor_info_array_t!
var cpuLoadInfoCount: mach_msg_type_number_t = 0
let err: kern_return_t = host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO, &numCPUsU, &cpuLoadInfo, &cpuLoadInfoCount)
if err == KERN_SUCCESS {
var updatedUsages: [CoreUsage] = []
for i in 0..<Int32(numCPUs) {
let userTicks = cpuLoadInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
let systemTicks = cpuLoadInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
let niceTicks = cpuLoadInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
let idleTicks = cpuLoadInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]
let total = userTicks + systemTicks + niceTicks + idleTicks
let userUsage = Float(userTicks) / Float(total) * 100
let systemUsage = Float(systemTicks) / Float(total) * 100
let niceUsage = Float(niceTicks) / Float(total) * 100
let idleUsage = Float(idleTicks) / Float(total) * 100
let coreUsage = CoreUsage(user: userUsage, system: systemUsage, nice: niceUsage, idle: idleUsage)
updatedUsages.append(coreUsage)
}
DispatchQueue.main.async {
self.cpuUsages = updatedUsages
}
// Deallocate previous CPU info
if let prevCpuInfo = prevCpuInfo {
let prevCpuInfoSize: size_t = MemoryLayout<integer_t>.stride * Int(numPrevCpuInfo)
vm_deallocate(mach_task_self_, vm_address_t(bitPattern: prevCpuInfo), vm_size_t(prevCpuInfoSize))
}
// Store the current info for future deallocation
prevCpuInfo = cpuLoadInfo
numPrevCpuInfo = cpuLoadInfoCount
} else {
print("Error gathering CPU info!")
}
}
}
struct CoreUsage {
var user: Float
var system: Float
var nice: Float
var idle: Float
}
#Preview {
ContentView()
}
Your calculation of the ticks
is incorrect, that's why you're getting wrong values. You have to factor in the prevCpuInfo
properly:
if let prevCpuInfo {
for i in 0..<Int32(numCPUs) {
let userTicks = cpuLoadInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)] - prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_USER)]
let systemTicks = cpuLoadInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)] - prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_SYSTEM)]
let niceTicks = cpuLoadInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)] - prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_NICE)]
let idleTicks = cpuLoadInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)] - prevCpuInfo[Int(CPU_STATE_MAX * i + CPU_STATE_IDLE)]
let total = userTicks + systemTicks + niceTicks + idleTicks
let userUsage = Float(userTicks) / Float(total) * 100
let systemUsage = Float(systemTicks) / Float(total) * 100
let niceUsage = Float(niceTicks) / Float(total) * 100
let idleUsage = Float(idleTicks) / Float(total) * 100
let coreUsage = CoreUsage(user: userUsage, system: systemUsage, nice: niceUsage, idle: idleUsage)
updatedUsages.append(coreUsage)
}
DispatchQueue.main.async {
self.cpuUsages = updatedUsages
}
}
The Combine publisher seems to stop triggering updates after the first fetch, and no new data is reflected in the UI.
I could not reproduce this and your code does not seem to have any errors in this regard.