Fix iOS playback settings menu text disappearing and resizing issues

When tapping menus in playback settings (playback mode, quality profile,
stream, rate, captions, audio track), the selected value text would
disappear and cause unwanted resizing animations.

Implemented ZStack overlay technique for all iOS menu buttons:
- Visible static label remains on screen
- Invisible Menu overlay (.opacity(0)) handles tap interactions
- Prevents text from disappearing when menu opens
- Eliminates resizing animations on option selection
This commit is contained in:
Arkadiusz Fal
2025-11-23 14:09:14 +01:00
parent 0c4609bcf1
commit 65e86d30ec

View File

@@ -209,16 +209,22 @@ struct PlaybackSettings: View {
.controlSize(.large) .controlSize(.large)
.frame(width: 100, alignment: .center) .frame(width: 100, alignment: .center)
#elseif os(iOS) #elseif os(iOS)
ZStack {
Text(player.rateLabel(player.currentRate))
.foregroundColor(.primary)
.frame(width: 70)
Menu { Menu {
ratePicker ratePicker
} label: { } label: {
Text(player.rateLabel(player.currentRate)) Text(player.rateLabel(player.currentRate))
.foregroundColor(.primary) .foregroundColor(.primary)
.frame(width: 70) .frame(width: 70)
.opacity(0)
} }
.transaction { t in t.animation = .none }
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundColor(.primary) .transaction { t in t.animation = .none }
}
.frame(width: 70, height: 40) .frame(width: 70, height: 40)
#else #else
Text(player.rateLabel(player.currentRate)) Text(player.rateLabel(player.currentRate))
@@ -331,12 +337,20 @@ struct PlaybackSettings: View {
.controlSize(.large) .controlSize(.large)
.frame(width: 300, alignment: .trailing) .frame(width: 300, alignment: .trailing)
#else #else
ZStack {
Label(player.playbackMode.description.localized(), systemImage: player.playbackMode.systemImage)
.foregroundColor(.accentColor)
Menu { Menu {
playbackModePicker playbackModePicker
} label: { } label: {
Label(player.playbackMode.description.localized(), systemImage: player.playbackMode.systemImage) Label(player.playbackMode.description.localized(), systemImage: player.playbackMode.systemImage)
.foregroundColor(.accentColor)
.opacity(0)
} }
.buttonStyle(.plain)
.transaction { t in t.animation = .none } .transaction { t in t.animation = .none }
}
#endif #endif
} }
@@ -356,16 +370,23 @@ struct PlaybackSettings: View {
.controlSize(.large) .controlSize(.large)
.frame(width: 300, alignment: .trailing) .frame(width: 300, alignment: .trailing)
#elseif os(iOS) #elseif os(iOS)
ZStack {
Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
.foregroundColor(.accentColor)
.frame(maxWidth: 240, alignment: .trailing)
Menu { Menu {
qualityProfilePicker qualityProfilePicker
} label: { } label: {
Text(player.qualityProfileSelection?.description ?? "Automatic".localized()) Text(player.qualityProfileSelection?.description ?? "Automatic".localized())
.frame(maxWidth: 240, alignment: .trailing)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.frame(maxWidth: 240, alignment: .trailing) .frame(maxWidth: 240, alignment: .trailing)
.opacity(0)
}
.buttonStyle(.plain)
.transaction { t in t.animation = .none }
}
.frame(maxWidth: 240, alignment: .trailing)
.frame(height: 40) .frame(height: 40)
#else #else
ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) { ControlsOverlayButton(focusedField: $focusedField, field: .qualityProfile) {
@@ -406,15 +427,22 @@ struct PlaybackSettings: View {
.controlSize(.large) .controlSize(.large)
.frame(width: 300, alignment: .trailing) .frame(width: 300, alignment: .trailing)
#elseif os(iOS) #elseif os(iOS)
ZStack {
Text(player.streamSelection?.resolutionAndFormat ?? "loading...")
.foregroundColor(player.streamSelection == nil ? .secondary : .accentColor)
.frame(width: 140, height: 40, alignment: .trailing)
Menu { Menu {
StreamControl() StreamControl()
} label: { } label: {
Text(player.streamSelection?.resolutionAndFormat ?? "loading...") Text(player.streamSelection?.resolutionAndFormat ?? "loading...")
.frame(width: 140, height: 40, alignment: .trailing)
.foregroundColor(player.streamSelection == nil ? .secondary : .accentColor) .foregroundColor(player.streamSelection == nil ? .secondary : .accentColor)
.frame(width: 140, height: 40, alignment: .trailing)
.opacity(0)
} }
.transaction { t in t.animation = .none }
.buttonStyle(.plain) .buttonStyle(.plain)
.transaction { t in t.animation = .none }
}
.frame(width: 140, height: 40, alignment: .trailing) .frame(width: 140, height: 40, alignment: .trailing)
#else #else
StreamControl(focusedField: $focusedField) StreamControl(focusedField: $focusedField)
@@ -429,6 +457,25 @@ struct PlaybackSettings: View {
.controlSize(.large) .controlSize(.large)
.frame(width: 300, alignment: .trailing) .frame(width: 300, alignment: .trailing)
#elseif os(iOS) #elseif os(iOS)
ZStack {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = player.captions,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
} else {
if videoCaptions?.isEmpty == true {
Text("Not available")
} else {
Text("Disabled")
}
}
}
.foregroundColor(.accentColor)
.frame(alignment: .trailing)
.frame(height: 40)
Menu { Menu {
if videoCaptions?.isEmpty == false { if videoCaptions?.isEmpty == false {
captionsPicker captionsPicker
@@ -440,7 +487,6 @@ struct PlaybackSettings: View {
let language = LanguageCodes(rawValue: captions.code) let language = LanguageCodes(rawValue: captions.code)
{ {
Text("\(language.description.capitalized) (\(language.rawValue))") Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else { } else {
if videoCaptions?.isEmpty == true { if videoCaptions?.isEmpty == true {
Text("Not available") Text("Not available")
@@ -449,13 +495,15 @@ struct PlaybackSettings: View {
} }
} }
} }
.foregroundColor(.accentColor)
.frame(alignment: .trailing) .frame(alignment: .trailing)
.frame(height: 40) .frame(height: 40)
.opacity(0)
.disabled(videoCaptions?.isEmpty == true) .disabled(videoCaptions?.isEmpty == true)
} }
.transaction { t in t.animation = .none }
.buttonStyle(.plain) .buttonStyle(.plain)
.foregroundColor(.accentColor) .transaction { t in t.animation = .none }
}
#else #else
ControlsOverlayButton(focusedField: $focusedField, field: .captions) { ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
HStack(spacing: 8) { HStack(spacing: 8) {
@@ -500,16 +548,23 @@ struct PlaybackSettings: View {
.controlSize(.large) .controlSize(.large)
.frame(width: 300, alignment: .trailing) .frame(width: 300, alignment: .trailing)
#elseif os(iOS) #elseif os(iOS)
ZStack {
Text(player.selectedAudioTrack?.displayLanguage ?? "Original")
.foregroundColor(.accentColor)
.frame(maxWidth: 240, alignment: .trailing)
Menu { Menu {
audioTrackPicker audioTrackPicker
} label: { } label: {
Text(player.selectedAudioTrack?.displayLanguage ?? "Original") Text(player.selectedAudioTrack?.displayLanguage ?? "Original")
.frame(maxWidth: 240, alignment: .trailing)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.frame(maxWidth: 240, alignment: .trailing) .frame(maxWidth: 240, alignment: .trailing)
.opacity(0)
}
.buttonStyle(.plain)
.transaction { t in t.animation = .none }
}
.frame(maxWidth: 240, alignment: .trailing)
.frame(height: 40) .frame(height: 40)
#else #else
ControlsOverlayButton(focusedField: $focusedField, field: .audioTrack) { ControlsOverlayButton(focusedField: $focusedField, field: .audioTrack) {