{"id":23168610,"url":"https://github.com/ghv/svmprefs","last_synced_at":"2025-08-18T06:33:43.038Z","repository":{"id":63910940,"uuid":"194284055","full_name":"ghv/SVMPrefs","owner":"ghv","description":"A code generation tool enable use of UserDefaults as computed properties in a class.","archived":false,"fork":false,"pushed_at":"2020-09-17T09:24:30.000Z","size":54,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-12-16T20:04:00.359Z","etag":null,"topics":["code-generator","ios","macos","preference","preferences","swift-library","swift-package-manager","swift4","swift5","swift5-1","userdefaults"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ghv.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-06-28T14:07:46.000Z","updated_at":"2020-12-31T10:07:05.000Z","dependencies_parsed_at":"2023-01-14T13:15:59.272Z","dependency_job_id":null,"html_url":"https://github.com/ghv/SVMPrefs","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ghv%2FSVMPrefs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ghv%2FSVMPrefs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ghv%2FSVMPrefs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ghv%2FSVMPrefs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ghv","download_url":"https://codeload.github.com/ghv/SVMPrefs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230210461,"owners_count":18190671,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","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":["code-generator","ios","macos","preference","preferences","swift-library","swift-package-manager","swift4","swift5","swift5-1","userdefaults"],"created_at":"2024-12-18T03:12:04.199Z","updated_at":"2024-12-18T03:12:11.651Z","avatar_url":"https://github.com/ghv.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# SVMPrefs\n\n![Code Coverage: 95%](https://img.shields.io/badge/Code%20Coverage-95%25-green.svg)\n![CI Workflow](https://github.com/ghv/SVMPrefs/workflows/CI/badge.svg)\n\n**Note**: This tool requires Xcode 11 for compilation as it uses some Swift 5.1 language features.\n\n`SVMPrefs` is a command line tool that generates the code to read and write preferences based on the `SVM` data.\n\nThe `SVM` name comes from the three main data elements: Store, Variable, and Migrate.\n\n## Why?\n\nA typical way `UserDefaults` is often used is as follows.\n\n```swift\nlet prefs = UserDefaults.standard\nif !prefs.bool(forKey: \"firstLaunch\") {\n    prefs.set(true, forKey: \"firstLaunch\") {\n    showFTUX()\n}\n```\n\nThis kind of on-the-spot use of `UserDefaults` has at least 11 issues:\n\n1.  The caller has to know the data source: `UserDefaults.standard`\n1.  The caller has to reference the key name, twice: `\"firstLaunch\"`\n1.  The caller has to know the type: `prefs.bool` and `true`\n1.  The caller has to know about any conversions: `!` and `true` (did you catch the inverted logic?)\n1.  All this code, at the point of use, adds noise around the real purpose of the code -- to call `showFTUX()` on first app launch.\n1.  This code is then repeated in other places for some preferences, thus violating the DRY principle\n1.  It is not easy to unit test with the above code.\n1.  There may be many other preferences throughout the code -- most likely without documentation\n1.  Migrating preferences to a different `UserDefaults` location is not trivial\n1.  Removing deprecated preferences is easy to leave undone or forgotten\n1.  There is limited code completion help with this approach\n\nA solution to the above is to define a dedicated class that encapsulates the details of each preference\nso that the application logic can focus on using them in a simple and clear way.\nWith a dedicated class, using a preference can look like this:\n\n```swift\nlet prefs = AppPrefs()\nif prefs.isFirstLaunch {\n    prefs.isFirstLaunch = false\n    showFTUX()\n}\n```\n\nSVMPrefs takes this one step further by generating the code to read, write,\nmigrate and delete preferences based on your SVM specifications.\n\n## Install from local build\n\n**Note**: This tool requires Xcode 11 for compilation as it uses some Swift 5.1 language features.\n\nRun `make install` from the SVMPrefs root directory to build and install the `svmprefs` binary in `/usr/local/bin`.\n\nYou can open the project using Xcode 11 by opening the `Package.swift` file or using `xed .` from the command line.\n\n## Command line\n\nThe basic command line is as follows: `svmprefs [command] [options] [args]`\n\n| Command                | Description               |\n|------------------------|---------------------------|\n| `help`                 | Shows help text           |\n| `version`              | Shows version information |\n| `gen` source_file_name | Processes the given file and generates the code in the SVM data |\n\nYou can run `svmprefs gen --help` to get additional details on the `gen` command.\n\n```\n\u003e svmprefs gen --help\n\nUsage: svmprefs gen \u003cinput\u003e [options]\n\nProcesses the given file and generates the code for the contained SVM data\n\nOptions:\n  -b, --backup            Create a backup copy of the source file (foo.m -\u003e foo.backup.m)\n  -d, --debug             Print debug output\n  -h, --help              Show help information\n  -i, --indent \u003cvalue\u003e    Set indent width. (Default: 4)\n```\n\n## Using in Xcode\n\nYou can integrate SVMPrefs in your Xcode project to have it generate the code prior to compiling as well as\nhighlight any errors in your SVM specifications via a run script.\n\nAdd a new \"Run Script Phase\" that occurs before compilation with something like the following.\n\n```sh\nset -e\n\nif which svmprefs \u003e/dev/null; then\n  # Update this section with the desired command line options and\n  # actual file paths to your code that have SVM data.\n  # NOTE: svmprefs supports just one file at a time.\n  svmprefs gen -i 4 $SRCROOT/Common/SharedUserDefaults.swift\nelse\n  echo \"WARNING: svmprefs is not installed. See: https://github.com/ghv/SVMPrefs\"\nfi\n```\n\n## SVM Data Format\n\nYou must add a comment block in your code that starts and ends with `SVMPREFS` like the following.\n\n```swift\n/*SVMPREFS [NB: the rest of this line reserved for svmprefs tool use]\n\n# this line is treated as a comment by svmprefs\nS demo\nV Bool | isDemo | demo_key_name | |\n\nSVMPREFS*/\n```\n\n## `#` \u0026mdash; Comment\n\nAny line with a `#` as the first non-white-space character is treated as a comment within the `SVMPREFS` comment block\n\n## `S` \u0026mdash; Store\n\nThe store record has three parameters that are `|` delimited\n\n*   `name` - A name for this store that is used to define the store's class instance variable and code mark identifier. The name can be anything except `delete` and `migrate`.\n*   `suite` - An expression that, if specified, is used to construct a store object with a suite name (AKA app group in iOS). See `UserDefaults`. Use `none` to omit generating a store variable as you will supply one in your class. Leave this blank or write `standard` to use `UserDefaults.standard`.\n*   `options` - A comma-delimited set of code generation flags. (See code below)\n\n```swift\nenum Options: String {\n    case generateRemoveAllMethod = \"RALL\"\n}\n```\n\nYou define one `S` record for each unique suite. Each `S` record is followed by any number of `V` records.\n\n## `V` \u0026mdash; Variable\n\nThe variable record has five parameters that are `|` delimited\n\n*   `type` - Any valid variable type expression including arrays, dates, optionals, and dictionaries.\n*   `name` - The property name for this preference. If the variable is a boolean type, it will have an `is` prefix prepended if not already prepended.\n*   `key` - The preference's key name. Leading and trailing white-space characters are not supported.\n*   `options` - An optional comma-delimited set of code generation flags (See code below)\n*   `default` - An optional default value to be returned if the preference does not exist in the store or has a null value.\n\n```swift\nenum Options: String {\n    case generateInvertedLogic = \"INV\"\n    case decorateWithObjC = \"OBJC\"\n\n    // Defining a Bool named 'firstLaunch' with this option will\n    // generate code for it as 'isFirstLaunch' in some places.\n    case decorateNameWithIsPrefix = \"IS\"\n\n    case omitGeneratingGetterAndSetter = \"NVAR\"\n    case omitGeneratingSetter = \"NSET\"\n\n    case addToRemoveAllMethod = \"RALL\"\n    // Use this to omit when RALL is set at the store level:\n    case omitFromRemoveAllMethod = \"NRALL\"\n\n    case generateRemovePrefMethod = \"REM\"\n    case generateIsSetMethod = \"ISSET\"\n}\n```\n\n### `M` \u0026mdash; Migrate\n\nIf you have one or more `S` records, you can use the `M` records to move the preferences from one store to another or delete them as your needs change.\n\nThe migrate record has four parameters that are `|` delimited:\n\n*   `source store` - The source `S` record's `name`\n*   `destination store` - The destination `S` record's `name`. Use `delete` if source variable is being deleted.\n*   `source variable name` - The variable `name` in the source store.\n*   `destination variable name` - The variable `name` in the destination store. Omit if being deleted.\n\nThe tool will insert all the migration code in a function called `migrate()` that you should call every time the app starts.\nMigration is performed as an object to object read and write. Once, migrated, the key is deleted from the source store.\nYou must include a code mark named `migrate` somewhere in your class.\n\nAny variable that is migrated or deleted will no longer be accessible from the source store as a property. However, the key for this property will\nremain in the source store's `Keys` enum.\n\n## Generated Code Marks\n\nThe generated code must be placed in a class in your source file. Add a pair of comments, as shown below, with the `identifier` being the store's `name`\nto indicate where the generated code is to be inserted.\n\n```swift\n    // MARK: BEGIN identifier\n    // MARK: END identifier\n```\n\nIf you have any migrations defined, you will also need to include a code mark for the `migrate` `identifier`.\n\nYou can specify multiple `identifier`s in the same MARK by joining them together with a comma delimiter in the order you want them to appear.\nThe begin and end marks must have the same identifiers in identical order. No spaces around the commas or you will get errors.\n\n```swift\n    // MARK: BEGIN foo,bar\n    // MARK: END foo,bar\n```\n\n### Minimal Swift example:\n\n```swift\n/*SVMPREFS\nS demo\nV Bool | isDemo | demo_key_name | |\nSVMPREFS*/\n\nclass MyDemoPreferences {\n\n    // ANYTHING HERE IS LEFT UNTOUCHED\n\n    // MARK: BEGIN demo\n    // ANYTHING HERE WILL BE REPLACED\n    // BY THE GENERATED CODE\n    // MARK: END demo\n\n    // ANYTHING HERE IS LEFT UNTOUCHED\n}\n```\n\n# Questions \u0026 Tips\n\n## How can I inject the preference to be read or written?\n\nThere may be cases where you need to provide a reference to a preference in a function so that the code in there can then read or write to that\npreference without having to know the actual property. If you use to use the key name as this reference, you can now use\na `KeyPath` or `WriteableKeyPath` for this purpose. Here is an example.\n\n```swift\n/*SVMPREFS\nS keypath\nV [String] | primaryList   | app.primaryList   | |\nV [String] | secondaryList | app.secondaryList | |\nSVMPREFS*/\n\nclass KeyPathPrefs {\n    // MARK: BEGIN keypath\n    // MARK: END keypath\n}\n\n// Somewhere in your app...\nfunc demo() {\n    // A function that needs to use one of several preferences\n    func processList(keyPath: KeyPath\u003cKeyPathPrefs, [String]\u003e) {\n        let prefs = KeyPathPrefs()\n        let list = prefs[keyPath: keyPath]\n        // Do something with this list...\n    }\n\n    // How you call it:\n    processList(keyPath: \\.primaryList)\n    processList(keyPath: \\.secondaryList)\n}\n```\n\n# License\n\nCopyright 2019-2020 The SVMPrefs Authors. SVMPrefs is licensed under the Apache 2.0 License. Contributions welcome.\n\nSee [LICENSE.md](LICENSE.md) for license information.\n\nSee [CONTRIBUTORS.md](CONTRIBUTORS.md) for The SVMPrefs Authors.\n\nSee [NOTICE.md](NOTICE.md) for dependency license information.\n\n# Thank You!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fghv%2Fsvmprefs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fghv%2Fsvmprefs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fghv%2Fsvmprefs/lists"}