A code generation tool enable use of UserDefaults as computed properties in a class.

Overview

SVMPrefs

Code Coverage: 95% CI Workflow

Note: This tool requires Xcode 11 for compilation as it uses some Swift 5.1 language features.

SVMPrefs is a command line tool that generates the code to read and write preferences based on the SVM data.

The SVM name comes from the three main data elements: Store, Variable, and Migrate.

Why?

A typical way UserDefaults is often used is as follows.

let prefs = UserDefaults.standard
if !prefs.bool(forKey: "firstLaunch") {
    prefs.set(true, forKey: "firstLaunch") {
    showFTUX()
}

This kind of on-the-spot use of UserDefaults has at least 11 issues:

  1. The caller has to know the data source: UserDefaults.standard
  2. The caller has to reference the key name, twice: "firstLaunch"
  3. The caller has to know the type: prefs.bool and true
  4. The caller has to know about any conversions: ! and true (did you catch the inverted logic?)
  5. All this code, at the point of use, adds noise around the real purpose of the code -- to call showFTUX() on first app launch.
  6. This code is then repeated in other places for some preferences, thus violating the DRY principle
  7. It is not easy to unit test with the above code.
  8. There may be many other preferences throughout the code -- most likely without documentation
  9. Migrating preferences to a different UserDefaults location is not trivial
  10. Removing deprecated preferences is easy to leave undone or forgotten
  11. There is limited code completion help with this approach

A solution to the above is to define a dedicated class that encapsulates the details of each preference so that the application logic can focus on using them in a simple and clear way. With a dedicated class, using a preference can look like this:

let prefs = AppPrefs()
if prefs.isFirstLaunch {
    prefs.isFirstLaunch = false
    showFTUX()
}

SVMPrefs takes this one step further by generating the code to read, write, migrate and delete preferences based on your SVM specifications.

Install from local build

Note: This tool requires Xcode 11 for compilation as it uses some Swift 5.1 language features.

Run make install from the SVMPrefs root directory to build and install the svmprefs binary in /usr/local/bin.

You can open the project using Xcode 11 by opening the Package.swift file or using xed . from the command line.

Command line

The basic command line is as follows: svmprefs [command] [options] [args]

Command Description
help Shows help text
version Shows version information
gen source_file_name Processes the given file and generates the code in the SVM data

You can run svmprefs gen --help to get additional details on the gen command.

> svmprefs gen --help

Usage: svmprefs gen <input> [options]

Processes the given file and generates the code for the contained SVM data

Options:
  -b, --backup            Create a backup copy of the source file (foo.m -> foo.backup.m)
  -d, --debug             Print debug output
  -h, --help              Show help information
  -i, --indent <value>    Set indent width. (Default: 4)

Using in Xcode

You can integrate SVMPrefs in your Xcode project to have it generate the code prior to compiling as well as highlight any errors in your SVM specifications via a run script.

Add a new "Run Script Phase" that occurs before compilation with something like the following.

set -e

if which svmprefs >/dev/null; then
  # Update this section with the desired command line options and
  # actual file paths to your code that have SVM data.
  # NOTE: svmprefs supports just one file at a time.
  svmprefs gen -i 4 $SRCROOT/Common/SharedUserDefaults.swift
else
  echo "WARNING: svmprefs is not installed. See: https://github.com/ghv/SVMPrefs"
fi

SVM Data Format

You must add a comment block in your code that starts and ends with SVMPREFS like the following.

/*SVMPREFS [NB: the rest of this line reserved for svmprefs tool use]

# this line is treated as a comment by svmprefs
S demo
V Bool | isDemo | demo_key_name | |

SVMPREFS*/

# — Comment

Any line with a # as the first non-white-space character is treated as a comment within the SVMPREFS comment block

S — Store

The store record has three parameters that are | delimited

  • 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.
  • 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.
  • options - A comma-delimited set of code generation flags. (See code below)
enum Options: String {
    case generateRemoveAllMethod = "RALL"
}

You define one S record for each unique suite. Each S record is followed by any number of V records.

V — Variable

The variable record has five parameters that are | delimited

  • type - Any valid variable type expression including arrays, dates, optionals, and dictionaries.
  • 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.
  • key - The preference's key name. Leading and trailing white-space characters are not supported.
  • options - An optional comma-delimited set of code generation flags (See code below)
  • default - An optional default value to be returned if the preference does not exist in the store or has a null value.
enum Options: String {
    case generateInvertedLogic = "INV"
    case decorateWithObjC = "OBJC"

    // Defining a Bool named 'firstLaunch' with this option will
    // generate code for it as 'isFirstLaunch' in some places.
    case decorateNameWithIsPrefix = "IS"

    case omitGeneratingGetterAndSetter = "NVAR"
    case omitGeneratingSetter = "NSET"

    case addToRemoveAllMethod = "RALL"
    // Use this to omit when RALL is set at the store level:
    case omitFromRemoveAllMethod = "NRALL"

    case generateRemovePrefMethod = "REM"
    case generateIsSetMethod = "ISSET"
}

M — Migrate

If 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.

The migrate record has four parameters that are | delimited:

  • source store - The source S record's name
  • destination store - The destination S record's name. Use delete if source variable is being deleted.
  • source variable name - The variable name in the source store.
  • destination variable name - The variable name in the destination store. Omit if being deleted.

The tool will insert all the migration code in a function called migrate() that you should call every time the app starts. Migration is performed as an object to object read and write. Once, migrated, the key is deleted from the source store. You must include a code mark named migrate somewhere in your class.

Any 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 remain in the source store's Keys enum.

Generated Code Marks

The 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 to indicate where the generated code is to be inserted.

    // MARK: BEGIN identifier
    // MARK: END identifier

If you have any migrations defined, you will also need to include a code mark for the migrate identifier.

You can specify multiple identifiers in the same MARK by joining them together with a comma delimiter in the order you want them to appear. The begin and end marks must have the same identifiers in identical order. No spaces around the commas or you will get errors.

    // MARK: BEGIN foo,bar
    // MARK: END foo,bar

Minimal Swift example:

/*SVMPREFS
S demo
V Bool | isDemo | demo_key_name | |
SVMPREFS*/

class MyDemoPreferences {

    // ANYTHING HERE IS LEFT UNTOUCHED

    // MARK: BEGIN demo
    // ANYTHING HERE WILL BE REPLACED
    // BY THE GENERATED CODE
    // MARK: END demo

    // ANYTHING HERE IS LEFT UNTOUCHED
}

Questions & Tips

How can I inject the preference to be read or written?

There 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 preference without having to know the actual property. If you use to use the key name as this reference, you can now use a KeyPath or WriteableKeyPath for this purpose. Here is an example.

/*SVMPREFS
S keypath
V [String] | primaryList   | app.primaryList   | |
V [String] | secondaryList | app.secondaryList | |
SVMPREFS*/

class KeyPathPrefs {
    // MARK: BEGIN keypath
    // MARK: END keypath
}

// Somewhere in your app...
func demo() {
    // A function that needs to use one of several preferences
    func processList(keyPath: KeyPath<KeyPathPrefs, [String]>) {
        let prefs = KeyPathPrefs()
        let list = prefs[keyPath: keyPath]
        // Do something with this list...
    }

    // How you call it:
    processList(keyPath: \.primaryList)
    processList(keyPath: \.secondaryList)
}

License

Copyright 2019-2020 The SVMPrefs Authors. SVMPrefs is licensed under the Apache 2.0 License. Contributions welcome.

See LICENSE.md for license information.

See CONTRIBUTORS.md for The SVMPrefs Authors.

See NOTICE.md for dependency license information.

Thank You!

You might also like...
A CLI tool for the survey of the SSH-Key strength in your GitHub organization members.

GitHub organization SSH-keys checker A CLI tool for the survey of the SSH-Key strength in your GitHub organization members. Requirements macOS 12.0+ S

A functional tool-belt for Swift Language similar to Lo-Dash or Underscore.js in Javascript

Dollar Dollar is a Swift library that provides useful functional programming helper methods without extending any built in objects. It is similar to L

SwiftDI - A dependency injection tool for Swift

SwiftDI SwiftDI is a dependency injection tool for Swift. With it you can build well-structured and easily testable applications for iOS class Example

Store and retrieve Codable objects to various persistence layers, in a couple lines of code!
Store and retrieve Codable objects to various persistence layers, in a couple lines of code!

tl;dr You love Swift's Codable protocol and use it everywhere, who doesn't! Here is an easy and very light way to store and retrieve Codable objects t

Sort import statements in your Swift source code
Sort import statements in your Swift source code

Sort Swift Imports Sort import statements in your Swift source code. 🏛 Swift Li

Enables developers to write code that interacts with CloudKit in targets that don't support the CloudKit Framework directly

CloudKit Web Services This package enables developers to write code that interac

Enables developers to write code that interacts with CloudKit in targets that don't support the CloudKit Framework directly

CloudKit Web Services This package enables developers to write code that interac

Typed key-value storage solution to store Codable types in various persistence layers with few lines of code!
Typed key-value storage solution to store Codable types in various persistence layers with few lines of code!

🗂 Stores A typed key-value storage solution to store Codable types in various persistence layers like User Defaults, File System, Core Data, Keychain

TypedDefaults is a utility library to type-safely use NSUserDefaults.

TypedDefaults TypedDefaults is a utility library to type-safely use NSUserDefaults. Motivation The talk Keep Calm and Type Erase On by Gwendolyn Westo

Comments
  • Add support for optional Bool, Double, Float and Int

    Add support for optional Bool, Double, Float and Int

    New Feature Submissions:

    1. [x] Does your submission pass tests?
    2. [x] Have you run SwiftLint on your code locally prior to submission?

    Changes to Core Features:

    • [x] Have you added an explanation of what your changes do and why you'd like us to include them?
    • [x] Have you written new tests for your core changes, as applicable?
    • [x] Have you successfully run the tests with your changes locally?

    You can now create V records for Bool?, Double?, Float?, and Int? types.

    opened by ghv 0
  • Crash when I have an empty code mark after one or more non-empty code marks.

    Crash when I have an empty code mark after one or more non-empty code marks.

    • [x] Provide a description of what the bug is about
    • [x] Provide a minimal code snippet / example that reproduces the bug.
    • [x] Provide a description of what you believe the output show be.

    I am seeing a crash, "Fatal error: Index out of range" in updateBeginEndTailComments when the input looks like this:

                import Foundation
                import AppKit
    
                /*SVMPREFS
                S main | | RALL
    
                V Bool | boolVar1    | boolVar2 | IS |
                V Bool | hasBoolVar2 | boolVar2 | |
                V Bool | boolVar3    | boolVar3 | |
                V Bool | boolVar4    | boolVar4 | |
    
                S copy | | RALL
    
                V Bool | boolVar3 | boolVar3 | |
                V Bool | boolVar4 | boolVar4 | |
    
                M main | copy   | boolVar3 | boolVar3
                M main | copy   | boolVar4 | boolVar4
                M main | delete | hasBoolVar2
    
                SVMPREFS*/
    
                class MyMigrationTests {
                    // MARK: BEGIN main
                    // this will be deleted
                    // MARK: END main
    
                    // MARK: BEGIN copy
                    // this will be deleted
                    // MARK: END copy
    
                    // MARK: BEGIN migrate
                    // MARK: END migrate
                }
    

    The migrate code mark has no code to delete and is therefore not having its code mark begin and code mark end indexes updated when removing the existing code between all code marks.

    It should adjust these indexes because there are code marks for which there was some code to be deleted above it.

    It is also possible that this would not crash if there are more lines after the empty code marks equal or greater in length to the number of lines removed in prior code marks. In this case the code would get inserted in the wrong place.

    opened by ghv 0
Owner
Gus Verdun
Sr. Manager, Mobile Software Engineering at Capital One (Hiring) during the day and personal projects at night. Bernese Mountain Dog lover.
Gus Verdun
Effortlessly synchronize UserDefaults over iCloud.

Zephyr ??️ Effortlessly sync UserDefaults over iCloud About Zephyr synchronizes specific keys and/or all of your UserDefaults over iCloud using NSUbiq

Arthur Ariel Sabintsev 841 Dec 23, 2022
Prephirences is a Swift library that provides useful protocols and convenience methods to manage application preferences, configurations and app-state. UserDefaults

Prephirences - Preϕrences Prephirences is a Swift library that provides useful protocols and convenience methods to manage application preferences, co

Eric Marchand 557 Nov 22, 2022
Simple, Strongly Typed UserDefaults for iOS, macOS and tvOS

简体中文 DefaultsKit leverages Swift 4's powerful Codable capabilities to provide a Simple and Strongly Typed wrapper on top of UserDefaults. It uses less

Nuno Dias 1.4k Dec 26, 2022
Modern interface to UserDefaults + Codable support

Default Modern interface to UserDefaults + Codable support What is Default? Default is a library that extends what UserDefaults can do by providing ex

Nicholas Maccharoli 475 Dec 20, 2022
Swifty and modern UserDefaults

Defaults Swifty and modern UserDefaults Store key-value pairs persistently across launches of your app. It uses NSUserDefaults underneath but exposes

Sindre Sorhus 1.3k Dec 31, 2022
A lightweight wrapper over UserDefaults/NSUserDefaults with an additional layer of AES-256 encryption

SecureDefaults for iOS, macOS Requirements • Usage • Installation • Contributing • Acknowledgments • Contributing • Author • License SecureDefaults is

Victor Peschenkov 216 Dec 22, 2022
🔍 Browse and edit UserDefaults on your app

UserDefaults-Browser Browse and edit UserDefaults on your app. (SwiftUI or UIKit) Browse Edit (as JSON) Edit (Date) Export Note: We recommend to use S

Yusuke Hosonuma 25 Nov 3, 2022
Shows the issue with swift using an ObjC class which has a property from a swift package.

SwiftObjCSwiftTest Shows the issue with swift using an ObjC class which has a property from a swift package. The Swift class (created as @objc derived

Scott Little 0 Nov 8, 2021
Classes-and-structures-in-swift - This source files show what is the difference between class and structure

This source files show what is the difference between class and structure You ca

null 0 Jan 4, 2022
Save NSObject into NSUserDefaults in one-line, auto class mapping

Akaibu What is it ? Archive any class in just ONE-LINE of code. Automatically map class's properties under the hood. Drop in replacement of NSObject S

Roy Tang 16 Jun 21, 2021