Skip to content

symentis/Palau

Repository files navigation

Build Status CocoaPods Compatible Carthage Compatible Platform swiftyness @elmkretzer @madhavajay

#Palau: NSUserDefaults with Wings!


Features | Included Types | Installation | Validators and Defaults | Custom Types | DidSet Callback |

-------

Features

  • Easily store your Custom Types in NSUserDefaults
  • Most Standard Types Out-of-the-box
  • Per-property based Chainable Rules
  • Supports NSCoding and RawRepresentable
  • 300% Type Safe :P
  • 100% Unit Test Coverage
  • Swift 3 features coming!

Already Included Types

Swift

  • Bool
  • Int
  • UInt
  • Float
  • Double
  • String
  • Array
  • Dictionary

Foundation

  • NSNumber
  • NSString
  • NSArray
  • NSDictionary
  • NSDate
  • NSData
  • UIColor

Requirements

  • Swift 2.2
  • iOS 8.0+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 7.3+

Installation

Carthage

To integrate Palau into your project using Carthage, add to your Cartfile:

github "symentis/Palau" ~> 1.0

Run carthage update to build the framework and drag the built Palau.framework into your Xcode project. See more instructions on the Carthage page.

CocoaPods

To integrate Palau into your project using CocoaPods, add to your Podfile:

use_frameworks!

pod 'Palau', '~> 1.0'

Run pod install to install the framework into your Xcode workspace.

Usage

Import Palau

import Palau

Once you import the framework you can setup PalauDefaults like:

/// Your defaults are defined as extension of PalauDefaults
///
/// - The generic type of your PalauDefaultsEntry must conform 
///   to the protocol PalauDefaultable.
/// - We provide support for the most common types. 
/// - `value` is a helper function defined in PalauDefaults
/// - The String "backingName" is the key used in NSUserDefaults
/// - The empty `set` is used to please the compiler
extension PalauDefaults {
  /// a NSUserDefaults Entry of Type String with the key "backingName"
  public static var name: PalauDefaultsEntry<String> {
    get { return value("backingName") }
    set { }
  }
}

Set

Every value of a PalauDefaultsEntry will always be optional. If you want to set a value you call:

PalauDefaults.name.value = "I am a great String value!"

Get

Getting your value back is as easy as:

/// name is an Optional<String>
let name = PalauDefaults.name.value

Delete

You can delete a property by setting it to nil:

PalauDefaults.name.value = nil

Or

/// skip custom rules and delete
PalauDefaults.name.clear()

Custom Rules

Providing a Default Value

If you want to provide a default, when there is no value set, you can write a custom rule. This allows fine granular control on your values.

We include two rule types by default: whenNil and ensure

import Palau
extension PalauDefaults {
  public static var color: PalauDefaultsEntry<UIColor> {
    /// whenNil provides a value that will be returned
    /// when the related NSUserDefaults value is nil 
    /// (e.g. the 1st time, or after clear)
    get { return value("color").whenNil(use: UIColor.redColor())  }
    set { }
  }
}

/// is UIColor.redColor() 
let color: UIColor? = PalauDefaults.color.value

Providing a Validator

You can also build up arbitrary rules for your value like:

/// Custom Validator Closure
let lessThan10: Int? -> Bool = {
  return $0.map { $0 < 10 } ?? false
}

/// the PalauDefaultsEntry with the key "intValueMin10" has 2 rules
/// - 1. when the value is nil - we will get or set 10
/// - 2. when the value is less than 10 (see lessThan10 closure) - we will also get or set 10
/// - Add as many chainable rules as you like
public static var intValueMin10: PalauDefaultsEntry<Int> {
  get { return value("intValue")
    .whenNil(use: 10)
    .ensure(when: lessThan10, use: 10) }
  set { }
}

/// try setting the property to 8
PalauDefaults.intValueMin10.value = 8
/// property ensured to be >= 10
assert(PalauDefaults.intValueMin10.value == 10)
/// try setting the property to 11
PalauDefaults.intValueMin10.value = 11
/// property changed to 11
assert(PalauDefaults.intValueMin10.value == 11)

Custom Types

In Swift 2.2 Classes and Protocols can be used to constrain the ValueType. For example this is how Palau adds support for RawRepresentable via an Extension:

/// Extension for RawRepresentable types aka enums
extension PalauDefaultable where ValueType: RawRepresentable {

  public static func get(key: String, from defaults: NSUD) -> ValueType? {
    guard let val = defaults.objectForKey(key) as? ValueType.RawValue else { return nil }
    return ValueType(rawValue: val)
  }

  public static func set(value: ValueType?, forKey key: String, in defaults: NSUD) -> Void {
    guard let value = value?.rawValue as? AnyObject else { return defaults.removeObjectForKey(key) }
    defaults.setObject(value, forKey: key)
  }
}

Generally for Types which conform to NSCoding you can usually just provide an extension like so:

/// Make UIColor PalauDefaultable
extension UIColor: PalauDefaultable {
  public typealias ValueType = UIColor
}

Look Mum, even Structs!

For custom types you can provide an extension on your type for PalauDefaultable, to implement a get and a get function.

// example Struct called Structy for demonstrating we can save a Struct with Palau
public struct Structy {
  let tuple: (String, String)
}

// our Structy PalauDefaultable extension allowing the mapping between PalauDefaults and the Type
// here we just map the two values to two keys named "1" and "2"
extension Structy: PalauDefaultable {
  public static func get(key: String, from defaults: NSUD) -> Structy? {
    guard let d = defaults.objectForKey(key) as? [String: AnyObject] ,
      let t1 = d["1"] as? String,
      let t2 = d["2"] as? String else { return nil }
    return Structy(tuple: (t1, t2))
  }

  public static func set(value: Structy?, forKey key: String, in defaults: NSUD) -> Void {
    guard let value = value else { return defaults.setObject(nil, forKey: key) }
    defaults.setObject(["1": value.tuple.0, "2": value.tuple.1], forKey: key)
  }
}

// now create a property on PalauDefaults
extension PalauDefaults {
  public static var structWithTuple: PalauDefaultsEntry<Structy> {
    get { return value("structy") }
    set { }
  }
}

DidSet Callback

You can easily register a didSet callback, which gets fired when the value has changed.

extension PalauDefaults {
  public static var strings: PalauDefaultsEntry<[String]> {
    get { return value("strings").didSet({ print("changed to:", $0, "from:", $1) }) }
    set { }
  }
}

Limitations for Swift 2.2

We are waiting for more Swift 3 generics features like extensions on Generic types.... yay!

Then we get even more type saftey on arrays and dictionaries. Plus we might be able to make generic types conform to PalauDefaultable.

FAQ

What's the origin of the name Palau?

Palau is named after the Palau swiftlet, a species of swift, endemic to the island of Palau.

Btw - if you really don`t like the name, you can use a typealias

typealias Defaults = PalauDefaults
typealias Defaultable = PalauDefaultable
/// for Swift 3 even:
/// typealias DefaultsEntry<T> = PalauDefaultsEntry<T>

Credits

Palau is owned and maintained by Symentis GmbH.

Developed by: Elmar Kretzer & Madhava Jay

Follow for more Swift Goodness: Twitter Twitter

##Logo

Awesome Logo by: 4th motion

String Test Fixtures

Markus Kuhn

License

Palau is released under the Apache 2.0 license. See LICENSE for details.