Type-Safe UserDefaults API

As the Swift APIs have evolved, Apple has also been improving their own frameworks to make them easier to work with in Swift. Two API improvements I’ve personally appreciated are how we work with NSAttributedString keys, and Notification names.

Deriving inspiration from those additions, I thought UserDefaults could probably benefit from a similar approach.

Keys

Lets start by defining a UserDefaults.Key type similar to NSAttributedString.Key.

extension UserDefaults {

    public struct Key: Hashable, RawRepresentable, ExpressibleByStringLiteral {
        public var rawValue: String

        public init(rawValue: String) {
            self.rawValue = rawValue
        }

        public init(stringLiteral value: String) {
            self.rawValue = value
        }
    }

}

Notice we’re adding ExpressibleByStringLiteral support to allow us to simplify key definitions.

let foo: UserDefaults.Key = "foo"

Type-Safety

Now lets add API that allows us to use our new Key type instead of raw strings. While we’re at it lets also add type-safety to allow us to infer function calls.

Note: This is a lighweight approach that doesn’t provide key-to-value type-safety. You will still need to know the expected type that’s returned for a given key.

func set<T>(_ value: T?, forKey key: Key) {
    set(value, forKey: key.rawValue)
}

func value<T>(forKey key: Key) -> T? {
    return value(forKey: key.rawValue) as? T
}

While we’re at it, let’s add a subscript too.

subscript<T>(key: Key) -> T? {
    get { return value(forKey: key) }
    set { set(newValue, forKey: key.rawValue) }
}

Finally we need to an API we can use when registering our initial values.

func register(defaults: [Key: Any]) {
    let mapped = Dictionary(uniqueKeysWithValues: defaults.map { (key, value) -&gt; (String, Any) in
        return (key.rawValue, value)
    })

    register(defaults: mapped)
}

API Usage

Now we have a clean API let define some keys:

public extension UserDefaults.Key {
    static let showLineNumbers: UserDefaults.Key = "ShowLineNumbers"
    static let fontFamily: UserDefaults.Key = "SourceFontFamily"
    static let fontSize: UserDefaults.Key = "SourceFontSize"
    static let defaultFontSize: UserDefaults.Key = "DefaultSourceFontSize"
}

Now we can make use of these keys and define some initial values:

let defaults = UserDefaults.standard

defaults.register(defaults: [
    .showLineNumbers: false,
    .fontFamily: defaultFont.familyName!,
    .fontSize: defaultSize,
    .defaultFontSize: defaultSize
])

showNumbers = defaults[.showLineNumbers]
defaults[.showLineNumbers] = false

Conclusion

I think this is a clean solution that feels familiar to other iOS framework APIs and is simple enough to start adding to an existing project immediately with no overhead.

There are other approaches to this problem, but I found this to be a lightweight solution that adds value from the outset, without incurring additional complexity in your code.

Checkout the link to get the full source, including more overrides and conveniences for dealing with various types.

Clean dynamic font API in Swift

There are generally a couple of ways to get custom fonts loaded into your apps.

  1. Include them in your bundle and load them at launch time via info.plist
  2. Dynamically load fonts after launch, even from a remote url

Loading many fonts via your info.plist is generally impractical as it has a detrimental effect on your apps launch time. However it removes the need for any code to load the fonts into memory and guarantees they exist well before you need to access them. As an added bonus, fonts loaded this way can be defined in XIB’s and Storyboard’s.

Dynamically loading fonts has the advantage of being able to load the fonts on-demand and you can even download them from a remote url. This approach also doesn’t require you to touch your info.plist.

I recently worked on an app that contained around 15-20 fonts, most of which were not required during the early stages of the apps lifecycle. So I opted for dynamically loading the fonts on-demand.

For the purposes of this article, I’d like to focus on a clean API pattern that I’ve used across various apps.

API Implementation

When dynamically loading fonts, we generally need 3 pieces of information. The font’s name, filename and extension.

public protocol FontCacheDescriptor: Codable {
    var fontName: String { get }
    var fileName: String { get }
    var fileExtension: String { get }
}

Now we have a type that describes our custom font. In most cases on iOS, we deal with TrueType fonts, so let’s define a default implementation for that.

public extension FontCacheDescriptor {
    public var fileExtension: String {
        return "ttf"
    }
}

The approach I’m going to suggest makes use of enum types. With that knowledge in hand, lets add a another extension to make use of the rawValue when our enum is RawRepresentable.

public extension FontCacheDescriptor 
   where Self: RawRepresentable, Self.RawValue == String {
      public var fontName: String {
          return rawValue
      }

      public var fileName: String {
          return rawValue
      }
}

Here’s comes the meat of this API. In order to load our custom font we need to perform the following tasks.

  1. Register the font with the system and load it into memory (only if its not already cached)
  2. If we’re targeting iOS 11+, scale the font’s size based on the current UIContentSizeCategoy
  3. Make a descriptor for the font

So lets add a convenience initializer to UIFont.

extension UIFont {

    public convenience init(descriptor: FontCacheDescriptor, size: CGFloat) {
        FontCache.cacheIfNeeded(named: descriptor.fileName, fileExtension: descriptor.fileExtension)
        let size = UIFontMetrics.default.scaledValue(for: size)
        let descriptor = UIFontDescriptor(name: descriptor.fontName, size: size)
        self.init(descriptor: descriptor, size: 0)
    }

}

API Usage

With all the code in place, we can now easily create clean APIs around our custom fonts for use in our UI code.

Lets say we have a font called Graphik and it has 4 variants.

extension UIFont {
    
    // The `rawValue` MUST match the filename (without extension)
    public enum Graphik: String, FontCacheDescriptor {
        case regular = "GraphikAltWeb-Regular"
        case medium = "GraphikAltWeb-Medium"
        case regularItalic = "GraphikAltWeb-RegularItalic"
        case mediumItalic = "GraphikAltWeb-MediumItalic"
    }
    
    /// Makes a new font with the specified variant, size
    public convenience init(graphik: Graphik, size: CGFloat) {
        self.init(descriptor: graphik, size: size)
    }
    
}

Now our UI code can simply create an instance of this font.

let font = UIFont(graphik: .regular, size: 16)

Conclusion

This API provides a clean and simple approach that provides various benefits:

  1. Typed font names
  2. Dynamic type support
  3. Dynamic font loading
  4. Font caching