fix: stop login button collapsing on press release

Replace the .borderedProminent + .controlSize(.large) login button with a
custom FlatPrimaryButtonStyle. 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 never moves.

Keeps the full-width fill, the large hit target, and a 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:36:30 -04:00
parent d69674b3ab
commit 4547075a21

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: .rect(cornerRadius: 14))
.contentShape(.rect(cornerRadius: 14))
}
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)