Skip to content

1.3.0

Latest

Choose a tag to compare

@VanAkenBen VanAkenBen released this 17 Oct 19:47

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. .classic keeps 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 (default true) gates the Category picker, as statusFilter gates 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 = .classic to 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.categoryColors keeps colouring Classic rows and the detail screen.
  • The list draws with system colours and the host's colours as set. primaryColor and 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 when primaryColor is .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. GenerateStringCatalogSymbols generates the package's localized symbols at build time. Xcode asks the host to trust the plugin once. A non-interactive xcodebuild passes -skipPackagePluginValidation.

Changed

  • Swift 6 language mode. Package.swift sets swiftLanguageModes: [.v6]. Deployment targets are unchanged (iOS 26, macOS 26, visionOS 26).
  • Configuration is main-actor isolated. SwiftlyFeedback.configuration, SwiftlyFeedback.appearance, and SwiftlyFeedback.current replace the nonisolated(unsafe) config, theme, and shared storage. The old names still compile as deprecated shims that forward to the new properties, so an existing SwiftlyFeedback.config.allowUndoVote = false keeps working and warns. Call the new properties from the main actor. See MIGRATION.md.
  • Breaking (behavioural): two arms of the HTTP-status → SwiftlyFeedbackError map 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 — was serverError(statusCode: 403) and is now forbidden(message:). A 429 was serverError(statusCode: 429) and is now rateLimited(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 keeps feedbackLimitReached(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 at configure. 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 leave current nil. Present since 1.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 as URLError, so the documented networkError case 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 since 1.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, including testflight, are returned. A fetch of that single item still throws decodingError. Dates accept whole-second ISO-8601 and the same instant with fractional seconds. A payload that omits fields added after 1.0.0 (is_hidden_over_limit, roadmap, sync state) still decodes. The list failure was present since 1.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 read voteCount on an optional VoteResult. The error-handling sample's typed catch was not exhaustive under Swift 6.
  • The SDK's feedback filters never reached the server. getFeedback(status:) and getFeedback(category:) built a query string and appended it to the request path, and URL.appendingPathComponent percent-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 surfaced SwiftlyFeedbackError.notFound. Every non-nil filter selection was affected, including the status filter in FeedbackListView's own toolbar; the unfiltered default appended nothing and worked, which is why this survived. Query building now goes through URLComponents/URLQueryItem with + 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 Strings enum. Every Strings.* member from 1.1.1 is gone, including Strings.feedbackDetailTitle and Strings.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.