{"id":25903834,"url":"https://github.com/shapehq/spices","last_synced_at":"2026-02-19T02:02:20.963Z","repository":{"id":279427178,"uuid":"256268107","full_name":"shapehq/spices","owner":"shapehq","description":"🫙🌶️ Spices makes it straightforward to create in-app debug menus by generating native UI from Swift.","archived":false,"fork":false,"pushed_at":"2025-09-04T05:23:41.000Z","size":8460,"stargazers_count":390,"open_issues_count":4,"forks_count":9,"subscribers_count":11,"default_branch":"main","last_synced_at":"2025-10-21T10:42:45.405Z","etag":null,"topics":["ios"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/shapehq.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2020-04-16T16:20:13.000Z","updated_at":"2025-10-11T15:42:01.000Z","dependencies_parsed_at":"2025-02-25T14:41:05.185Z","dependency_job_id":"1e7a7e43-1f1e-491b-97e5-72363489f50f","html_url":"https://github.com/shapehq/spices","commit_stats":null,"previous_names":["shapehq/spices"],"tags_count":23,"template":false,"template_full_name":null,"purl":"pkg:github/shapehq/spices","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shapehq%2Fspices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shapehq%2Fspices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shapehq%2Fspices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shapehq%2Fspices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/shapehq","download_url":"https://codeload.github.com/shapehq/spices/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/shapehq%2Fspices/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29600847,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T00:59:38.239Z","status":"online","status_checked_at":"2026-02-19T02:00:07.702Z","response_time":117,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ios"],"created_at":"2025-03-03T04:01:57.485Z","updated_at":"2026-02-19T02:02:20.946Z","avatar_url":"https://github.com/shapehq.png","language":"Swift","funding_links":[],"categories":["Swift"],"sub_categories":[],"readme":"# 🫙🌶 Spices\n\n\u003cdiv align=\"center\"\u003e\n\u003ch4\u003eSpices makes it straightforward to create in-app debug menus by generating native UI from Swift.\u003c/h4\u003e\n\n\u003cimg src=\"spices.png\" alt=\"Logo for Spices. A wooden shelf holds four glass jars filled with different spices. Above the shelf is a circular sign with the word 'Spices' and a red crossed-out bug.\" /\u003e\n\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fshapehq%2Fspices%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/shapehq/spices)\n[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fshapehq%2Fspices%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/shapehq/spices)\\\n[![Build](https://github.com/shapehq/shpspices/actions/workflows/build.yml/badge.svg)](https://github.com/shapehq/shpspices/actions/workflows/build.yml)\n[![Build Example Project](https://github.com/shapehq/shpspices/actions/workflows/build_example_project.yml/badge.svg)](https://github.com/shapehq/shpspices/actions/workflows/build_example_project.yml)\n[![SwiftLint](https://github.com/shapehq/shpspices/actions/workflows/swiftlint.yml/badge.svg)](https://github.com/shapehq/shpspices/actions/workflows/swiftlint.yml)\\\n[![Run Tests](https://github.com/shapehq/spices/actions/workflows/test.yml/badge.svg)](https://github.com/shapehq/spices/actions/workflows/test.yml)\n[![Build Documentation](https://github.com/shapehq/spices/actions/workflows/build_documentation.yml/badge.svg)](https://github.com/shapehq/spices/actions/workflows/build_documentation.yml)\n[![CodeQL](https://github.com/shapehq/spices/actions/workflows/codeql.yml/badge.svg)](https://github.com/shapehq/spices/actions/workflows/codeql.yml)\n\u003c/div\u003e\n\n- [👋 Introduction](#-introduction)\n- [🚀 Getting Started](#-getting-started)\n    - [Step 1: Add the Spices Swift Package](#step-1-add-the-spices-swift-package)\n    - [Step 2: Create an In-App Debug Menu](#step-2-create-an-in-app-debug-menu)\n    - [Step 3: Present the In-App Debug Menu](#step-3-present-the-in-app-debug-menu)\n    - [Step 4: Observing Values](#step-4-observing-values)\n- [🧪 Example Projects](#-example-projects)\n- [📖 Documentation](#-documentation)\n  - [Toggles](#toggles)\n  - [Pickers](#pickers)\n  - [Buttons](#buttons)\n  - [Text Fields](#text-fields)\n  - [Group Settings Using Nested Spice Stores](#group-settings-using-nested-spice-stores)\n  - [Inject Your Own Views](#inject-your-own-views)\n  - [Require Restart](#require-restart)\n  - [Display Custom Name](#display-custom-name)\n  - [Specify Editor Title](#specify-editor-title)\n  - [Store Values in Custom UserDefaults](#store-values-in-custom-userdefaults)\n  - [Store Values Under Custom Key](#store-values-under-custom-key)\n  - [Using with @AppStorage](#using-with-appstorage)\n- [🤔 Why \"Spices\"?](#-why-spices)\n\n## 👋 Introduction\n\nSpices generates native in-app debug menus from Swift code using the `@Spice` property wrapper and `SpiceStore` protocol and stores settings in [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults).\n\nWe built Spices at [Shape](https://shape.dk) (becoming [Framna](https://framna.com)) to provide a frictionless API for quickly creating these menus. Common use cases include environment switching, resetting state, and enabling features during development.\n\n\u003cimg src=\"code.gif?raw=true\" /\u003e\n\n\n## 🚀 Getting Started\n\nThis section details the steps needed to add an in-app debug menu using Spices.\n\n### Step 1: Add the Spices Swift Package\n\nAdd Spices to your Xcode project or Swift package.\n\n```swift\nlet package = Package(\n    dependencies: [\n        .package(url: \"git@github.com:shapehq/spices.git\", from: \"4.0.0\")\n    ]\n)\n```\n\n### Step 2: Create an In-App Debug Menu\n\nSpices uses [reflection](https://en.wikipedia.org/wiki/Reflective_programming) to generate UI from the properties of a type conforming to the `SpiceStore` protocol\n\n\u003e [!IMPORTANT]\n\u003e Reflection is a technique that should be used with care. We use it in Spices, a tool meant purely for debugging, in order to make it frictionless to add a debug menu.\n\nThe following shows an example conformance to the SpiceDispenser protocol. You may copy this into your project to get started.\n\n```swift\nenum ServiceEnvironment: String, CaseIterable {\n    case production\n    case staging\n}\n\nclass AppSpiceStore: SpiceStore {\n    @Spice(requiresRestart: true) var environment: ServiceEnvironment = .production\n    @Spice var enableLogging = false\n    @Spice var clearCache = {\n        try await Task.sleep(for: .seconds(1))\n        URLCache.shared.removeAllCachedResponses()\n    }\n    @Spice var featureFlags = FeatureFlagsSpiceStore()\n}\n\nclass FeatureFlagsSpiceStore: SpiceStore {\n    @Spice var notifications = false\n    @Spice var fastRefreshWidgets = false\n}\n```\n\nBased on the above code, Spices will generate an in-app debug menu like the one shown below.\n\n\u003cimg src=\"/example.gif\" width=\"300\"/\u003e\n\n### Step 3: Present the In-App Debug Menu\n\nThe app must be configured to display the spice editor. The approach depends on whether your app is using a SwiftUI or UIKit lifecycle.\n\n\u003e [!WARNING]\n\u003e The in-app debug menu may contain sensitive information.  Ensure it's only accessible in debug and beta builds by excluding the menu's presentation code from release builds using conditional compilation (e.g., `#if DEBUG`).  The examples in this section demonstrate this technique.\n\n#### SwiftUI Lifecycle\n\nUse the `presentSpiceEditorOnShake(_:)` view modifier to show the editor when the device is shaken.\n\n```swift\nstruct ContentView: View {\n    @StateObject var spiceStore = AppSpiceStore()\n\n    var body: some View {\n        VStack {\n            Image(systemName: \"globe\")\n                .imageScale(.large)\n                .foregroundStyle(.tint)\n            Text(\"Hello, world!\")\n        }\n        .padding()\n        #if DEBUG\n        .presentSpiceEditorOnShake(editing: spiceStore)\n        #endif\n    }\n}\n```\n\nAlternatively, manually initialize and display an instance of `SpiceEditor`.\n\n```swift\nstruct ContentView: View {\n    @StateObject var spiceStore = AppSpiceStore()\n    @State var isSpiceEditorPresented = false\n\n    var body: some View {\n        Button {\n            isSpiceEditorPresented = true\n        } label: {\n            Text(\"Present Spice Editor\")\n        }\n        .sheet(isPresented: $isSpiceEditorPresented) {\n            SpiceEditor(editing: spiceStore)\n        }\n    }\n}\n```\n\n#### UIKit Lifecycle\n\nUse the an instance of `SpiceEditorWindow` to show the editor when the device is shaken.\n\n```swift\nclass SceneDelegate: UIResponder, UIWindowSceneDelegate {\n    var window: UIWindow?\n\n    func scene(\n        _ scene: UIScene,\n        willConnectTo session: UISceneSession,\n        options connectionOptions: UIScene.ConnectionOptions\n    ) {\n        let windowScene = scene as! UIWindowScene\n        #if DEBUG\n        window = SpiceEditorWindow(windowScene: windowScene, editing: AppSpiceStore.shared)\n        #else\n        window = UIWindow(windowScene: windowScene)\n        #endif\n        window?.rootViewController = ViewController()\n        window?.makeKeyAndVisible()\n    }\n}\n```\n\nAlternatively, initialize an instance of `SpiceEditorViewController` and present it.\n\n```swift\nlet viewController = SpiceEditorViewController(editing: AppSpiceStore.shared)\npresent(spicesViewController, animated: true)\n```\n\n### Step 4: Observing Values\n\nThe currently selected value can be referenced through a spice store:\n\n```swift\nAppSpiceStore.environment\n```\n\n#### SwiftUI Lifecycle\n\nSpice stores conforming to the `SpiceStore` protocol also conform to [ObservableObject](https://developer.apple.com/documentation/combine/observableobject), and as such, can be observed from SwiftUI using [StateObject](https://developer.apple.com/documentation/swiftui/stateobject), [ObservedObject](https://developer.apple.com/documentation/swiftui/observedobject), or [EnvironmentObject](https://developer.apple.com/documentation/swiftui/environmentobject).\n\n```swift\nclass AppSpiceStore: SpiceStore {\n    @Spice var enableLogging = false\n}\n\nstruct ContentView: View {\n    @StateObject var spiceStore = AppSpiceStore()\n    \n    var body: some View {\n        Text(\"Is logging enabled: \" + (spiceStore.enableLogging ? \"👍\" : \"👎\"))\n    }\n}\n```\n\n#### UIKit Lifecycle\n\nProperties using the `@Spice` property wrapper exposes a publisher that can be used to observe changes to the value using [Combine](https://developer.apple.com/documentation/combine).\n\n```swift\nclass ContentViewController: UIViewController {\n    private let spiceStore = AppSpiceStore.shared\n    private var cancellables: Set\u003cAnyCancellable\u003e = []\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n        spiceStore.$enableLogging\n            .sink { isEnabled in\n                print(\"Is logging enabled: \" + (isEnabled ? \"👍\" : \"👎\"))\n            }\n            .store(in: \u0026cancellables)\n    }\n}\n```\n\n## 🧪 Example Projects\n\nThe example projects in the [Examples](/Examples) folder shows how Spices can be used to add an in-app debug menu to iOS apps with SwiftUI and UIKit lifecycles.\n\n## 📖 Documentation\n\n\u003ca href=\"https://swiftpackageindex.com/shapehq/spices/documentation\"\u003e\u003cimg src=\"https://swiftpackageindex.com/images/logo.svg\" width=\"40\" align=\"left\" /\u003e\u003c/a\u003e\n\u003ch3\u003eThe documentation is \u003ca href=\"https://swiftpackageindex.com/shapehq/spices/documentation\"\u003eavailable on Swift Package Index\u003c/a\u003e.\u003c/h3\u003e\n\u003cbr/\u003e\n\nThe following sections document select APIs and use cases.\n\n### Toggles\n\nToggles are created for boolean variables in a spice store.\n\n```swift\n@Spice var enableLogging = false\n```\n\n### Pickers\n\nPickers are created for types conforming to both [RawRepresentable](https://developer.apple.com/documentation/swift/rawrepresentable) and [CaseIterable](https://developer.apple.com/documentation/swift/caseiterable). This is typically enums.\n\n```swift\nenum ServiceEnvironment: String, CaseIterable {\n    case production\n    case staging\n}\n\nclass AppSpiceStore: SpiceStore {\n    @Spice var environment: ServiceEnvironment = .production\n}\n```\n\nConforming the type to `SpicesTitleProvider` lets you override the displayed name for each case.\n\n```swift\nenum ServiceEnvironment: String, CaseIterable, SpicesTitleProvider {\n    case production\n    case staging\n\n    var spicesTitle: String {\n        switch self {\n        case .production:\n            \"🚀 Production\"\n        case .staging:\n            \"🧪 Staging\"\n        }\n    }\n}\n```\n\n### Buttons\n\nClosures with no arguments are treated as buttons.\n\n```swift\n@Spice var clearCache = {\n    URLCache.shared.removeAllCachedResponses()\n}\n```\n\nProviding an asynchronous closure causes a loading indicator to be displayed for the duration of the operation.\n\n```swift\n@Spice var clearCache = {\n    try await Task.sleep(for: .seconds(1))\n    URLCache.shared.removeAllCachedResponses()\n}\n```\n\nAn error message is automatically shown if the closure throws an error.\n\n### Text Fields\n\nText fields are created for string variables in a spice store.\n\n```swift\n@Spice var url = \"http://example.com\"\n```\n\n### Group Settings Using Nested Spice Stores\n\nSpice stores can be nested to create a hierarchical user interface.\n\n```swift\nclass AppSpiceStore: SpiceStore {\n    @Spice var featureFlags = FeatureFlagsSpiceStore()\n}\n\nclass FeatureFlagsSpiceStore: SpiceStore {\n    @Spice var notifications = false\n    @Spice var fastRefreshWidgets = false\n}\n```\n\nBy default, a nested spice store is presented as a new screen in the navigation stack. This behavior is equivalent to:\n\n```swift\n@Spice(presentation: .push) var featureFlags = FeatureFlagsSpiceStore()\n```\n\nA nested spice store can also be presented as a modal instead of being pushed onto the navigation stack:\n\n```swift\n@Spice(presentation: .modal) var featureFlags = FeatureFlagsSpiceStore()\n```\n\nAlternatively, it can be displayed as an inlined section within the settings list:\n\n```swift\n@Spice(presentation: .inline) var featureFlags = FeatureFlagsSpiceStore()\n```\n\nWhen inlining a nested spice store, a header and footer can be provided for better context:\n\n```swift\n@Spice(\n  presentation: .push,\n  header: \"Features\",\n  footer: \"Test features that are yet to be released.\"\n)\nvar featureFlags = FeatureFlagsSpiceStore()\n```\n\n### Inject Your Own Views\n\nYou can embed your own views into Spices, for example, to display static information.\n\nThe `@Spice` property wrapper allows you to define custom views within Spices settings. These views can be inlined by default or presented using different styles.\n\nBy default, views are inlined within the settings list:\n\n```swift\n@Spice var version = LabeledContent(\"Version\", value: \"1.0 (1)\")\n```\n\nYou can change the presentation style using the presentation argument.\n\nThe `.push` presentation pushes the view onto the navigation stack.\n\n```swift\n@Spice(presentation: .push) var helloWorld = VStack {\n    Image(systemName: \"globe\")\n        .imageScale(.large)\n        .foregroundStyle(.tint)\n    Text(\"Hello, world!\")\n}\n.padding()\n```\n\nThe `.modal` presentation presents the view modally on top of Spices.\n\n```swift\n@Spice(presentation: .modal) var helloWorld = // ...\n```\n\n### Require Restart\n\nSetting `requiresRestart` to true will cause the app to be shut down after changing the value. Use this only when necessary, as users do not expect a restart.\n\n```swift\n@Spice(requiresRestart: true) var environment: ServiceEnvironment = .production\n```\n\n### Display Custom Name\n\nBy default, the editor displays a formatted version of the property name. You can override this by manually specifying a custom name.\n\n```swift\n@Spice(name: \"Debug Logging\") var enableLogging = false\n```\n\n### Specify Editor Title\n\nBy default the editor will be displayed with the title \"Debug Menu\". This can be customized as follows.\n\n**SwiftUI Lifecycle**\n\nThe `presentSpiceEditorOnShake(editing:title:)` view modifier takes a title as follows.\n\n```swift\n.presentSpiceEditorOnShake(editing: spiceStore, title: \"Config\")\n```\n\nThe title can also be specified when manually creating and presenting an instance of `SpiceEditor`.\n\n```swift\nSpiceEditor(editing: spiceStore, title: \"Config\")\n```\n\n**UIKit Lifecycle**\n\nThe ``SpiceEditorWindow`` can be initialized with a title as follows.\n\n```swift\nSpiceEditorWindow(windowScene: windowScene, editing: AppSpiceStore.shared, title: \"Config\")\n```\n\nThe title can also be specified when manually creating and presenting an instance of `SpiceEditorViewController`.\n\n```swift\nlet viewController = SpiceEditorViewController(editing: AppSpiceStore.shared, title: \"Config\")\n```\n\n### Store Values in Custom UserDefaults\n\nBy default, values are stored in [UserDefaults.standard](https://developer.apple.com/documentation/foundation/userdefaults/1416603-standard). To use a different [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults) instance, such as for sharing data with an app group, implement the `userDefaults` property of `SpiceStore`.\n\n```swift\nclass AppSpiceStore: SpiceStore {\n    let userDefaults = UserDefaults(suiteName: \"group.dk.shape.example\")\n}\n```\n\n### Store Values Under Custom Key\n\nValues are stored in [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults) using a key derived from the property name, optionally prefixed with the names of nested spice stores. You can override this by specifying a custom key.\n\n```swift\n@Spice(key: \"env\") var environment: ServiceEnvironment = .production\n```\n\n### Using with @AppStorage\n\nValues are stored in [UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults) and can be used with [@AppStorage](https://developer.apple.com/documentation/swiftui/appstorage) for seamless integration in SwiftUI.\n\n```swift\nstruct ExampleView: View {\n    @AppStorage(\"enableLogging\") var enableLogging = false\n\n    var body: some View {\n        Toggle(isOn: $enableLogging) {\n            Text(\"Enable Logging\")\n        }\n    }\n}\n```\n\n## 🤔 Why \"Spices\"?\n\nThe name \"Spices\" evolved from our original repository, \"ConfigVars\", which we used internally at [Shape](https://shape.dk) (becoming [Framna](https://framna.com)) for several years. That early version didn’t use reflection, but when we experimented with a new implementation that did, we jokingly called it \"spicing it up.\" The idea stuck, and we realized developers could also use the package to \"spice up\" their own projects, adding extra debugging \"spices\" as needed.\n\nFor the first few years, the project was called \"Config Vars\", but we never really loved that name. It felt too generic. When we decided to open-source the package, we considered reverting to the original name or using other generic alternatives like \"configs,\" \"variables,\" \"tweaks,\" or \"configuration variables.\"\n\nHowever, these terms are so widely used and have so many different meanings that we worried about causing naming conflicts in developers' codebases. Ultimately, we stuck with \"Spices\" because it’s unique, memorable, and less likely to clash with existing concepts.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshapehq%2Fspices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fshapehq%2Fspices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fshapehq%2Fspices/lists"}