5

I am building a Swift app that monitors the battery percentage, as well as the charging state, of a Mac laptop's battery. On iOS, there is a batteryLevelDidChange notification that is sent when the device's battery percentage changes, as well as a batteryStateDidChange notification that is sent when the device is plugged in, unplugged, and fully charged.

What is the macOS equivalent of those two notifications in Swift, or more specifically, for kIOPSCurrentCapacityKey and kIOPSIsChargingKey? I read through the notification documentation and didn't see any notifications for either. Here is the code I have for fetching the current battery charge level and charging status:

import Cocoa
import IOKit.ps

class MainViewController: NSViewController {

enum BatteryError: Error { case error }

func getMacBatteryPercent() {

    do {
        guard let snapshot = IOPSCopyPowerSourcesInfo()?.takeRetainedValue()
            else { throw BatteryError.error }

        guard let sources: NSArray = IOPSCopyPowerSourcesList(snapshot)?.takeRetainedValue()
            else { throw BatteryError.error }

        for powerSource in sources {
            guard let info: NSDictionary = IOPSGetPowerSourceDescription(snapshot, ps as CFTypeRef)?.takeUnretainedValue()
                else { throw BatteryError.error }

            if let name = info[kIOPSNameKey] as? String,
                let state = info[kIOPSIsChargingKey] as? Bool,
                let capacity = info[kIOPSCurrentCapacityKey] as? Int,
                let max = info[kIOPSMaxCapacityKey] as? Int {
                print("\(name): \(capacity) of \(max), \(state)")
            }
        }
    } catch {
        print("Unable to get mac battery percent.")
    }
}

override func viewDidLoad() {
    super.viewDidLoad() 

    getMacBatteryPercent()
}
}
TonyStark4ever
  • 848
  • 1
  • 9
  • 24

2 Answers2

4

(I'm replying to this almost 3-year-old question as it is the third result that comes up on the Google search "swift iokit notification".)

The functions you're looking for are IOPSNotificationCreateRunLoopSource and IOPSCreateLimitedPowerNotification.

Simplest usage of IOPSNotificationCreateRunLoopSource:

import IOKit

let loop = IOPSNotificationCreateRunLoopSource({ _ in
    // Perform usual battery status fetching
}, nil).takeRetainedValue() as CFRunLoopSource
CFRunLoopAddSource(CFRunLoopGetCurrent(), loop, .defaultMode)

Note that the second parameter context is passed as the only parameter in the callback function, which can be used to pass the instance as a pointer to the closure since C functions do not capture context. (See the link below for actual implementation.)

Here is my code that converts the C-style API into a more Swift-friendly one using the observer pattern: (don't know how much performance benefit it will has for removing run loops)

import Cocoa
import IOKit

// Swift doesn't support nested protocol(?!)
protocol BatteryInfoObserverProtocol: AnyObject {
    func batteryInfo(didChange info: BatteryInfo)
}

class BatteryInfo {
    typealias ObserverProtocol = BatteryInfoObserverProtocol
    struct Observation {
        weak var observer: ObserverProtocol?
    }
    
    static let shared = BatteryInfo()
    private init() {}
    
    private var notificationSource: CFRunLoopSource?
    var observers = [ObjectIdentifier: Observation]()
    
    private func startNotificationSource() {
        if notificationSource != nil {
            stopNotificationSource()
        }
        notificationSource = IOPSNotificationCreateRunLoopSource({ _ in
            BatteryInfo.shared.observers.forEach { (_, value) in
                value.observer?.batteryInfo(didChange: BatteryInfo.shared)
            }
        }, nil).takeRetainedValue() as CFRunLoopSource
        CFRunLoopAddSource(CFRunLoopGetCurrent(), notificationSource, .defaultMode)
    }
    private func stopNotificationSource() {
        guard let loop = notificationSource else { return }
        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), loop, .defaultMode)
    }
    
    func addObserver(_ observer: ObserverProtocol) {
        if observers.count == 0 {
            startNotificationSource()
        }
        observers[ObjectIdentifier(observer)] = Observation(observer: observer)
    }
    func removeObserver(_ observer: ObserverProtocol) {
        observers.removeValue(forKey: ObjectIdentifier(observer))
        if observers.count == 0 {
            stopNotificationSource()
        }
    }
    
    // Functions for retrieving different properties in the battery description...
}

Usage:

class MyBatteryObserver: BatteryInfo.ObserverProtocol {
    init() {
        BatteryInfo.shared.addObserver(self)
    }
    deinit {
        BatteryInfo.shared.removeObserver(self)
    }
    
    func batteryInfo(didChange info: BatteryInfo) {
        print("Changed")
    }
}

Credits to this post and Koen.'s answer.

KeroppiMomo
  • 564
  • 6
  • 18
2

I'd Use this link to get the percentage (looks cleaner) Fetch the battery status of my MacBook with Swift

And to find changes in the state, use a timer to re-declare your battery state every 5 seconds and then set it as a new variable var OldBattery:Int re-declare it once again and set it as NewBattery, then, write this code:

if (OldBattery =! NewBattery) {
      print("battery changed!")
      // write the function you want to happen here
}
Sceptual
  • 53
  • 7
  • You don’t want to use a `Timer` since that approach would be impact performance. Use `IOPSNotificationCreateRunLoopSource` instead → https://developer.apple.com/documentation/iokit/1523868-iopsnotificationcreaterunloopsou – ixany Jul 08 '22 at 06:44