feat: add Add item and Try it OS shortcuts - #31
Merged
Conversation
Android's intent action and iOS's UIApplicationShortcutItem type are bare strings crossing a platform boundary, so they are declared once in commonMain and each platform maps its own identifier onto them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Static shortcuts, so they exist from install and localize through @string with no runtime code. The intents are explicit, so the custom actions are a payload for MainActivity to read rather than something the system has to resolve. ShortcutCommand is deliberately not a data class, for the same reason as SharedPhoto: identity equality re-fires LaunchedEffect when the same shortcut is tapped twice in a row. Try It needs only the tab switch, so App consumes it. Add Item is threaded down to WardrobeScreen, which consumes it once the sheet is open — the pager sets beyondViewportPageCount = 1, so an unconsumed command would reopen the sheet on every return to the tab. The shortcut drawables bake in the ink colour instead of the existing icons' placeholder black: a launcher does not tint a shortcut icon. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Registered at launch rather than declared in Info.plist so the titles come from Localizable.strings like every other user-facing string and the icons are SF Symbols like the rest of the app. They appear after the first launch, not at install, which is the accepted cost. SwiftUI has no hook for performActionFor, so a minimal app/scene delegate pair is attached. Both delivery paths are covered: willConnectTo on a cold launch and windowScene(_:performActionFor:) while running. A tap is marked handled by id instead of cleared on the inbox, because the @published replay can land mid-update and SwiftUI forbids publishing changes from there. WardrobeScreen uses task(id:) rather than onChange so a command already set when the view first appears is not missed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A plain drawable is scaled onto a white badge by the launcher, which is why the glyphs had to carry dark ink. Going adaptive means the shortcut owns its background, so a white glyph stays legible whatever the launcher's theme, and a pinned shortcut matches the app icon instead of sitting on a white disc. The background reuses the launcher icon's green for exactly that reason. The monochrome layer opts into themed icons on Android 13+. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds OS-level shortcuts for the app's two highest-intent actions: Add item and Try it, reachable by long-pressing the app icon on both platforms.
Worn already proves it can be entered from outside the app — the system-share path drops a photo straight into Try It. This adds the other outside entry point, the icon itself. Both shortcuts land the user on the right surface and stop there: no camera or picker is auto-fired. Add item opens the Wardrobe tab with the Add Item sheet up; Try it opens the Try It tab idle.
There is no navigation library here — navigation is a flat tab switch plus screen-local sheet booleans — so a shortcut is expressed as select a tab plus, optionally, set a screen's sheet flag. The work reuses the "pending command hoisted at the entry point, threaded down, consumed exactly once" pattern that
SharedPhotoestablished.Changes
AppShortcutenum incommonMain. Android's intent action and iOS'sUIApplicationShortcutItem.typeare bare strings crossing a platform boundary, so the ids are declared once and each platform maps its own identifier onto them. Covered byAppShortcutTest.res/xml/shortcuts.xml(new directory) plus theandroid.app.shortcutsmeta-data onMainActivity. Static, so they exist from install and localize through@stringwith no runtime code. The intents are explicit (targetPackage+targetClass), so the custom actions are a payload forMainActivityto read rather than something the system has to resolve — no new intent-filter.handleShareIntentgeneralised tohandleIntent; a newShortcutCommand(deliberately not a data class, so identity equality re-firesLaunchedEffecton a repeat tap, same asSharedPhoto) is hoisted inMainActivityand threaded throughAppintoWardrobeScreen.Services/QuickActions.swift: the actions are registered at launch rather than declared inInfo.plist, so titles come fromLocalizable.stringslike every other user-facing string and the icons are SF Symbols like the rest of the app. Trade-off: they appear after the first launch, not at install.AppDelegate+SceneDelegatepair, since SwiftUI has no hook forperformActionFor. Both delivery paths are covered:willConnectToon a cold launch,windowScene(_:performActionFor:)while running.shortcut_add_item_short/long,shortcut_try_it_short/long) in all four files. The pt-BR short label is "Adicionar" rather than "Adicionar item", because launchers truncate short labels hard.Two notes on consuming the command exactly once, both load-bearing:
beyondViewportPageCount = 1, so it disposes Wardrobe when it is more than a page away. An unconsumed command would reopen the sheet on every return to the tab — henceonAddSheetOpened.idinstead of cleared on the inbox: the@Publishedreplay can land mid-update, and SwiftUI forbids publishing changes from there.WardrobeScreenuses.task(id:)rather than.onChangeso a command already set when the view first appears is not missed.Test plan
Automated (run and passing):
./gradlew :shared:allTests—AppShortcutTest, 5 tests./gradlew detekt./gradlew :composeApp:assembleDebugAndroid —
./gradlew :composeApp:installDebug, then:adb shell dumpsys activity activities | grep MainActivity).Shortcut intents can also be driven directly:
iOS — open
iosApp/in Xcode, run once (dynamic actions need one launch), then long-press the Home Screen icon:plus/viewfindersymbols and localized titles.performActionFor; cold (swipe the app away first) →willConnectTo. They are separate delegate callbacks.Checklist
./gradlew detektpasses🤖 Generated with Claude Code