Last active
February 16, 2025 05:58
-
-
Save ObuchiYuki/16477c5c59d778b1eb1ead687a0b8f0a to your computer and use it in GitHub Desktop.
SystemVolumeManager for SwiftUI
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
systemVolumeController
Modifier to app's root viewSystemVolumeController
as EnvironmentObjectSystemVolumeController.activate()
andSystemVolumeController.deactivate()
for activate / deactivateSystemVolumeController.volume
isBinding<Double>
SystemVolumeController.showsSystemVolumeHUD
to show/hide system volume HUD