Repository navigation
This release was first tagged 3.0.0 on 14 October 2026 and was renumbered 1.3.0 on 17 October 2026.
1.3.0 contains source-breaking changes despite the minor version number. See MIGRATION.md.
The feedback list's Showcase look, from spec FEEDBACK01. The changes listed under "Also first shipped in 1.3.0" below break source compatibility with the 1.2.0 tag — Swift 6 language mode, main-actor config / theme / shared, new SwiftlyFeedbackError cases, and the removal of the public Strings enum. MIGRATION.md covers the upgrade. The Showcase list itself is additive, and the one deprecated property keeps compiling.
Added
configuration.listStyle(FeedbackListStyle):.showcase, the default, draws each request as its own card with a status eyebrow, the whole description, a category and comment footer, and a vote capsule..classickeeps the 1.2 list.- Sort & Filter menu. The toolbar menu now holds Sort By, a Filter section with Status and Category pickers, the translation-target picker, and Clear Filters while filtered. While a filter is active the icon fills, the button takes the prominent style, and its accessibility value reads "Filtered by …".
- "Filtered by … Clear" row at the top of a filtered list, in both styles, and a "No Matching Requests" state with Clear Filters when the filters leave nothing.
configuration.buttons.categoryFilter.display(defaulttrue) gates the Category picker, asstatusFiltergates Status.- 19 strings for the menu, the filter row, the filtered-empty state, and VoiceOver, in all eleven locales.
Changed
- Showcase is the default list. Set
configuration.listStyle = .classicto keep the previous look. The detail screen, the submit form, and the vote dialog are unchanged. - Status and category filters run on the device. The list loads once and filters the loaded rows, so changing a filter no longer refetches.
getFeedback(status:category:)still sends server-side filters for direct API callers. - Closed requests show their vote count as text in the Showcase list, with "Voting is closed" read by VoiceOver, instead of a disabled button.
- In the Showcase list the category is plain text.
appearance.categoryColorskeeps colouring Classic rows and the detail screen. - The list draws with system colours and the host's colours as set.
primaryColorand the status colours are never altered. Supporting text (the description, the footer facts, a closed count, the "Filtered by" sentence) uses the system secondary style, and the vote capsule sits on a system fill. These adapt to light mode, dark mode and Increase Contrast on their own. The vote control's chevron on Classic rows and the detail screen uses the system accent again whenprimaryColoris.default, instead of a fixed blue. - The vote hint matches what a tap does. When votes cannot be removed, a voted control is announced as "You've voted. Votes can't be removed" instead of offering to remove the vote. This applies to the list, the Classic row, and the detail screen.
- The Improvement category glyph is
slider.horizontal.3, so it no longer looks like the vote arrow.
Deprecated
configuration.expandDescriptionInList. It has no effect: both list styles show the whole description. Removed in the next major release.
Also first shipped in 1.3.0 (drafted earlier under the untagged 1.1.2 and 2.0.0-unreleased draft headings, never tagged):
Added
- String catalog plugin.
GenerateStringCatalogSymbolsgenerates the package's localized symbols at build time. Xcode asks the host to trust the plugin once. A non-interactivexcodebuildpasses-skipPackagePluginValidation.
Changed
- Swift 6 language mode.
Package.swiftsetsswiftLanguageModes: [.v6]. Deployment targets are unchanged (iOS 26, macOS 26, visionOS 26). - Configuration is main-actor isolated.
SwiftlyFeedback.configuration,SwiftlyFeedback.appearance, andSwiftlyFeedback.currentreplace thenonisolated(unsafe)config,theme, andsharedstorage. The old names still compile as deprecated shims that forward to the new properties, so an existingSwiftlyFeedback.config.allowUndoVote = falsekeeps working and warns. Call the new properties from the main actor. See MIGRATION.md. - Breaking (behavioural): two arms of the HTTP-status →
SwiftlyFeedbackErrormap move, and a host app that switches exhaustively will see the difference. A 403 carrying the shared middleware's body — a role, access or suspension refusal — wasserverError(statusCode: 403)and is nowforbidden(message:). A 429 wasserverError(statusCode: 429)and is nowrateLimited(message:), so a host app can back off instead of treating a rate limit as a server fault. Every 402 the feedback, project and member caps raise keepsfeedbackLimitReached(message:). See MIGRATION.md for the full list of new error cases.
Fixed
updateUser(customID:)now changes the id on the live client. The custom id was written to the Keychain, then registration kept sending the id captured atconfigure. Later feedback, votes, comments, and events stayed on the anonymous id until the next launch. A custom id set while configuration is still resolving is applied when the client is published, and an iCloud lookup that is in flight no longer overwrites a custom id stored during that wait. The iCloud lookup gives up after 3 seconds so an offline CloudKit call cannot leavecurrentnil. Present since1.0.0.- Logs no longer include the API key or personal data. Request and response bodies (email, user id, MRR) were written to OSLog, and a decode failure logged the payload. Logs now carry the method, path, and status only. Present since
1.0.0. - Offline and timeout failures surface as
SwiftlyFeedbackError.networkError. Transport errors were rethrown asURLError, so the documentednetworkErrorcase never matched. Each request now times out after 30 seconds. The client does not retry: a retried submit or vote would be sent twice. Present since1.0.0. - An unknown feedback status no longer blanks the list. One undecodable row used to fail the whole
[Feedback]decode. That row is dropped and the rows this build understands, includingtestflight, are returned. A fetch of that single item still throwsdecodingError. Dates accept whole-second ISO-8601 and the same instant with fractional seconds. A payload that omits fields added after1.0.0(is_hidden_over_limit, roadmap, sync state) still decodes. The list failure was present since1.0.0. The server's encoder does not currently emit fractional seconds; accepting them keeps a future encoder from dropping those rows. - DocC samples and the README samples match the API. The configuration overview called the property
configurationuration. The vote sample readvoteCounton an optionalVoteResult. The error-handling sample's typedcatchwas not exhaustive under Swift 6. - The SDK's feedback filters never reached the server.
getFeedback(status:)andgetFeedback(category:)built a query string and appended it to the request path, andURL.appendingPathComponentpercent-escapes?to%3F— so the whole thing became one path segment, the request named a route that does not exist, the server answered 404, and the SDK surfacedSwiftlyFeedbackError.notFound. Every non-nilfilter selection was affected, including the status filter inFeedbackListView's own toolbar; the unfiltered default appended nothing and worked, which is why this survived. Query building now goes throughURLComponents/URLQueryItemwith+escaped to%2B(the server's form parser rewrites a literal+to a space before decoding), so event names and filter values containing&,#,+or%now arrive intact. Query-less requests are byte-identical to before.
Removed
- Breaking: the public
Stringsenum. EveryStrings.*member from 1.1.1 is gone, includingStrings.feedbackDetailTitleandStrings.commentsTitle. Hosts override copy through string catalog keys. The detail screen titles itself with the feedback item's title. The comments header is a pluralized catalog string inside the package.