Skip to content

Add role-only AsyncButton initializer - #28

Open
vsolano9 wants to merge 3 commits into
SchmiedmayerLab:mainfrom
vsolano9:codex/issue-26-async-button-role-only
Open

Add role-only AsyncButton initializer#28
vsolano9 wants to merge 3 commits into
SchmiedmayerLab:mainfrom
vsolano9:codex/issue-26-async-button-role-only

Conversation

@vsolano9

Copy link
Copy Markdown

Summary

  • add the OS 26 AsyncButton(role:state:action:) initializer requested in [SpeziViews] add AsyncButton.init(role:state:action:) #26
  • use SwiftUI's system-provided role label while preserving shared action, state, and debounce behavior
  • cover native labels and both .overlay and .listRow processing states in the existing UI fixture

Testing

  • testAsyncButtonInToolbar
  • testAsyncButtonDebounce
  • SpeziViews Swift package target build

Closes #26

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 86d8026f-2202-4c12-951d-4b1b084667f8

📝 Walkthrough

Walkthrough

AsyncButton now supports role-only system labels through a new initializer available on Apple OS versions 26 and later. It conditionally renders custom or role-based buttons while preserving asynchronous action handling, disabled states, and processing indicators. The test app adds cancellation, overlay, and list-row scenarios, and UI tests verify labels, completion states, activity indicators, and button re-enablement.

Sequence Diagram(s)

sequenceDiagram
  participant TestUI
  participant AsyncButton
  participant actionSignal
  participant ViewState
  TestUI->>AsyncButton: Tap role-only button
  AsyncButton->>ViewState: Enter processing state
  AsyncButton->>actionSignal: Run async action
  actionSignal-->>AsyncButton: Complete action
  AsyncButton->>ViewState: Exit processing state
  AsyncButton-->>TestUI: Show completion and re-enable
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: adding a role-only AsyncButton initializer.
Description check ✅ Passed The description is directly related and accurately summarizes the initializer addition, UI coverage, and tests.
Linked Issues check ✅ Passed The changes add AsyncButton.init(role:state:action:) and preserve shared behavior, matching issue #26.
Out of Scope Changes check ✅ Passed The UI fixture and tests support the new initializer and do not introduce unrelated scope.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
Sources/SpeziViews/Views/Button/AsyncButton.swift (1)

279-295: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a non-throwing role-only initializer without state.

The existing custom-label initializers provide variants for both throwing actions (requiring a state binding) and non-throwing actions (omitting the state binding). To maintain API symmetry and developer ergonomics, consider adding a non-throwing role-only initializer that omits the state parameter and defaults to .constant(.idle).

💡 Proposed addition
+    /// Creates an async button with a system-provided label for the supplied role.
+    /// - Parameters:
+    ///   - role: A button role that determines the system-provided label.
+    ///   - action: An asynchronous button action.
+    `@available`(iOS 26, macOS 26, tvOS 26, watchOS 26, visionOS 26, *)
+    public init(
+        role: ButtonRole,
+        action: `@MainActor` `@escaping` () async -> Void
+    ) where Label == DefaultButtonLabel {
+        self.role = role
+        self._viewState = .constant(.idle)
+        self.action = action
+        self.label = nil
+    }
+
     /// Creates an async throwing button with a system-provided label for the supplied role.
     /// - Parameters:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/SpeziViews/Views/Button/AsyncButton.swift` around lines 279 - 295, In
AsyncButton’s role-only initializer set, add a non-throwing overload accepting
only ButtonRole and an asynchronous non-throwing action, without a state
parameter. Initialize the view state with a constant idle binding and preserve
the system-provided DefaultButtonLabel behavior of the existing role-based
initializer.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/SpeziViews/Views/Button/AsyncButton.swift`:
- Around line 124-132: The .task setup in the AsyncButton view recreates
actionSignal and can discard taps submitted before the task starts. Preserve a
single action-delivery path across view appearances by removing per-appearance
stream replacement and using stable Task management or a persisted continuation;
ensure any action queued while buttonState is .disabled is recovered and
processed when the task begins, and cancel active work on disappearance as
appropriate.

---

Nitpick comments:
In `@Sources/SpeziViews/Views/Button/AsyncButton.swift`:
- Around line 279-295: In AsyncButton’s role-only initializer set, add a
non-throwing overload accepting only ButtonRole and an asynchronous non-throwing
action, without a state parameter. Initialize the view state with a constant
idle binding and preserve the system-provided DefaultButtonLabel behavior of the
existing role-based initializer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 69130631-7c25-417e-9482-bbc717a3011e

📥 Commits

Reviewing files that changed from the base of the PR and between 80845ae and 00579a1.

📒 Files selected for processing (3)
  • Sources/SpeziViews/Views/Button/AsyncButton.swift
  • Tests/SpeziViewsTests/UITests/TestApp/ViewsTests/AsyncButtonToolbarTest.swift
  • Tests/SpeziViewsTests/UITests/TestAppUITests/SpeziViews/ViewsTests.swift

Comment thread Sources/SpeziViews/Views/Button/AsyncButton.swift

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@PSchmiedmayer PSchmiedmayer added the enhancement New feature or request label Jul 26, 2026
@vsolano9

vsolano9 commented Aug 8, 2026

Copy link
Copy Markdown
Author

Belated answer to the follow-up-issue question — no, please don't auto-open one. If this gets filed it should be by a maintainer who has decided it's real, not by a bot off a PR thread; an issue nobody chose to own is just noise in the tracker.

Since I'm here though, one correction to my own earlier reply, because I was less precise than the finding deserved. I said the lifecycle predates this PR and left it there. That's true and it's still why it stays out of #28, but it isn't a comment on whether the concern is right. Reading it properly: the severity claim holds structurally, whatever the odds of hitting it.

private func submitAction() {
    guard buttonState == .idle else { return }
    buttonState = .disabled                                   // set BEFORE the yield
    self.actionSignal.continuation.yield(.runAction(self.action))
}

and the only path back is runAction's defer { buttonState = .idle }, which only runs if something consumed that yield. So a yield into the pre-.task stream isn't a dropped tap that the user can simply repeat — guard buttonState == .idle then rejects every later tap, and the button is disabled for the lifetime of the view. Disable-then-yield with recovery owned entirely by the consumer is what turns a lost event into a stuck control.

What I have not shown is that the window is reachable — whether a tap can actually land between the button becoming hit-testable and .task starting its loop. I'd want a real runtime test rather than an assertion, and I don't have one, so I'm not claiming a reproducible bug.

If it is worth closing regardless of reachability, the shape that removes the window entirely is to stop replacing the stream: keep the one made at @State init and let .task only consume it. The // durability over multiple appears comment on line 134 says why it's replaced, so that's a real tradeoff and a maintainer's call, not mine to make inside a PR about a role-only initializer.

#28 stays scoped to the role-only initializer either way.

@vsolano9
vsolano9 force-pushed the codex/issue-26-async-button-role-only branch from 7022585 to e0d5417 Compare August 22, 2026 05:51
The custom-label initializers come in throwing and non-throwing pairs, but
the role-only initializer existed only in the throwing form that requires a
ViewState binding. Add the non-throwing overload so callers that do not need
error propagation can use the system-provided role label, keeping the
constant idle state and the DefaultButtonLabel rendering path.

Cover the new initializer in the existing AsyncButton toolbar fixture.
@vsolano9
vsolano9 force-pushed the codex/issue-26-async-button-role-only branch from e0d5417 to d7ad5aa Compare August 23, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[SpeziViews] add AsyncButton.init(role:state:action:)

3 participants