SwiftUI Approach
Spent a days worth of work doing this as I'm new to Swift but I've come up with a solution that:
A: can read out the volume
B: works even if volume is at max or min
C: probably is customizable to your liking
struct VolumeEventReader<Content: View>: UIViewControllerRepresentable {
let builder: (Float) -> Content
class Coordinator: NSObject {
var parent: VolumeEventReader
var lastVolumeNotificationSequenceNumber: Int?
var currentVolume = AVAudioSession.sharedInstance().outputVolume
init(_ parent: VolumeEventReader) {
self.parent = parent
}
@objc func volumeChanged(_ notification: NSNotification) {
DispatchQueue.main.async { [self] in
volumeControlIOS15(notification)
}
}
func manageVolume(volume: Float, minVolume: Float) {
switch volume {
case minVolume: do {
currentVolume = minVolume + 0.0625
}
case 1: do {
currentVolume = 0.9375
}
default: break
}
if volume > currentVolume {
// Volume up
}
if volume < currentVolume {
// Volume down
}
parent.updateUIView(volume: volume)
currentVolume = volume
}
func volumeControlIOS15(_ notification: NSNotification) {
let minVolume: Float = 0.0625
if let volume = notification.userInfo!["Volume"] as? Float {
//avoiding duplicate events if same ID notification was generated
if let seqN = self.lastVolumeNotificationSequenceNumber {
if seqN == notification.userInfo!["SequenceNumber"] as! Int {
// Duplicate nofification received
}
else {
self.lastVolumeNotificationSequenceNumber = (notification.userInfo!["SequenceNumber"] as! Int)
manageVolume(volume: volume, minVolume: minVolume)
}
}
else {
self.lastVolumeNotificationSequenceNumber = (notification.userInfo!["SequenceNumber"] as! Int)
manageVolume(volume: volume, minVolume: minVolume)
}
}
}
}
let viewController = UIViewController()
func makeUIViewController(context: Context) -> UIViewController {
let volumeView = MPVolumeView(frame: CGRect.zero)
volumeView.isHidden = true
viewController.view.addSubview(volumeView)
let childView = UIHostingController(rootView: builder(AVAudioSession.sharedInstance().outputVolume))
addChildViewController(childView, to: viewController)
return viewController
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
private func addChildViewController(_ child: UIViewController, to parent: UIViewController) {
if parent.children.count > 0{
let viewControllers:[UIViewController] = parent.children
for viewContoller in viewControllers{
viewContoller.willMove(toParent: nil)
viewContoller.view.removeFromSuperview()
viewContoller.removeFromParent()
}
}
parent.addChild(child)
child.view.translatesAutoresizingMaskIntoConstraints = false
parent.view.addSubview(child.view)
child.didMove(toParent: parent)
child.view.backgroundColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0)
NSLayoutConstraint.activate([
child.view.leadingAnchor.constraint(equalTo: parent.view.leadingAnchor),
child.view.trailingAnchor.constraint(equalTo: parent.view.trailingAnchor),
child.view.topAnchor.constraint(equalTo: parent.view.topAnchor),
child.view.bottomAnchor.constraint(equalTo: parent.view.bottomAnchor)
])
}
func makeCoordinator() -> Coordinator {
let coordinator = Coordinator(self)
NotificationCenter.default.addObserver(
coordinator,
selector: #selector(Coordinator.volumeChanged(_:)),
name: NSNotification.Name(rawValue: "SystemVolumeDidChange"),
object: nil
)
return coordinator
}
func updateUIView(volume: Float) {
let childView = UIHostingController(rootView: builder(volume))
addChildViewController(childView, to: self.viewController)
}
}
This will give you a VolumeEventReader that can be used in Swift as such:
struct ContentView: View {
var body: some View {
VStack {
VolumeEventReader { volume in
VStack {
Text("Volume: \(volume)")
}
.onAppear {
print("\(volume)")
}
}
Text("Hello World")
}
}
}
Note: you can put any View inside VolumeEventReader I was just giving an example with VStack. It was inspired by GeometryReader.
Credit to answers that led me to this solution:
System Volume Change Observer not working on iOS 15
Observing system volume in SwiftUI