Skip to content

Instantly share code, notes, and snippets.

@ohmantics
Created February 12, 2024 21:09
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ohmantics/b0d44830dfd998ea5016112199145cef to your computer and use it in GitHub Desktop.
Save ohmantics/b0d44830dfd998ea5016112199145cef to your computer and use it in GitHub Desktop.
//
// VolumePopupView.swift
//
// Created by Alex Rosenberg on 1/24/24.
//
import SwiftUI
import AVFoundation
import MediaPlayer
extension MPVolumeView {
static func setVolume(_ volume: Float) -> Void {
let volumeView = MPVolumeView()
let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.01) {
slider?.value = volume
}
}
}
func ImageForVolumeLevel(level: Float) -> some View {
let allowedRange = 0.0...1.0
precondition(allowedRange.contains(Double(level)))
var systemName = "speaker.slash.fill"
// switch (level) {
// case 0.0 : systemName = "speaker.slash.fill"
// case 0.0..<0.333 : systemName = "speaker.wave.1.fill"
// case 0.333..<0.666 : systemName = "speaker.wave.2.fill"
// case 0.666...1 : systemName = "speaker.wave.3.fill"
// default: unreachable()
// }
switch (level) {
case 0.0: systemName = "speaker.slash.fill"
default: systemName = "speaker.wave.3.fill"
}
return Image(systemName: systemName, variableValue: Double(level))
.imageScale(.large)
}
struct VolumeImageView: View {
@State private var volume: Float = 0
private let action: (() -> Void)?
public init(action: (() -> Void)? = nil) {
self.action = action
}
var body: some View {
Button(action: { action?() }) {
ImageForVolumeLevel(level: volume)
}
.onReceive(AVAudioSession.sharedInstance().publisher(for: \.outputVolume), perform: { value in
self.volume = value
})
.buttonStyle(.plain)
.frameForLargestSymbol(symbols: ["speaker.slash.fill",
"speaker.wave.1.fill",
"speaker.wave.2.fill",
"speaker.wave.3.fill"],
alignment: .leading)
}
}
struct VolumePopupView: View {
@Binding var volumeIsShowing : Bool
@State var volume: Float = 0
@State var timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
var body: some View {
HStack {
Slider(value: $volume, onEditingChanged: { data in
MPVolumeView.setVolume(self.volume)
})
.controlSize(.small)
.onReceive(AVAudioSession.sharedInstance().publisher(for: \.outputVolume), perform: { value in
self.volume = value
})
.padding(.leading)
ImageForVolumeLevel(level: volume)
.frameForLargestSymbol(symbols: ["speaker.slash.fill",
"speaker.wave.1.fill",
"speaker.wave.2.fill",
"speaker.wave.3.fill"],
alignment: .leading,
scale: .large)
.simultaneousGesture(volumeIsShowing ? TapGesture().onEnded {
volumeIsShowing = false
} : nil)
.padding(10)
}
// TODO should have a 5s inactivity timer self-dismiss
// see https://stackoverflow.com/questions/63927489/how-to-track-all-touches-across-swiftui-app
//MPVolumeViewRepresenter() // TODO this doesn't work in the simulator
//.frame(height: 24)
//.offset(y: 2) // centering
}
}
struct MPVolumeViewRepresenter: UIViewRepresentable {
func makeUIView(context: Context) -> MPVolumeView {
MPVolumeView(frame: .zero)
}
func updateUIView(_ uiView: MPVolumeView, context: UIViewRepresentableContext<MPVolumeViewRepresenter>) {}
}
#Preview {
@State var volumeIsShowing : Bool = true
return VolumePopupView(volumeIsShowing: $volumeIsShowing).previewLayout(.sizeThatFits)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment