From a1dea935b0f4b99ba1ef7a952d5314512fe91dc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 14:42:19 -0400 Subject: [PATCH] fix: stop login button collapsing on press release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Forji/Forji/Views/InstanceFormView.swift | 45 ++++++++++++++++++++---- 1 file changed, 38 insertions(+), 7 deletions(-) diff --git a/Forji/Forji/Views/InstanceFormView.swift b/Forji/Forji/Views/InstanceFormView.swift index 1fdcebc..5d99253 100644 --- a/Forji/Forji/Views/InstanceFormView.swift +++ b/Forji/Forji/Views/InstanceFormView.swift @@ -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)