iOS: put the audio and pacing numbers on the number row

First screens onto the shared control. Emulator Volume, Buffer Size,
Output Latency, Fast Forward Volume, Queue Size and FPS Target all become
the same row, and IntSliderRow goes since the new one covers what it did.

Three things these rows gain. Bounds now come from the range constant
rather than being written out again as text, so a Text("150%") can no
longer disagree with the slider it sits under. Every row can be typed
into. And they all carry a VoiceOver label and value, where only the
volume slider did.

Queue Size keeps its stepper. Fifteen values and you usually want a
specific one.

Emulator volume gets a named range like the other numeric settings, and
the clamp helper now reads from it rather than repeating 0 and 150.
This commit is contained in:
J1coding
2026-08-03 09:33:29 +02:00
committed by Jeen
parent e71fc0789f
commit 0f437e758b
5 changed files with 33 additions and 131 deletions
@@ -5,6 +5,6 @@ import Foundation
extension SettingsStore {
static func clampedEmulatorVolumePercent(_ value: Int) -> Int {
min(max(value, 0), 150)
clamped(value, to: emulatorVolumeRange)
}
}
@@ -117,6 +117,7 @@ final class SettingsStore {
static let audioBufferMsRange = 10...200
static let audioOutputLatencyMsRange = 5...200
static let fastForwardVolumeRange = 0...200
static let emulatorVolumeRange = 0...150
static let shadeBoostRange = 1...100
static let casSharpnessRange = 0...100
static let targetFPSRange = Int(minTargetFPS)...Int(maxTargetFPS)
@@ -9,40 +9,11 @@ struct AudioSettingsView: View {
var body: some View {
Form {
Section {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(settings.localized("Emulator Volume"))
Spacer()
Text(Self.formatPercent(settings.emulatorVolumePercent))
.foregroundStyle(.secondary)
.font(.callout.monospacedDigit())
}
Slider(
value: Binding(
get: { Double(settings.emulatorVolumePercent) },
set: { settings.emulatorVolumePercent = Int($0.rounded()) }
),
in: 0...150,
step: 1
)
.accessibilityLabel(settings.localized("Emulator Volume"))
.accessibilityValue(Self.formatPercent(settings.emulatorVolumePercent))
.accessibilityHint(settings.localized("Adjusts emulator game audio without changing iOS system volume or other apps."))
HStack {
Text("0%")
Spacer()
Button(settings.localized("Reset")) {
settings.emulatorVolumePercent = SettingsStore.defaultEmulatorVolumePercent
}
.buttonStyle(.borderless)
Spacer()
Text("150%")
}
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}
NumberRow("Emulator Volume", value: $settings.emulatorVolumePercent,
in: SettingsStore.emulatorVolumeRange, format: .percent,
default: SettingsStore.defaultEmulatorVolumePercent,
hint: "Adjusts emulator game audio without changing iOS system volume or other apps.",
settings: settings)
Text(settings.localized("Controls emulator and game audio only. iOS system volume and other apps stay separate."))
.font(.caption)
@@ -57,9 +28,15 @@ struct AudioSettingsView: View {
.font(.caption)
.foregroundStyle(.secondary)
IntSliderRow("Buffer Size", value: $settings.audioBufferMs, range: SettingsStore.audioBufferMsRange, suffix: " ms", defaultValue: 50, settings: settings)
IntSliderRow("Output Latency", value: $settings.audioOutputLatencyMs, range: SettingsStore.audioOutputLatencyMsRange, suffix: " ms", defaultValue: 20, settings: settings)
IntSliderRow("Fast-Forward Volume", value: $settings.audioFastForwardVolume, range: SettingsStore.fastForwardVolumeRange, suffix: "%", defaultValue: 100, settings: settings)
NumberRow("Buffer Size", value: $settings.audioBufferMs,
in: SettingsStore.audioBufferMsRange, format: .milliseconds,
default: 50, settings: settings)
NumberRow("Output Latency", value: $settings.audioOutputLatencyMs,
in: SettingsStore.audioOutputLatencyMsRange, format: .milliseconds,
default: 20, settings: settings)
NumberRow("Fast-Forward Volume", value: $settings.audioFastForwardVolume,
in: SettingsStore.fastForwardVolumeRange, format: .percent,
default: 100, settings: settings)
Text(settings.localized("Lower buffer or latency reduces lag but can cause crackling. Fast-forward volume is a percentage of normal volume used while fast-forwarding."))
.font(.caption)
@@ -82,8 +59,4 @@ struct AudioSettingsView: View {
.navigationTitle(settings.localized("Audio"))
.navigationBarTitleDisplayMode(.inline)
}
private static func formatPercent(_ value: Int) -> String {
"\(SettingsStore.clampedEmulatorVolumePercent(value))%"
}
}
@@ -29,24 +29,22 @@ struct FramePacingSettingsView: View {
Section {
frameLimiterRows
Stepper("\(settings.localized("Queue Size")): \(settings.vsyncQueueSize)",
value: $settings.vsyncQueueSize,
in: SettingsStore.vsyncQueueRange)
NumberRow("Queue Size", value: $settings.vsyncQueueSize,
in: SettingsStore.vsyncQueueRange, style: .stepper,
settings: settings)
Toggle(settings.localized("Sync to Host Refresh"), isOn: $settings.syncToHostRefresh)
Text(settings.localized("Sync to Host Refresh needs a restart to take effect."))
.font(.caption)
.foregroundStyle(.secondary)
// Same two keys the Audio screen shows, so use the same control. Stepping
// from 10 to 200 a tap at a time was never a serious way to set these.
IntSliderRow("Buffer Size", value: $settings.audioBufferMs,
range: SettingsStore.audioBufferMsRange,
suffix: " ms", defaultValue: 50, settings: settings)
NumberRow("Buffer Size", value: $settings.audioBufferMs,
in: SettingsStore.audioBufferMsRange, format: .milliseconds,
default: 50, settings: settings)
IntSliderRow("Output Latency", value: $settings.audioOutputLatencyMs,
range: SettingsStore.audioOutputLatencyMsRange,
suffix: " ms", defaultValue: 20, settings: settings)
NumberRow("Output Latency", value: $settings.audioOutputLatencyMs,
in: SettingsStore.audioOutputLatencyMsRange, format: .milliseconds,
default: 20, settings: settings)
} header: {
Text(settings.localized("Individual Settings"))
}
@@ -97,25 +95,14 @@ struct FramePacingSettingsView: View {
if settings.frameLimiterEnabled {
VStack(alignment: .leading, spacing: 10) {
HStack {
Text(settings.localized("FPS Target"))
Spacer()
Text(Self.formatFPS(settings.targetFPS))
.foregroundStyle(.secondary)
.font(.callout.monospacedDigit())
}
Slider(
value: Binding(
get: { settings.targetFPS },
set: { value in
settings.targetFPS = value
enforceHardcoreSpeedFloorIfNeeded()
}
),
in: SettingsStore.minTargetFPS...SettingsStore.maxTargetFPS,
step: 1.0
)
NumberRow("FPS Target", value: Binding(
get: { settings.targetFPS },
set: { value in
settings.targetFPS = value
enforceHardcoreSpeedFloorIfNeeded()
}
), in: SettingsStore.minTargetFPS...SettingsStore.maxTargetFPS,
format: .framesPerSecond, step: 1, settings: settings)
HStack {
quickTargetButton(30)
@@ -189,10 +176,6 @@ struct FramePacingSettingsView: View {
.font(.caption.monospacedDigit())
}
private static func formatFPS(_ value: Float) -> String {
String(format: "%.0f FPS", value)
}
private static func formatCompactFPS(_ value: Float) -> String {
String(format: "%.0f", value)
}
@@ -1,55 +0,0 @@
// IntSliderRow.swift Labeled integer slider used by the global settings screens.
// SPDX-License-Identifier: GPL-3.0+
import SwiftUI
/// Slider with the value in the header, the bounds underneath, and a reset in the middle.
/// Lived in AudioSettingsView until Frame Pacing needed the same rows for the same keys.
struct IntSliderRow: View {
let title: String
@Binding var value: Int
let range: ClosedRange<Int>
var suffix: String = ""
let defaultValue: Int
let settings: SettingsStore
init(_ title: String, value: Binding<Int>, range: ClosedRange<Int>,
suffix: String = "", defaultValue: Int, settings: SettingsStore) {
self.title = title
self._value = value
self.range = range
self.suffix = suffix
self.defaultValue = defaultValue
self.settings = settings
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(settings.localized(title))
Spacer()
Text(formatted(value))
.foregroundStyle(.secondary)
.font(.callout.monospacedDigit())
}
Slider(value: Binding(
get: { Double(value) },
set: { value = Int($0.rounded()) }
), in: Double(range.lowerBound)...Double(range.upperBound))
HStack {
Text(formatted(range.lowerBound))
Spacer()
Button(settings.localized("Reset")) { value = defaultValue }
.buttonStyle(.borderless)
Spacer()
Text(formatted(range.upperBound))
}
.font(.caption.monospacedDigit())
.foregroundStyle(.secondary)
}
}
private func formatted(_ value: Int) -> String {
"\(value)\(suffix)"
}
}