fix: stop login button collapsing on press release

Give the login button a custom FlatPrimaryButtonStyle instead of
.borderedProminent + .controlSize(.large). The system style's built-in press
treatment springs/scales on release, which on iOS 26 read as the button's
lower edge collapsing when the finger lifted. The flat style changes only
background opacity on press — no scaleEffect, no spring — so the frame holds.

Keeps the full-width fill, the large hit target, and the ZStack label that
holds the frame steady while the loading spinner is shown.

Generated with Claude Opus 4.8 (1M)
This commit is contained in:
Claude 2026-05-29 14:42:19 -04:00
parent 992c628abd
commit a1dea935b0

View file

@ -144,19 +144,18 @@ struct InstanceFormView: View {
Section {
Button(action: handleSave) {
HStack {
Spacer()
ZStack {
Text(isAddMode ? "Login" : "Save")
.fontWeight(.semibold)
.opacity(isLoading ? 0 : 1)
if isLoading {
ProgressView()
.progressViewStyle(.circular)
} else {
Text(isAddMode ? "Login" : "Save")
.fontWeight(.semibold)
.tint(.white)
}
Spacer()
}
}
.buttonStyle(.borderedProminent)
.buttonStyle(FlatPrimaryButtonStyle())
.disabled(
isLoading || serverURL.isEmpty
|| (authMode == .credentials
@ -330,6 +329,38 @@ struct InstanceFormView: View {
}
}
/// A full-width primary button style with a large hit target.
///
/// Defines its own appearance instead of relying on `.borderedProminent`, whose built-in
/// press treatment springs/scales on release on iOS 26 that read as the button's lower
/// edge "collapsing" when the finger lifted. This style changes only the background opacity
/// on press, with no `scaleEffect` and no spring, so the frame never moves.
private struct FlatPrimaryButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
Background(configuration: configuration)
}
private struct Background: View {
let configuration: ButtonStyleConfiguration
@Environment(\.isEnabled) private var isEnabled
var body: some View {
configuration.label
.font(.headline)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding(.vertical, 14)
.background(Color.accentColor.opacity(backgroundOpacity), in: Capsule())
.contentShape(Capsule())
}
private var backgroundOpacity: Double {
if !isEnabled { return 0.35 }
return configuration.isPressed ? 0.85 : 1.0
}
}
}
#if DEBUG
#Preview("Add") {
InstanceFormView(authService: .previewDefault, mode: .add)