Add support for key observation and other improvements (#10)
And some other improvements: - Shorter syntax for defining keys. - It now writes the default value of `Defaults.Key` back to the actual UserDefaults on creation. - Support alternative UserDefaults suites. - Improved docs and test coverage.
This commit is contained in:
parent
1912c5b20a
commit
257ce0df90
|
@ -1,3 +1,3 @@
|
||||||
language: swift
|
language: swift
|
||||||
osx_image: xcode9.3
|
osx_image: xcode10
|
||||||
script: xcodebuild test -project Defaults.xcodeproj -scheme Defaults-macOS -quiet
|
script: xcodebuild test -project Defaults.xcodeproj -scheme Defaults-macOS
|
||||||
|
|
|
@ -17,6 +17,14 @@
|
||||||
8933C7901EB5B82D000D00A4 /* DefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7891EB5B82A000D00A4 /* DefaultsTests.swift */; };
|
8933C7901EB5B82D000D00A4 /* DefaultsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8933C7891EB5B82A000D00A4 /* DefaultsTests.swift */; };
|
||||||
DD7502881C68FEDE006590AF /* Defaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* Defaults.framework */; };
|
DD7502881C68FEDE006590AF /* Defaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6DA0F1BF000BD002C0205 /* Defaults.framework */; };
|
||||||
DD7502921C690C7A006590AF /* Defaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* Defaults.framework */; };
|
DD7502921C690C7A006590AF /* Defaults.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 52D6D9F01BEFFFBE002C0205 /* Defaults.framework */; };
|
||||||
|
E3EB3E33216505920033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
|
||||||
|
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
|
||||||
|
E3EB3E36216507B50033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
|
||||||
|
E3EB3E37216507B50033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
|
||||||
|
E3EB3E38216507B60033B089 /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E34216507AE0033B089 /* Observation.swift */; };
|
||||||
|
E3EB3E39216507C30033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
|
||||||
|
E3EB3E3A216507C40033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
|
||||||
|
E3EB3E3B216507C40033B089 /* util.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3EB3E32216505920033B089 /* util.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -55,6 +63,8 @@
|
||||||
AD2FAA281CD0B6E100659CF4 /* DefaultsTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DefaultsTests.plist; sourceTree = "<group>"; };
|
AD2FAA281CD0B6E100659CF4 /* DefaultsTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = DefaultsTests.plist; sourceTree = "<group>"; };
|
||||||
DD75027A1C68FCFC006590AF /* Defaults-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Defaults-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
DD75027A1C68FCFC006590AF /* Defaults-macOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Defaults-macOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
DD75028D1C690C7A006590AF /* Defaults-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Defaults-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
DD75028D1C690C7A006590AF /* Defaults-tvOS Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Defaults-tvOS Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
E3EB3E32216505920033B089 /* util.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = util.swift; sourceTree = "<group>"; usesTabs = 1; };
|
||||||
|
E3EB3E34216507AE0033B089 /* Observation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Observation.swift; sourceTree = "<group>"; usesTabs = 1; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -151,6 +161,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
8933C7841EB5B820000D00A4 /* Defaults.swift */,
|
8933C7841EB5B820000D00A4 /* Defaults.swift */,
|
||||||
|
E3EB3E34216507AE0033B089 /* Observation.swift */,
|
||||||
|
E3EB3E32216505920033B089 /* util.swift */,
|
||||||
);
|
);
|
||||||
path = Sources;
|
path = Sources;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -352,19 +364,19 @@
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
52D6D97B1BEFF229002C0205 = {
|
52D6D97B1BEFF229002C0205 = {
|
||||||
CreatedOnToolsVersion = 7.1;
|
CreatedOnToolsVersion = 7.1;
|
||||||
LastSwiftMigration = 0800;
|
LastSwiftMigration = 1000;
|
||||||
};
|
};
|
||||||
52D6D9851BEFF229002C0205 = {
|
52D6D9851BEFF229002C0205 = {
|
||||||
CreatedOnToolsVersion = 7.1;
|
CreatedOnToolsVersion = 7.1;
|
||||||
LastSwiftMigration = 0800;
|
LastSwiftMigration = 1000;
|
||||||
};
|
};
|
||||||
52D6D9E11BEFFF6E002C0205 = {
|
52D6D9E11BEFFF6E002C0205 = {
|
||||||
CreatedOnToolsVersion = 7.1;
|
CreatedOnToolsVersion = 7.1;
|
||||||
LastSwiftMigration = 0800;
|
LastSwiftMigration = 1000;
|
||||||
};
|
};
|
||||||
52D6D9EF1BEFFFBE002C0205 = {
|
52D6D9EF1BEFFFBE002C0205 = {
|
||||||
CreatedOnToolsVersion = 7.1;
|
CreatedOnToolsVersion = 7.1;
|
||||||
LastSwiftMigration = 0800;
|
LastSwiftMigration = 1000;
|
||||||
};
|
};
|
||||||
52D6DA0E1BF000BD002C0205 = {
|
52D6DA0E1BF000BD002C0205 = {
|
||||||
CreatedOnToolsVersion = 7.1;
|
CreatedOnToolsVersion = 7.1;
|
||||||
|
@ -376,7 +388,7 @@
|
||||||
};
|
};
|
||||||
DD75028C1C690C7A006590AF = {
|
DD75028C1C690C7A006590AF = {
|
||||||
CreatedOnToolsVersion = 7.2.1;
|
CreatedOnToolsVersion = 7.2.1;
|
||||||
LastSwiftMigration = 0800;
|
LastSwiftMigration = 1000;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -384,15 +396,18 @@
|
||||||
compatibilityVersion = "Xcode 10.0";
|
compatibilityVersion = "Xcode 10.0";
|
||||||
developmentRegion = English;
|
developmentRegion = English;
|
||||||
hasScannedForEncodings = 0;
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
);
|
||||||
mainGroup = 52D6D9721BEFF229002C0205;
|
mainGroup = 52D6D9721BEFF229002C0205;
|
||||||
productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */;
|
productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
52D6D97B1BEFF229002C0205 /* Defaults-iOS */,
|
|
||||||
52D6DA0E1BF000BD002C0205 /* Defaults-macOS */,
|
52D6DA0E1BF000BD002C0205 /* Defaults-macOS */,
|
||||||
52D6D9E11BEFFF6E002C0205 /* Defaults-watchOS */,
|
52D6D97B1BEFF229002C0205 /* Defaults-iOS */,
|
||||||
52D6D9EF1BEFFFBE002C0205 /* Defaults-tvOS */,
|
52D6D9EF1BEFFFBE002C0205 /* Defaults-tvOS */,
|
||||||
|
52D6D9E11BEFFF6E002C0205 /* Defaults-watchOS */,
|
||||||
52D6D9851BEFF229002C0205 /* Defaults-iOS Tests */,
|
52D6D9851BEFF229002C0205 /* Defaults-iOS Tests */,
|
||||||
DD7502791C68FCFC006590AF /* Defaults-macOS Tests */,
|
DD7502791C68FCFC006590AF /* Defaults-macOS Tests */,
|
||||||
DD75028C1C690C7A006590AF /* Defaults-tvOS Tests */,
|
DD75028C1C690C7A006590AF /* Defaults-tvOS Tests */,
|
||||||
|
@ -458,6 +473,8 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */,
|
8933C7851EB5B820000D00A4 /* Defaults.swift in Sources */,
|
||||||
|
E3EB3E35216507AE0033B089 /* Observation.swift in Sources */,
|
||||||
|
E3EB3E33216505920033B089 /* util.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -473,6 +490,8 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E3EB3E3A216507C40033B089 /* util.swift in Sources */,
|
||||||
|
E3EB3E37216507B50033B089 /* Observation.swift in Sources */,
|
||||||
8933C7871EB5B820000D00A4 /* Defaults.swift in Sources */,
|
8933C7871EB5B820000D00A4 /* Defaults.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -481,6 +500,8 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E3EB3E3B216507C40033B089 /* util.swift in Sources */,
|
||||||
|
E3EB3E38216507B60033B089 /* Observation.swift in Sources */,
|
||||||
8933C7881EB5B820000D00A4 /* Defaults.swift in Sources */,
|
8933C7881EB5B820000D00A4 /* Defaults.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -489,6 +510,8 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E3EB3E39216507C30033B089 /* util.swift in Sources */,
|
||||||
|
E3EB3E36216507B50033B089 /* Observation.swift in Sources */,
|
||||||
8933C7861EB5B820000D00A4 /* Defaults.swift in Sources */,
|
8933C7861EB5B820000D00A4 /* Defaults.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
@ -668,7 +691,7 @@
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_COMPILATION_MODE = singlefile;
|
SWIFT_COMPILATION_MODE = singlefile;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
@ -695,7 +718,7 @@
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
@ -716,7 +739,7 @@
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_COMPILATION_MODE = singlefile;
|
SWIFT_COMPILATION_MODE = singlefile;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
|
@ -737,7 +760,7 @@
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
|
@ -761,7 +784,7 @@
|
||||||
PRODUCT_NAME = Defaults;
|
PRODUCT_NAME = Defaults;
|
||||||
SDKROOT = watchos;
|
SDKROOT = watchos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
TARGETED_DEVICE_FAMILY = 4;
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
WATCHOS_DEPLOYMENT_TARGET = 3.0;
|
WATCHOS_DEPLOYMENT_TARGET = 3.0;
|
||||||
};
|
};
|
||||||
|
@ -789,7 +812,7 @@
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
TARGETED_DEVICE_FAMILY = 4;
|
TARGETED_DEVICE_FAMILY = 4;
|
||||||
WATCHOS_DEPLOYMENT_TARGET = 3.0;
|
WATCHOS_DEPLOYMENT_TARGET = 3.0;
|
||||||
};
|
};
|
||||||
|
@ -815,7 +838,7 @@
|
||||||
PRODUCT_NAME = Defaults;
|
PRODUCT_NAME = Defaults;
|
||||||
SDKROOT = appletvos;
|
SDKROOT = appletvos;
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
TARGETED_DEVICE_FAMILY = 3;
|
TARGETED_DEVICE_FAMILY = 3;
|
||||||
TVOS_DEPLOYMENT_TARGET = 10.0;
|
TVOS_DEPLOYMENT_TARGET = 10.0;
|
||||||
};
|
};
|
||||||
|
@ -843,7 +866,7 @@
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
TARGETED_DEVICE_FAMILY = 3;
|
TARGETED_DEVICE_FAMILY = 3;
|
||||||
TVOS_DEPLOYMENT_TARGET = 10.0;
|
TVOS_DEPLOYMENT_TARGET = 10.0;
|
||||||
};
|
};
|
||||||
|
@ -908,7 +931,6 @@
|
||||||
DD7502831C68FCFC006590AF /* Debug */ = {
|
DD7502831C68FCFC006590AF /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_IDENTITY = "-";
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
INFOPLIST_FILE = Configs/DefaultsTests.plist;
|
INFOPLIST_FILE = Configs/DefaultsTests.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
@ -927,7 +949,6 @@
|
||||||
DD7502841C68FCFC006590AF /* Release */ = {
|
DD7502841C68FCFC006590AF /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
CODE_SIGN_IDENTITY = "-";
|
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
INFOPLIST_FILE = Configs/DefaultsTests.plist;
|
INFOPLIST_FILE = Configs/DefaultsTests.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
@ -948,6 +969,8 @@
|
||||||
DD7502961C690C7A006590AF /* Debug */ = {
|
DD7502961C690C7A006590AF /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
INFOPLIST_FILE = Configs/DefaultsTests.plist;
|
INFOPLIST_FILE = Configs/DefaultsTests.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -956,8 +979,9 @@
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.Defaults.Defaults-tvOS-Tests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.Defaults.Defaults-tvOS-Tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SDKROOT = appletvos;
|
SDKROOT = appletvos;
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
TVOS_DEPLOYMENT_TARGET = 10.0;
|
TVOS_DEPLOYMENT_TARGET = 10.0;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
|
@ -965,6 +989,8 @@
|
||||||
DD7502971C690C7A006590AF /* Release */ = {
|
DD7502971C690C7A006590AF /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
CODE_SIGN_STYLE = Manual;
|
||||||
|
DEVELOPMENT_TEAM = "";
|
||||||
INFOPLIST_FILE = Configs/DefaultsTests.plist;
|
INFOPLIST_FILE = Configs/DefaultsTests.plist;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
|
@ -973,10 +999,11 @@
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "com.Defaults.Defaults-tvOS-Tests";
|
PRODUCT_BUNDLE_IDENTIFIER = "com.Defaults.Defaults-tvOS-Tests";
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SDKROOT = appletvos;
|
SDKROOT = appletvos;
|
||||||
SWIFT_COMPILATION_MODE = wholemodule;
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
SWIFT_VERSION = 4.0;
|
SWIFT_VERSION = 4.2;
|
||||||
TVOS_DEPLOYMENT_TARGET = 10.0;
|
TVOS_DEPLOYMENT_TARGET = 10.0;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
|
|
|
@ -26,11 +26,12 @@
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
codeCoverageEnabled = "YES"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO">
|
skipped = "NO"
|
||||||
|
parallelizable = "YES"
|
||||||
|
testExecutionOrdering = "random">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "52D6D9851BEFF229002C0205"
|
BlueprintIdentifier = "52D6D9851BEFF229002C0205"
|
||||||
|
|
|
@ -26,11 +26,12 @@
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
codeCoverageEnabled = "YES"
|
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO">
|
skipped = "NO"
|
||||||
|
parallelizable = "YES"
|
||||||
|
testExecutionOrdering = "random">
|
||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "DD75028C1C690C7A006590AF"
|
BlueprintIdentifier = "DD75028C1C690C7A006590AF"
|
||||||
|
|
|
@ -3,48 +3,66 @@ import Foundation
|
||||||
|
|
||||||
public final class Defaults {
|
public final class Defaults {
|
||||||
public class Keys {
|
public class Keys {
|
||||||
|
public typealias Key = Defaults.Key
|
||||||
|
public typealias OptionalKey = Defaults.OptionalKey
|
||||||
|
|
||||||
fileprivate init() {}
|
fileprivate init() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class Key<T: Codable>: Keys {
|
public final class Key<T: Codable>: Keys {
|
||||||
fileprivate let name: String
|
public let name: String
|
||||||
fileprivate let defaultValue: T
|
public let defaultValue: T
|
||||||
|
public let suite: UserDefaults
|
||||||
|
|
||||||
public init(_ key: String, default defaultValue: T) {
|
public init(_ key: String, default defaultValue: T, suite: UserDefaults = .standard) {
|
||||||
self.name = key
|
self.name = key
|
||||||
self.defaultValue = defaultValue
|
self.defaultValue = defaultValue
|
||||||
|
self.suite = suite
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
// Sets the default value in the actual UserDefaults, so it can be used in other contexts, like binding.
|
||||||
|
if UserDefaults.isNativelySupportedType(T.self) {
|
||||||
|
suite.register(defaults: [key: defaultValue])
|
||||||
|
} else if let value = suite._encode(defaultValue) {
|
||||||
|
suite.register(defaults: [key: value])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class OptionalKey<T: Codable>: Keys {
|
public final class OptionalKey<T: Codable>: Keys {
|
||||||
fileprivate let name: String
|
public let name: String
|
||||||
|
public let suite: UserDefaults
|
||||||
|
|
||||||
public init(_ key: String) {
|
public init(_ key: String, suite: UserDefaults = .standard) {
|
||||||
self.name = key
|
self.name = key
|
||||||
|
self.suite = suite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate init() {}
|
||||||
|
|
||||||
public subscript<T: Codable>(key: Defaults.Key<T>) -> T {
|
public subscript<T: Codable>(key: Defaults.Key<T>) -> T {
|
||||||
get {
|
get {
|
||||||
return UserDefaults.standard[key]
|
return key.suite[key]
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
UserDefaults.standard[key] = newValue
|
key.suite[key] = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public subscript<T: Codable>(key: Defaults.OptionalKey<T>) -> T? {
|
public subscript<T: Codable>(key: Defaults.OptionalKey<T>) -> T? {
|
||||||
get {
|
get {
|
||||||
return UserDefaults.standard[key]
|
return key.suite[key]
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
UserDefaults.standard[key] = newValue
|
key.suite[key] = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func clear() {
|
public func clear(suite: UserDefaults = .standard) {
|
||||||
for key in UserDefaults.standard.dictionaryRepresentation().keys {
|
for key in suite.dictionaryRepresentation().keys {
|
||||||
UserDefaults.standard.removeObject(forKey: key)
|
suite.removeObject(forKey: key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +72,7 @@ public let defaults = Defaults()
|
||||||
|
|
||||||
extension UserDefaults {
|
extension UserDefaults {
|
||||||
private func _get<T: Codable>(_ key: String) -> T? {
|
private func _get<T: Codable>(_ key: String) -> T? {
|
||||||
if isNativelySupportedType(T.self) {
|
if UserDefaults.isNativelySupportedType(T.self) {
|
||||||
return object(forKey: key) as? T
|
return object(forKey: key) as? T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,24 +92,28 @@ extension UserDefaults {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func _set<T: Codable>(_ key: String, to value: T) {
|
fileprivate func _encode<T: Codable>(_ value: T) -> String? {
|
||||||
if isNativelySupportedType(T.self) {
|
|
||||||
set(value, forKey: key)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
do {
|
||||||
// Some codable values like URL and enum are encoded as a top-level
|
// Some codable values like URL and enum are encoded as a top-level
|
||||||
// string which JSON can't handle, so we need to wrap it in an array
|
// string which JSON can't handle, so we need to wrap it in an array
|
||||||
// We need this: https://forums.swift.org/t/allowing-top-level-fragments-in-jsondecoder/11750
|
// We need this: https://forums.swift.org/t/allowing-top-level-fragments-in-jsondecoder/11750
|
||||||
let data = try JSONEncoder().encode([value])
|
let data = try JSONEncoder().encode([value])
|
||||||
let string = String(data: data, encoding: .utf8)?.dropFirst().dropLast()
|
return String(String(data: data, encoding: .utf8)!.dropFirst().dropLast())
|
||||||
set(string, forKey: key)
|
|
||||||
} catch {
|
} catch {
|
||||||
print(error)
|
print(error)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func _set<T: Codable>(_ key: String, to value: T) {
|
||||||
|
if UserDefaults.isNativelySupportedType(T.self) {
|
||||||
|
set(value, forKey: key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
set(_encode(value), forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
public subscript<T: Codable>(key: Defaults.Key<T>) -> T {
|
public subscript<T: Codable>(key: Defaults.Key<T>) -> T {
|
||||||
get {
|
get {
|
||||||
return _get(key.name) ?? key.defaultValue
|
return _get(key.name) ?? key.defaultValue
|
||||||
|
@ -115,7 +137,7 @@ extension UserDefaults {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isNativelySupportedType<T>(_ type: T.Type) -> Bool {
|
fileprivate static func isNativelySupportedType<T>(_ type: T.Type) -> Bool {
|
||||||
switch type {
|
switch type {
|
||||||
case is Bool.Type,
|
case is Bool.Type,
|
||||||
is String.Type,
|
is String.Type,
|
||||||
|
|
|
@ -0,0 +1,174 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// TODO: Nest this inside `Defaults` if Swift ever supported nested protocols.
|
||||||
|
public protocol DefaultsObservation {
|
||||||
|
func invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Defaults {
|
||||||
|
private static func deserialize<T: Decodable>(_ value: Any?, to type: T.Type) -> T? {
|
||||||
|
guard
|
||||||
|
let value = value,
|
||||||
|
!(value is NSNull)
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// This handles the case where the value was a plist value using `isNativelySupportedType`
|
||||||
|
if let value = value as? T {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using the array trick as done below in `UserDefaults#_set()`
|
||||||
|
return [T].init(jsonString: "\([value])")?.first
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate final class BaseChange {
|
||||||
|
fileprivate let kind: NSKeyValueChange
|
||||||
|
fileprivate let indexes: IndexSet?
|
||||||
|
fileprivate let isPrior: Bool
|
||||||
|
fileprivate let newValue: Any?
|
||||||
|
fileprivate let oldValue: Any?
|
||||||
|
|
||||||
|
fileprivate init(change: [NSKeyValueChangeKey: Any]) {
|
||||||
|
kind = NSKeyValueChange(rawValue: change[.kindKey] as! UInt)!
|
||||||
|
indexes = change[.indexesKey] as? IndexSet
|
||||||
|
isPrior = change[.notificationIsPriorKey] as? Bool ?? false
|
||||||
|
oldValue = change[.oldKey]
|
||||||
|
newValue = change[.newKey]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct KeyChange<T: Codable> {
|
||||||
|
public let kind: NSKeyValueChange
|
||||||
|
public let indexes: IndexSet?
|
||||||
|
public let isPrior: Bool
|
||||||
|
public let newValue: T
|
||||||
|
public let oldValue: T
|
||||||
|
|
||||||
|
fileprivate init(change: BaseChange, defaultValue: T) {
|
||||||
|
self.kind = change.kind
|
||||||
|
self.indexes = change.indexes
|
||||||
|
self.isPrior = change.isPrior
|
||||||
|
self.oldValue = deserialize(change.oldValue, to: T.self) ?? defaultValue
|
||||||
|
self.newValue = deserialize(change.newValue, to: T.self) ?? defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct OptionalKeyChange<T: Codable> {
|
||||||
|
public let kind: NSKeyValueChange
|
||||||
|
public let indexes: IndexSet?
|
||||||
|
public let isPrior: Bool
|
||||||
|
public let newValue: T?
|
||||||
|
public let oldValue: T?
|
||||||
|
|
||||||
|
fileprivate init(change: BaseChange) {
|
||||||
|
self.kind = change.kind
|
||||||
|
self.indexes = change.indexes
|
||||||
|
self.isPrior = change.isPrior
|
||||||
|
self.oldValue = deserialize(change.oldValue, to: T.self)
|
||||||
|
self.newValue = deserialize(change.newValue, to: T.self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class UserDefaultsKeyObservation: NSObject, DefaultsObservation {
|
||||||
|
fileprivate typealias Callback = (BaseChange) -> Void
|
||||||
|
|
||||||
|
private weak var object: UserDefaults?
|
||||||
|
private let key: String
|
||||||
|
private let callback: Callback
|
||||||
|
|
||||||
|
fileprivate init(object: UserDefaults, key: String, callback: @escaping Callback) {
|
||||||
|
self.object = object
|
||||||
|
self.key = key
|
||||||
|
self.callback = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileprivate func start(options: NSKeyValueObservingOptions) {
|
||||||
|
object?.addObserver(self, forKeyPath: key, options: options, context: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func invalidate() {
|
||||||
|
object?.removeObserver(self, forKeyPath: key, context: nil)
|
||||||
|
object = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next block_based_kvo
|
||||||
|
override func observeValue(
|
||||||
|
forKeyPath keyPath: String?,
|
||||||
|
of object: Any?,
|
||||||
|
change: [NSKeyValueChangeKey: Any]?, // swiftlint:disable:this discouraged_optional_collection
|
||||||
|
context: UnsafeMutableRawPointer?
|
||||||
|
) {
|
||||||
|
guard
|
||||||
|
let selfObject = self.object,
|
||||||
|
selfObject == object as? NSObject,
|
||||||
|
let change = change
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(BaseChange(change: change))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Observe a defaults key
|
||||||
|
|
||||||
|
```
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let observer = defaults.observe(.isUnicornMode) { change in
|
||||||
|
print(change.newValue)
|
||||||
|
//=> false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
public func observe<T: Codable>(
|
||||||
|
_ key: Defaults.Key<T>,
|
||||||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||||||
|
handler: @escaping (KeyChange<T>) -> Void
|
||||||
|
) -> DefaultsObservation {
|
||||||
|
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
||||||
|
handler(
|
||||||
|
KeyChange<T>(change: change, defaultValue: key.defaultValue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
observation.start(options: options)
|
||||||
|
return observation
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Observe an optional defaults key
|
||||||
|
|
||||||
|
```
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let isUnicornMode = OptionalKey<Bool>("isUnicornMode")
|
||||||
|
}
|
||||||
|
|
||||||
|
let observer = defaults.observe(.isUnicornMode) { change in
|
||||||
|
print(change.newValue)
|
||||||
|
//=> Optional(nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
*/
|
||||||
|
public func observe<T: Codable>(
|
||||||
|
_ key: Defaults.OptionalKey<T>,
|
||||||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||||||
|
handler: @escaping (OptionalKeyChange<T>) -> Void
|
||||||
|
) -> DefaultsObservation {
|
||||||
|
let observation = UserDefaultsKeyObservation(object: key.suite, key: key.name) { change in
|
||||||
|
handler(
|
||||||
|
OptionalKeyChange<T>(change: change)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
observation.start(options: options)
|
||||||
|
return observation
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Decodable {
|
||||||
|
init?(jsonData: Data) {
|
||||||
|
guard let value = try? JSONDecoder().decode(Self.self, from: jsonData) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self = value
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(jsonString: String) {
|
||||||
|
guard let data = jsonString.data(using: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.init(jsonData: data)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,8 @@ import Foundation
|
||||||
import XCTest
|
import XCTest
|
||||||
import Defaults
|
import Defaults
|
||||||
|
|
||||||
let fixtureUrl = URL(string: "https://sindresorhus.com")!
|
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
||||||
|
let fixtureURL2 = URL(string: "https://example.com")!
|
||||||
|
|
||||||
enum FixtureEnum: String, Codable {
|
enum FixtureEnum: String, Codable {
|
||||||
case tenMinutes = "10 Minutes"
|
case tenMinutes = "10 Minutes"
|
||||||
|
@ -10,11 +11,14 @@ enum FixtureEnum: String, Codable {
|
||||||
case oneHour = "1 Hour"
|
case oneHour = "1 Hour"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let fixtureDate = Date()
|
||||||
|
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let key = Defaults.Key<Bool>("key", default: false)
|
static let key = Key<Bool>("key", default: false)
|
||||||
static let url = Defaults.Key<URL>("url", default: fixtureUrl)
|
static let url = Key<URL>("url", default: fixtureURL)
|
||||||
static let `enum` = Defaults.Key<FixtureEnum>("enum", default: .oneHour)
|
static let `enum` = Key<FixtureEnum>("enum", default: .oneHour)
|
||||||
static let data = Defaults.Key<Data>("data", default: Data(bytes: []))
|
static let data = Key<Data>("data", default: Data(bytes: []))
|
||||||
|
static let date = Key<Date>("date", default: fixtureDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class DefaultsTests: XCTestCase {
|
final class DefaultsTests: XCTestCase {
|
||||||
|
@ -23,22 +27,46 @@ final class DefaultsTests: XCTestCase {
|
||||||
defaults.clear()
|
defaults.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
super.setUp()
|
||||||
|
defaults.clear()
|
||||||
|
}
|
||||||
|
|
||||||
func testKey() {
|
func testKey() {
|
||||||
let key = Defaults.Key<Bool>("key", default: false)
|
let key = Defaults.Key<Bool>("independentKey", default: false)
|
||||||
XCTAssertFalse(UserDefaults.standard[key])
|
XCTAssertFalse(defaults[key])
|
||||||
UserDefaults.standard[key] = true
|
defaults[key] = true
|
||||||
XCTAssertTrue(UserDefaults.standard[key])
|
XCTAssertTrue(defaults[key])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testOptionalKey() {
|
func testOptionalKey() {
|
||||||
let key = Defaults.OptionalKey<Bool>("key")
|
let key = Defaults.OptionalKey<Bool>("independentOptionalKey")
|
||||||
XCTAssertNil(UserDefaults.standard[key])
|
XCTAssertNil(defaults[key])
|
||||||
|
defaults[key] = true
|
||||||
|
XCTAssertTrue(defaults[key]!)
|
||||||
|
defaults[key] = nil
|
||||||
|
XCTAssertNil(defaults[key])
|
||||||
|
defaults[key] = false
|
||||||
|
XCTAssertFalse(defaults[key]!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyRegistersDefault() {
|
||||||
|
let keyName = "registersDefault"
|
||||||
|
XCTAssertEqual(UserDefaults.standard.bool(forKey: keyName), false)
|
||||||
|
_ = Defaults.Key<Bool>(keyName, default: true)
|
||||||
|
XCTAssertEqual(UserDefaults.standard.bool(forKey: keyName), true)
|
||||||
|
|
||||||
|
// Test that it works with multiple keys with defaults
|
||||||
|
let keyName2 = "registersDefault2"
|
||||||
|
_ = Defaults.Key<String>(keyName2, default: keyName2)
|
||||||
|
XCTAssertEqual(UserDefaults.standard.string(forKey: keyName2), keyName2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyWithUserDefaultSubscript() {
|
||||||
|
let key = Defaults.Key<Bool>("keyWithUserDeaultSubscript", default: false)
|
||||||
|
XCTAssertFalse(UserDefaults.standard[key])
|
||||||
UserDefaults.standard[key] = true
|
UserDefaults.standard[key] = true
|
||||||
XCTAssertTrue(UserDefaults.standard[key]!)
|
XCTAssertTrue(UserDefaults.standard[key])
|
||||||
UserDefaults.standard[key] = nil
|
|
||||||
XCTAssertNil(UserDefaults.standard[key])
|
|
||||||
UserDefaults.standard[key] = false
|
|
||||||
XCTAssertFalse(UserDefaults.standard[key]!)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func testKeys() {
|
func testKeys() {
|
||||||
|
@ -48,7 +76,7 @@ final class DefaultsTests: XCTestCase {
|
||||||
}
|
}
|
||||||
|
|
||||||
func testUrlType() {
|
func testUrlType() {
|
||||||
XCTAssertEqual(defaults[.url], fixtureUrl)
|
XCTAssertEqual(defaults[.url], fixtureURL)
|
||||||
|
|
||||||
let newUrl = URL(string: "https://twitter.com")!
|
let newUrl = URL(string: "https://twitter.com")!
|
||||||
defaults[.url] = newUrl
|
defaults[.url] = newUrl
|
||||||
|
@ -67,10 +95,100 @@ final class DefaultsTests: XCTestCase {
|
||||||
XCTAssertEqual(defaults[.data], newData)
|
XCTAssertEqual(defaults[.data], newData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testDateType() {
|
||||||
|
XCTAssertEqual(defaults[.date], fixtureDate)
|
||||||
|
|
||||||
|
let newDate = Date()
|
||||||
|
defaults[.date] = newDate
|
||||||
|
XCTAssertEqual(defaults[.date], newDate)
|
||||||
|
}
|
||||||
|
|
||||||
func testClear() {
|
func testClear() {
|
||||||
defaults[.key] = true
|
let key = Defaults.Key<Bool>("clear", default: false)
|
||||||
XCTAssertTrue(defaults[.key])
|
defaults[key] = true
|
||||||
|
XCTAssertTrue(defaults[key])
|
||||||
defaults.clear()
|
defaults.clear()
|
||||||
XCTAssertFalse(defaults[.key])
|
XCTAssertFalse(defaults[key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCustomSuite() {
|
||||||
|
let customSuite = UserDefaults(suiteName: "com.sindresorhus.customSuite")!
|
||||||
|
let key = Defaults.Key<Bool>("customSuite", default: false, suite: customSuite)
|
||||||
|
XCTAssertFalse(customSuite[key])
|
||||||
|
XCTAssertFalse(defaults[key])
|
||||||
|
defaults[key] = true
|
||||||
|
XCTAssertTrue(customSuite[key])
|
||||||
|
XCTAssertTrue(defaults[key])
|
||||||
|
defaults.clear(suite: customSuite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testObserveKey() {
|
||||||
|
let key = Defaults.Key<Bool>("observeKey", default: false)
|
||||||
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
|
var observation: DefaultsObservation!
|
||||||
|
observation = defaults.observe(key, options: [.old, .new]) { change in
|
||||||
|
XCTAssertFalse(change.oldValue)
|
||||||
|
XCTAssertTrue(change.newValue)
|
||||||
|
observation.invalidate()
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[key] = true
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testObserveOptionalKey() {
|
||||||
|
let key = Defaults.OptionalKey<Bool>("observeOptionalKey")
|
||||||
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
|
var observation: DefaultsObservation!
|
||||||
|
observation = defaults.observe(key, options: [.old, .new]) { change in
|
||||||
|
XCTAssertNil(change.oldValue)
|
||||||
|
XCTAssertTrue(change.newValue!)
|
||||||
|
observation.invalidate()
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[key] = true
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testObserveKeyURL() {
|
||||||
|
let fixtureURL = URL(string: "https://sindresorhus.com")!
|
||||||
|
let fixtureURL2 = URL(string: "https://example.com")!
|
||||||
|
let key = Defaults.Key<URL>("observeKeyURL", default: fixtureURL)
|
||||||
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
|
var observation: DefaultsObservation!
|
||||||
|
observation = defaults.observe(key, options: [.old, .new]) { change in
|
||||||
|
XCTAssertEqual(change.oldValue, fixtureURL)
|
||||||
|
XCTAssertEqual(change.newValue, fixtureURL2)
|
||||||
|
observation.invalidate()
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[key] = fixtureURL2
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testObserveKeyEnum() {
|
||||||
|
let key = Defaults.Key<FixtureEnum>("observeKeyEnum", default: .oneHour)
|
||||||
|
let expect = expectation(description: "Observation closure being called")
|
||||||
|
|
||||||
|
var observation: DefaultsObservation!
|
||||||
|
observation = defaults.observe(key, options: [.old, .new]) { change in
|
||||||
|
XCTAssertEqual(change.oldValue, .oneHour)
|
||||||
|
XCTAssertEqual(change.newValue, .tenMinutes)
|
||||||
|
observation.invalidate()
|
||||||
|
expect.fulfill()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[key] = .tenMinutes
|
||||||
|
|
||||||
|
waitForExpectations(timeout: 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
127
readme.md
127
readme.md
|
@ -8,7 +8,8 @@
|
||||||
- **Strongly typed:** You declare the type and default value upfront.
|
- **Strongly typed:** You declare the type and default value upfront.
|
||||||
- **Codable support:** You can store any [Codable](https://developer.apple.com/documentation/swift/codable) value, like an enum.
|
- **Codable support:** You can store any [Codable](https://developer.apple.com/documentation/swift/codable) value, like an enum.
|
||||||
- **Debuggable:** The data is stored as JSON-serialized values.
|
- **Debuggable:** The data is stored as JSON-serialized values.
|
||||||
- **Lightweight:** It's only ~100 lines of code.
|
- **Observation:** Observe changes to keys.
|
||||||
|
- **Lightweight:** It's only ~300 lines of code.
|
||||||
|
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
@ -53,9 +54,9 @@ import Cocoa
|
||||||
import Defaults
|
import Defaults
|
||||||
|
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let quality = Defaults.Key<Double>("quality", default: 0.8)
|
static let quality = Key<Double>("quality", default: 0.8)
|
||||||
// ^ ^ ^ ^
|
// ^ ^ ^ ^
|
||||||
// Key Type UserDefaults name Default value
|
// Key Type UserDefaults name Default value
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -79,7 +80,7 @@ You can also declare optional keys for when you don't want to declare a default
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let name = Defaults.OptionalKey<Double>("name")
|
static let name = OptionalKey<Double>("name")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let name = defaults[.name] {
|
if let name = defaults[.name] {
|
||||||
|
@ -87,7 +88,6 @@ if let name = defaults[.name] {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Enum example
|
### Enum example
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
@ -98,76 +98,165 @@ enum DurationKeys: String, Codable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let defaultDuration = Defaults.Key<DurationKeys>("defaultDuration", default: .oneHour)
|
static let defaultDuration = Key<DurationKeys>("defaultDuration", default: .oneHour)
|
||||||
}
|
}
|
||||||
|
|
||||||
defaults[.defaultDuration].rawValue
|
defaults[.defaultDuration].rawValue
|
||||||
//=> "1 Hour"
|
//=> "1 Hour"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### It's just UserDefaults with sugar
|
### It's just UserDefaults with sugar
|
||||||
|
|
||||||
This works too:
|
This works too:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
extension Defaults.Keys {
|
extension Defaults.Keys {
|
||||||
static let isUnicorn = Defaults.Key<Bool>("isUnicorn", default: true)
|
static let isUnicorn = Key<Bool>("isUnicorn", default: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
UserDefaults.standard[.isUnicorn]
|
UserDefaults.standard[.isUnicorn]
|
||||||
//=> true
|
//=> true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
### Shared UserDefaults
|
### Shared UserDefaults
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
extension Defaults.Keys {
|
|
||||||
static let isUnicorn = Defaults.Key<Bool>("isUnicorn", default: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
let extensionDefaults = UserDefaults(suiteName: "com.unicorn.app")!
|
let extensionDefaults = UserDefaults(suiteName: "com.unicorn.app")!
|
||||||
|
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let isUnicorn = Key<Bool>("isUnicorn", default: true, suite: extensionDefaults)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[.isUnicorn]
|
||||||
|
//=> true
|
||||||
|
|
||||||
|
// Or
|
||||||
|
|
||||||
extensionDefaults[.isUnicorn]
|
extensionDefaults[.isUnicorn]
|
||||||
//=> true
|
//=> true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Use keys directly
|
||||||
|
|
||||||
|
You are not required to attach keys to `Defaults.Keys`.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let isUnicorn = Defaults.Key<Bool>("isUnicorn", default: true)
|
||||||
|
|
||||||
|
defaults[isUnicorn]
|
||||||
|
//=> true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Observe changes to a key
|
||||||
|
|
||||||
|
```swift
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let isUnicornMode = Key<Bool>("isUnicornMode", default: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
let observer = defaults.observe(.isUnicornMode) { change in
|
||||||
|
// Initial event
|
||||||
|
print(change.oldValue)
|
||||||
|
//=> false
|
||||||
|
print(change.newValue)
|
||||||
|
//=> false
|
||||||
|
|
||||||
|
// First actual event
|
||||||
|
print(change.oldValue)
|
||||||
|
//=> false
|
||||||
|
print(change.newValue)
|
||||||
|
//=> true
|
||||||
|
}
|
||||||
|
|
||||||
|
defaults[.isUnicornMode] = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default values are registered with UserDefaults
|
||||||
|
|
||||||
|
When you create a `Defaults.Key`, it automatically registers the `default` value with normal UserDefaults. This means you can make use of the default value in, for example, bindings in Interface Builder.
|
||||||
|
|
||||||
|
```swift
|
||||||
|
extension Defaults.Keys {
|
||||||
|
static let isUnicornMode = Key<Bool>("isUnicornMode", default: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
print(UserDefaults.standard.bool(forKey: isUnicornMode.name))
|
||||||
|
//=> true
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## API
|
## API
|
||||||
|
|
||||||
### `let defaults = Defaults()`
|
### `let defaults = Defaults()`
|
||||||
|
|
||||||
#### Defaults.Keys
|
#### `Defaults.Keys`
|
||||||
|
|
||||||
Type: `class`
|
Type: `class`
|
||||||
|
|
||||||
Stores the keys.
|
Stores the keys.
|
||||||
|
|
||||||
#### Defaults.Key
|
#### `Defaults.Key` *(alias `Defaults.Keys.Key`)*
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Defaults.Key<T>(_ key: String, default: T, suite: UserDefaults = .standard)
|
||||||
|
```
|
||||||
|
|
||||||
Type: `class`
|
Type: `class`
|
||||||
|
|
||||||
Create a key with a default value.
|
Create a key with a default value.
|
||||||
|
|
||||||
#### Defaults.OptionalKey
|
The default value is written to the actual `UserDefaults` and can be used elsewhere. For example, with Interface Builder binding.
|
||||||
|
|
||||||
|
#### `Defaults.OptionalKey` *(alias `Defaults.Keys.OptionalKey`)*
|
||||||
|
|
||||||
|
```swift
|
||||||
|
Defaults.OptionalKey<T>(_ key: String, suite: UserDefaults = .standard)
|
||||||
|
```
|
||||||
|
|
||||||
Type: `class`
|
Type: `class`
|
||||||
|
|
||||||
Create a key with an optional value.
|
Create a key with an optional value.
|
||||||
|
|
||||||
##### defaults.clear()
|
#### `Defaults#clear`
|
||||||
|
|
||||||
|
```swift
|
||||||
|
clear(suite: UserDefaults = .standard)
|
||||||
|
```
|
||||||
|
|
||||||
Type: `func`
|
Type: `func`
|
||||||
|
|
||||||
Clear the user defaults.
|
Clear the user defaults.
|
||||||
|
|
||||||
|
#### `Defaults#observe`
|
||||||
|
|
||||||
|
```swift
|
||||||
|
observe<T: Codable>(
|
||||||
|
_ key: Defaults.Key<T>,
|
||||||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||||||
|
handler: @escaping (KeyChange<T>) -> Void
|
||||||
|
) -> DefaultsObservation
|
||||||
|
```
|
||||||
|
|
||||||
|
```swift
|
||||||
|
observe<T: Codable>(
|
||||||
|
_ key: Defaults.OptionalKey<T>,
|
||||||
|
options: NSKeyValueObservingOptions = [.initial, .old, .new],
|
||||||
|
handler: @escaping (OptionalKeyChange<T>) -> Void
|
||||||
|
) -> DefaultsObservation
|
||||||
|
```
|
||||||
|
|
||||||
|
Type: `func`
|
||||||
|
|
||||||
|
Observe changes to a key or an optional key.
|
||||||
|
|
||||||
|
By default, it will also trigger an initial event on creation. This can be useful for setting default values on controls. You can override this behavior with the `options` argument.
|
||||||
|
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
|
|
||||||
### How is this different from [`SwiftyUserDefaults`](https://github.com/radex/SwiftyUserDefaults)?
|
### How is this different from [`SwiftyUserDefaults`](https://github.com/radex/SwiftyUserDefaults)?
|
||||||
|
|
||||||
It's inspired by it and other solutions. The main difference is that this module doesn't hardcode the default values and comes with Codable support.
|
It's inspired by that package and other solutions. The main difference is that this module doesn't hardcode the default values and comes with Codable support.
|
||||||
|
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
Loading…
Reference in New Issue