Skip to content

Instantly share code, notes, and snippets.

@ObuchiYuki
Last active February 16, 2025 05:58
Show Gist options
  • Save ObuchiYuki/16477c5c59d778b1eb1ead687a0b8f0a to your computer and use it in GitHub Desktop.
Save ObuchiYuki/16477c5c59d778b1eb1ead687a0b8f0a to your computer and use it in GitHub Desktop.
SystemVolumeManager for SwiftUI
//
// SystemVolumeManager.swift
// SystemVolumeController
//
// Created by yuki on 2025/02/15.
//
import SwiftUI
import MediaPlayer
extension View {
public func systemVolumeController(showsSystemVolumeHUD: Bool = false) -> some View {
self.modifier(SystemVolumeModifier(showsSystemVolumeHUD: showsSystemVolumeHUD))
}
}
@MainActor
final public class SystemVolumeController: ObservableObject {
@Published public var volume: Double = 0 {
didSet {
guard self.volume != oldValue, !self.updateBySystem else { return }
assert(self.isActivated, "SystemVolumeController is not activated. This may cause an undefined behavior. Call activate() on .onAppear")
self.slider?.value = Float(self.volume)
}
}
public var showsSystemVolumeHUD: Bool {
get { self.volumeView.isHidden }
set { self.volumeView.isHidden = newValue }
}
public func activate() {
if self.isActivated { return }
self.isActivated = true
self.slider?.addTarget(self, action: #selector(volumeChanged(_:)), for: .valueChanged)
self.window?.addSubview(self.volumeView)
self.volume = Double(self.slider?.value ?? 0)
}
public func deactivate() {
if !self.isActivated { return }
self.isActivated = false
self.slider?.removeTarget(self, action: #selector(volumeChanged(_:)), for: .valueChanged)
self.volumeView.removeFromSuperview()
}
private var isActivated: Bool = false
private var updateBySystem = false
private var window: UIWindow?
private var slider: UISlider? {
self.volumeView.subviews.first { $0 is UISlider } as? UISlider
}
private let volumeView = MPVolumeView()
init() {
self.volumeView.frame = CGRect(origin: .zero, size: CGSize(width: 1, height: 1))
self.volumeView.showsVolumeSlider = true
self.volumeView.alpha = 0.000001 // Hide the view without using isHidden
}
fileprivate func hasRegisteredWindow() -> Bool {
self.window != nil
}
fileprivate func registerWindow(_ window: UIWindow) {
self.window = window
}
@objc private func volumeChanged(_ sender: UISlider) {
self.updateBySystem = true
self.volume = Double(sender.value)
self.updateBySystem = false
}
}
fileprivate struct SystemVolumeModifier: ViewModifier {
@StateObject private var controller = SystemVolumeController()
private var showsSystemVolumeHUD: Bool
init(showsSystemVolumeHUD: Bool) {
self.showsSystemVolumeHUD = showsSystemVolumeHUD
}
func body(content: Content) -> some View {
content
.overlay(alignment: .topLeading) {
IntrospectView { view in
guard !self.controller.hasRegisteredWindow(), let window = view.window else { return }
self.controller.registerWindow(window)
}
.frame(width: 0, height: 0)
}
.onChange(of: self.showsSystemVolumeHUD, initial: true) { value, _ in
self.controller.showsSystemVolumeHUD = value
}
.environmentObject(self.controller)
}
}
@MainActor
fileprivate struct IntrospectView: UIViewRepresentable {
let handler: (UIView) -> Void
func makeUIView(context: Context) -> UIView {
ObservableView(didMoveToWindowHandler: self.handler)
}
func updateUIView(_ uiView: UIView, context: Context) {}
}
fileprivate final class ObservableView: UIView {
let didMoveToWindowHandler: (UIView) -> Void
init(didMoveToWindowHandler: @escaping (UIView) -> Void) {
self.didMoveToWindowHandler = didMoveToWindowHandler
super.init(frame: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func didMoveToWindow() {
self.didMoveToWindowHandler(self)
}
}
@ObuchiYuki
Copy link
Author

  • Add systemVolumeController Modifier to app's root view
  • Use SystemVolumeController as EnvironmentObject
  • SystemVolumeController.activate() and SystemVolumeController.deactivate() for activate / deactivate
  • SystemVolumeController.volume is Binding<Double>
  • SystemVolumeController.showsSystemVolumeHUD to show/hide system volume HUD
struct YourApp: SwiftUI.App {
    var body: some View {
        ContentView()
            .systemVolumeController() // Enable system volume controller
    }
}
struct ContentView: View {
    @EnvironmentObject var volumeController: SystemVolumeController
        var body: some View {
            Slider(value: $volumeController.volume, in: 0...1) // Use volume as Binding<Double>
                .onAppear {
                    volumeController.activate() // Activate the controller
                    volumeController.showsSystemVolumeHUD = true // Shows system volume HUD if needed
                }
                .onDisappear { volumeController.deactivate() } // Deactivate the controller
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment