diff --git a/Biome.podspec b/Biome.podspec index ba8c568..f5772ab 100644 --- a/Biome.podspec +++ b/Biome.podspec @@ -1,11 +1,11 @@ Pod::Spec.new do |s| s.name = 'Biome' - s.version = '1.0.0' + s.version = '2.0.0' s.summary = 'Simple environment management for iOS / macOS / tvOS / watchOS!' s.description = <<-DESC Biome is a simple way to manage sets of variables between environments you might need to work with when developing your application. -One problem developers face, is the need to create multiple builds in order to test how their application functions in different environments. Biome aims to reduce the amount of re-building that needs to occur, by providing a mechanism to switch configurations during run-time. +One problem developers face, is the need to create multiple builds in order to test how their application functions in different environments. Biome aims to reduce the amount of re-building that needs to occur by providing a mechanism to switch values and configurations during run-time. DESC s.homepage = 'https://github.com/ndizazzo/Biome' diff --git a/Biome.xcodeproj/project.pbxproj b/Biome.xcodeproj/project.pbxproj index 6bf6b1b..5724a69 100644 --- a/Biome.xcodeproj/project.pbxproj +++ b/Biome.xcodeproj/project.pbxproj @@ -7,69 +7,83 @@ objects = { /* Begin PBXBuildFile section */ - C5EE7B1E1E6DE98200EC5A49 /* Biome.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C5EE7B141E6DE98200EC5A49 /* Biome.framework */; }; + C5340FCC20C7963C0045BA5B /* Biome.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B2F1E6DEA4100EC5A49 /* Biome.swift */; }; + C534207620C8DF180081B6E5 /* HashableType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C534207520C8DF180081B6E5 /* HashableType.swift */; }; + C534EB0F20D30FDE006414ED /* BiomeGroupTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C534EB0E20D30FDE006414ED /* BiomeGroupTests.swift */; }; + C534EB1120D42093006414ED /* HashableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C534EB1020D42093006414ED /* HashableTests.swift */; }; + C570F7F720D2C43800890450 /* SelectableArrayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C59A9BE720D1C6FB00C150E9 /* SelectableArrayTests.swift */; }; + C570F7F820D2C44D00890450 /* testProperties.plist in Resources */ = {isa = PBXBuildFile; fileRef = C5EE7B391E6DEAA100EC5A49 /* testProperties.plist */; }; + C570F7F920D2C44D00890450 /* testProperties.json in Resources */ = {isa = PBXBuildFile; fileRef = C59A9BE920D2B50800C150E9 /* testProperties.json */; }; + C570F7FB20D2CDC700890450 /* BiomeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C570F7FA20D2CDC700890450 /* BiomeTests.swift */; }; + C58861F920DAC6870074D71E /* BiomeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B301E6DEA4100EC5A49 /* BiomeDataSource.swift */; }; + C59A9BE220D19B4900C150E9 /* BiomeGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C59A9BE120D19B4900C150E9 /* BiomeGroup.swift */; }; + C59A9BE420D19B7500C150E9 /* BiomeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C59A9BE320D19B7500C150E9 /* BiomeManager.swift */; }; + C59A9BE620D19C1F00C150E9 /* SelectableArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = C59A9BE520D19C1F00C150E9 /* SelectableArray.swift */; }; + C5E5F8E720D44832005F8FB7 /* BiomeManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B361E6DEAA100EC5A49 /* BiomeManagerTests.swift */; }; C5EE7B251E6DE98200EC5A49 /* Biome.h in Headers */ = {isa = PBXBuildFile; fileRef = C5EE7B171E6DE98200EC5A49 /* Biome.h */; settings = {ATTRIBUTES = (Public, ); }; }; - C5EE7B321E6DEA4100EC5A49 /* Biome.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B2F1E6DEA4100EC5A49 /* Biome.swift */; }; - C5EE7B331E6DEA4100EC5A49 /* BiomeDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B301E6DEA4100EC5A49 /* BiomeDataSource.swift */; }; - C5EE7B341E6DEA4100EC5A49 /* PlistBiomeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B311E6DEA4100EC5A49 /* PlistBiomeProvider.swift */; }; - C5EE7B3A1E6DEAA100EC5A49 /* BiomeManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B361E6DEAA100EC5A49 /* BiomeManagerTests.swift */; }; - C5EE7B3B1E6DEAA100EC5A49 /* BiomeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B371E6DEAA100EC5A49 /* BiomeTests.swift */; }; - C5EE7B3C1E6DEAA100EC5A49 /* PlistBiomeProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE7B381E6DEAA100EC5A49 /* PlistBiomeProviderTests.swift */; }; - C5EE7B3D1E6DEAA100EC5A49 /* testProperties.plist in Resources */ = {isa = PBXBuildFile; fileRef = C5EE7B391E6DEAA100EC5A49 /* testProperties.plist */; }; - C5EE7B611E6DFD3500EC5A49 /* Carthage.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = C5EE7B601E6DFD3500EC5A49 /* Carthage.xcconfig */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - C5EE7B1F1E6DE98200EC5A49 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = C5EE7B0B1E6DE98200EC5A49 /* Project object */; - proxyType = 1; - remoteGlobalIDString = C5EE7B131E6DE98200EC5A49; - remoteInfo = Biome; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXFileReference section */ + C534207520C8DF180081B6E5 /* HashableType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashableType.swift; sourceTree = ""; }; + C534EB0E20D30FDE006414ED /* BiomeGroupTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiomeGroupTests.swift; sourceTree = ""; }; + C534EB1020D42093006414ED /* HashableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashableTests.swift; sourceTree = ""; }; + C570F7FA20D2CDC700890450 /* BiomeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiomeTests.swift; sourceTree = ""; }; + C59A9BE120D19B4900C150E9 /* BiomeGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BiomeGroup.swift; path = Classes/BiomeGroup.swift; sourceTree = ""; }; + C59A9BE320D19B7500C150E9 /* BiomeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = BiomeManager.swift; path = Classes/BiomeManager.swift; sourceTree = ""; }; + C59A9BE520D19C1F00C150E9 /* SelectableArray.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SelectableArray.swift; path = Classes/SelectableArray.swift; sourceTree = ""; }; + C59A9BE720D1C6FB00C150E9 /* SelectableArrayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableArrayTests.swift; sourceTree = ""; }; + C59A9BE920D2B50800C150E9 /* testProperties.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = testProperties.json; sourceTree = ""; }; + C59A9BEF20D2B9E600C150E9 /* BiomeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BiomeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + C59A9BF320D2B9E600C150E9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C5EE7B141E6DE98200EC5A49 /* Biome.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Biome.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C5EE7B171E6DE98200EC5A49 /* Biome.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Biome.h; sourceTree = ""; }; C5EE7B181E6DE98200EC5A49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C5EE7B1D1E6DE98200EC5A49 /* BiomeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BiomeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - C5EE7B241E6DE98200EC5A49 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C5EE7B2F1E6DEA4100EC5A49 /* Biome.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Biome.swift; path = Classes/Biome.swift; sourceTree = ""; }; C5EE7B301E6DEA4100EC5A49 /* BiomeDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = BiomeDataSource.swift; path = Classes/BiomeDataSource.swift; sourceTree = ""; }; - C5EE7B311E6DEA4100EC5A49 /* PlistBiomeProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PlistBiomeProvider.swift; path = Classes/PlistBiomeProvider.swift; sourceTree = ""; }; C5EE7B361E6DEAA100EC5A49 /* BiomeManagerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiomeManagerTests.swift; sourceTree = ""; }; - C5EE7B371E6DEAA100EC5A49 /* BiomeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BiomeTests.swift; sourceTree = ""; }; - C5EE7B381E6DEAA100EC5A49 /* PlistBiomeProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlistBiomeProviderTests.swift; sourceTree = ""; }; C5EE7B391E6DEAA100EC5A49 /* testProperties.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = testProperties.plist; sourceTree = ""; }; C5EE7B601E6DFD3500EC5A49 /* Carthage.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Carthage.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ - C5EE7B101E6DE98200EC5A49 /* Frameworks */ = { + C59A9BEC20D2B9E600C150E9 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; - C5EE7B1A1E6DE98200EC5A49 /* Frameworks */ = { + C5EE7B101E6DE98200EC5A49 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C5EE7B1E1E6DE98200EC5A49 /* Biome.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + C59A9BF020D2B9E600C150E9 /* BiomeTests */ = { + isa = PBXGroup; + children = ( + C570F7FA20D2CDC700890450 /* BiomeTests.swift */, + C534EB0E20D30FDE006414ED /* BiomeGroupTests.swift */, + C5EE7B361E6DEAA100EC5A49 /* BiomeManagerTests.swift */, + C59A9BE720D1C6FB00C150E9 /* SelectableArrayTests.swift */, + C5EE7B391E6DEAA100EC5A49 /* testProperties.plist */, + C59A9BE920D2B50800C150E9 /* testProperties.json */, + C59A9BF320D2B9E600C150E9 /* Info.plist */, + C534EB1020D42093006414ED /* HashableTests.swift */, + ); + path = BiomeTests; + sourceTree = ""; + }; C5EE7B0A1E6DE98200EC5A49 = { isa = PBXGroup; children = ( C5EE7B601E6DFD3500EC5A49 /* Carthage.xcconfig */, C5EE7B161E6DE98200EC5A49 /* Biome */, - C5EE7B211E6DE98200EC5A49 /* BiomeTests */, + C59A9BF020D2B9E600C150E9 /* BiomeTests */, C5EE7B151E6DE98200EC5A49 /* Products */, ); sourceTree = ""; @@ -78,7 +92,7 @@ isa = PBXGroup; children = ( C5EE7B141E6DE98200EC5A49 /* Biome.framework */, - C5EE7B1D1E6DE98200EC5A49 /* BiomeTests.xctest */, + C59A9BEF20D2B9E600C150E9 /* BiomeTests.xctest */, ); name = Products; sourceTree = ""; @@ -93,32 +107,15 @@ path = Biome; sourceTree = ""; }; - C5EE7B211E6DE98200EC5A49 /* BiomeTests */ = { - isa = PBXGroup; - children = ( - C5EE7B351E6DEA4B00EC5A49 /* Biome */, - C5EE7B241E6DE98200EC5A49 /* Info.plist */, - ); - path = BiomeTests; - sourceTree = ""; - }; C5EE7B2E1E6DE9DA00EC5A49 /* Biome */ = { isa = PBXGroup; children = ( C5EE7B2F1E6DEA4100EC5A49 /* Biome.swift */, + C59A9BE120D19B4900C150E9 /* BiomeGroup.swift */, + C59A9BE320D19B7500C150E9 /* BiomeManager.swift */, C5EE7B301E6DEA4100EC5A49 /* BiomeDataSource.swift */, - C5EE7B311E6DEA4100EC5A49 /* PlistBiomeProvider.swift */, - ); - name = Biome; - sourceTree = ""; - }; - C5EE7B351E6DEA4B00EC5A49 /* Biome */ = { - isa = PBXGroup; - children = ( - C5EE7B361E6DEAA100EC5A49 /* BiomeManagerTests.swift */, - C5EE7B371E6DEAA100EC5A49 /* BiomeTests.swift */, - C5EE7B381E6DEAA100EC5A49 /* PlistBiomeProviderTests.swift */, - C5EE7B391E6DEAA100EC5A49 /* testProperties.plist */, + C534207520C8DF180081B6E5 /* HashableType.swift */, + C59A9BE520D19C1F00C150E9 /* SelectableArray.swift */, ); name = Biome; sourceTree = ""; @@ -137,6 +134,23 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + C59A9BEE20D2B9E600C150E9 /* BiomeTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = C59A9BF420D2B9E600C150E9 /* Build configuration list for PBXNativeTarget "BiomeTests" */; + buildPhases = ( + C59A9BEB20D2B9E600C150E9 /* Sources */, + C59A9BEC20D2B9E600C150E9 /* Frameworks */, + C59A9BED20D2B9E600C150E9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = BiomeTests; + productName = BiomeTest; + productReference = C59A9BEF20D2B9E600C150E9 /* BiomeTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; C5EE7B131E6DE98200EC5A49 /* Biome */ = { isa = PBXNativeTarget; buildConfigurationList = C5EE7B281E6DE98200EC5A49 /* Build configuration list for PBXNativeTarget "Biome" */; @@ -155,43 +169,27 @@ productReference = C5EE7B141E6DE98200EC5A49 /* Biome.framework */; productType = "com.apple.product-type.framework"; }; - C5EE7B1C1E6DE98200EC5A49 /* BiomeTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = C5EE7B2B1E6DE98200EC5A49 /* Build configuration list for PBXNativeTarget "BiomeTests" */; - buildPhases = ( - C5EE7B191E6DE98200EC5A49 /* Sources */, - C5EE7B1A1E6DE98200EC5A49 /* Frameworks */, - C5EE7B1B1E6DE98200EC5A49 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - C5EE7B201E6DE98200EC5A49 /* PBXTargetDependency */, - ); - name = BiomeTests; - productName = BiomeTests; - productReference = C5EE7B1D1E6DE98200EC5A49 /* BiomeTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ C5EE7B0B1E6DE98200EC5A49 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 0820; + DefaultBuildSystemTypeForWorkspace = Latest; + LastSwiftUpdateCheck = 1000; + LastUpgradeCheck = 1000; ORGANIZATIONNAME = "Nick DiZazzo"; TargetAttributes = { + C59A9BEE20D2B9E600C150E9 = { + CreatedOnToolsVersion = 10.0; + DevelopmentTeam = KAQ9WEF9C7; + ProvisioningStyle = Automatic; + }; C5EE7B131E6DE98200EC5A49 = { CreatedOnToolsVersion = 8.2.1; - LastSwiftMigration = 0820; + LastSwiftMigration = 1000; ProvisioningStyle = Manual; }; - C5EE7B1C1E6DE98200EC5A49 = { - CreatedOnToolsVersion = 8.2.1; - ProvisioningStyle = Automatic; - }; }; }; buildConfigurationList = C5EE7B0E1E6DE98200EC5A49 /* Build configuration list for PBXProject "Biome" */; @@ -207,62 +205,114 @@ projectRoot = ""; targets = ( C5EE7B131E6DE98200EC5A49 /* Biome */, - C5EE7B1C1E6DE98200EC5A49 /* BiomeTests */, + C59A9BEE20D2B9E600C150E9 /* BiomeTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - C5EE7B121E6DE98200EC5A49 /* Resources */ = { + C59A9BED20D2B9E600C150E9 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C5EE7B611E6DFD3500EC5A49 /* Carthage.xcconfig in Resources */, + C570F7F920D2C44D00890450 /* testProperties.json in Resources */, + C570F7F820D2C44D00890450 /* testProperties.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - C5EE7B1B1E6DE98200EC5A49 /* Resources */ = { + C5EE7B121E6DE98200EC5A49 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C5EE7B3D1E6DEAA100EC5A49 /* testProperties.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ - C5EE7B0F1E6DE98200EC5A49 /* Sources */ = { + C59A9BEB20D2B9E600C150E9 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C5EE7B341E6DEA4100EC5A49 /* PlistBiomeProvider.swift in Sources */, - C5EE7B321E6DEA4100EC5A49 /* Biome.swift in Sources */, - C5EE7B331E6DEA4100EC5A49 /* BiomeDataSource.swift in Sources */, + C570F7FB20D2CDC700890450 /* BiomeTests.swift in Sources */, + C534EB1120D42093006414ED /* HashableTests.swift in Sources */, + C5E5F8E720D44832005F8FB7 /* BiomeManagerTests.swift in Sources */, + C570F7F720D2C43800890450 /* SelectableArrayTests.swift in Sources */, + C534EB0F20D30FDE006414ED /* BiomeGroupTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - C5EE7B191E6DE98200EC5A49 /* Sources */ = { + C5EE7B0F1E6DE98200EC5A49 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C5EE7B3B1E6DEAA100EC5A49 /* BiomeTests.swift in Sources */, - C5EE7B3A1E6DEAA100EC5A49 /* BiomeManagerTests.swift in Sources */, - C5EE7B3C1E6DEAA100EC5A49 /* PlistBiomeProviderTests.swift in Sources */, + C59A9BE220D19B4900C150E9 /* BiomeGroup.swift in Sources */, + C59A9BE620D19C1F00C150E9 /* SelectableArray.swift in Sources */, + C59A9BE420D19B7500C150E9 /* BiomeManager.swift in Sources */, + C5340FCC20C7963C0045BA5B /* Biome.swift in Sources */, + C534207620C8DF180081B6E5 /* HashableType.swift in Sources */, + C58861F920DAC6870074D71E /* BiomeDataSource.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - C5EE7B201E6DE98200EC5A49 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = C5EE7B131E6DE98200EC5A49 /* Biome */; - targetProxy = C5EE7B1F1E6DE98200EC5A49 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin XCBuildConfiguration section */ + C59A9BF520D2B9E600C150E9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = KAQ9WEF9C7; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = BiomeTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks\n@executable_path/Frameworks\n@loader_path/../Frameworks\n@executable_path/../Frameworks @loader_path/../Frameworks @executable_path/../Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + PRODUCT_BUNDLE_IDENTIFIER = org.ndizazzo.BiomeTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Debug; + }; + C59A9BF620D2B9E600C150E9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = KAQ9WEF9C7; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = BiomeTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks\n@executable_path/Frameworks\n@loader_path/../Frameworks\n@executable_path/../Frameworks @loader_path/../Frameworks @executable_path/../Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = org.ndizazzo.BiomeTest; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = macosx; + TARGETED_DEVICE_FAMILY = 3; + }; + name = Release; + }; C5EE7B261E6DE98200EC5A49 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -272,15 +322,23 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -312,6 +370,7 @@ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator macosx"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 9.0; VERSIONING_SYSTEM = "apple-generic"; @@ -329,15 +388,23 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -361,6 +428,7 @@ SDKROOT = macosx; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator appletvos appletvsimulator macosx"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; TVOS_DEPLOYMENT_TARGET = 9.0; VALIDATE_PRODUCT = YES; @@ -388,7 +456,6 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -409,62 +476,35 @@ PRODUCT_BUNDLE_IDENTIFIER = com.ndizazzo.Biome; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_VERSION = 3.0; - }; - name = Release; - }; - C5EE7B2C1E6DE98200EC5A49 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C5EE7B601E6DFD3500EC5A49 /* Carthage.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - INFOPLIST_FILE = BiomeTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.ndizazzo.BiomeTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; - }; - name = Debug; - }; - C5EE7B2D1E6DE98200EC5A49 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = C5EE7B601E6DFD3500EC5A49 /* Carthage.xcconfig */; - buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - INFOPLIST_FILE = BiomeTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - PRODUCT_BUNDLE_IDENTIFIER = com.ndizazzo.BiomeTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - C5EE7B0E1E6DE98200EC5A49 /* Build configuration list for PBXProject "Biome" */ = { + C59A9BF420D2B9E600C150E9 /* Build configuration list for PBXNativeTarget "BiomeTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - C5EE7B261E6DE98200EC5A49 /* Debug */, - C5EE7B271E6DE98200EC5A49 /* Release */, + C59A9BF520D2B9E600C150E9 /* Debug */, + C59A9BF620D2B9E600C150E9 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - C5EE7B281E6DE98200EC5A49 /* Build configuration list for PBXNativeTarget "Biome" */ = { + C5EE7B0E1E6DE98200EC5A49 /* Build configuration list for PBXProject "Biome" */ = { isa = XCConfigurationList; buildConfigurations = ( - C5EE7B291E6DE98200EC5A49 /* Debug */, - C5EE7B2A1E6DE98200EC5A49 /* Release */, + C5EE7B261E6DE98200EC5A49 /* Debug */, + C5EE7B271E6DE98200EC5A49 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - C5EE7B2B1E6DE98200EC5A49 /* Build configuration list for PBXNativeTarget "BiomeTests" */ = { + C5EE7B281E6DE98200EC5A49 /* Build configuration list for PBXNativeTarget "Biome" */ = { isa = XCConfigurationList; buildConfigurations = ( - C5EE7B2C1E6DE98200EC5A49 /* Debug */, - C5EE7B2D1E6DE98200EC5A49 /* Release */, + C5EE7B291E6DE98200EC5A49 /* Debug */, + C5EE7B2A1E6DE98200EC5A49 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; diff --git a/Biome.xcodeproj/xcshareddata/xcschemes/Biome.xcscheme b/Biome.xcodeproj/xcshareddata/xcschemes/Biome.xcscheme index 3aa7ed2..d697de5 100644 --- a/Biome.xcodeproj/xcshareddata/xcschemes/Biome.xcscheme +++ b/Biome.xcodeproj/xcshareddata/xcschemes/Biome.xcscheme @@ -1,6 +1,6 @@ + + + + + codeCoverageEnabled = "YES" + onlyGenerateCoverageForSpecifiedTargets = "YES" + shouldUseLaunchSchemeArgsEnv = "NO"> + + + + + + + + @@ -49,6 +78,13 @@ ReferencedContainer = "container:Biome.xcodeproj"> + + + + diff --git a/Biome/Classes/Biome.swift b/Biome/Classes/Biome.swift index d74266f..9c52914 100644 --- a/Biome/Classes/Biome.swift +++ b/Biome/Classes/Biome.swift @@ -2,115 +2,43 @@ // Biome.swift // Biome // -// Created by Nick DiZazzo on 2017-02-28. -// Copyright © 2017 Nick DiZazzo. All rights reserved. +// Created by Nick DiZazzo on 2018-05-11. +// Copyright © 2018 Nick DiZazzo. All rights reserved. // import Foundation // MARK: - -// MARK: BiomeProvider - -public protocol BiomeProvider { - var biomeName: String { get } - func mappedPropertyDictionary() -> [String: Any] -} - -// MARK: - -// MARK: BiomeManager +// MARK: Biome -public enum BiomeError: Error { - case previouslyRegistered - case notRegistered +/// A simple protocol that tells the `BiomeGroup` and `BiomeManager` how to work with objects. +public protocol Biome: Codable { + var identifier: String { get } + var keyCount: Int { get } } -public protocol BiomeManagerDelegate { - func switched(to biome: Biome?) +enum BiomeError: LocalizedError { + case missingResource } -public class BiomeManager { - public static let shared = BiomeManager() - - internal var biomes: Set = Set() - - public var count: Int { get { return self.biomes.count } } - public var current: Biome? { didSet { self.delegate?.switched(to: self.current) } } - public var delegate: BiomeManagerDelegate? - - public var keys: Set { - get { - var unversalKeys = Set() - - for biome in self.biomes { - biome.properties.keys.forEach { unversalKeys.insert($0) } - } - - return unversalKeys - } - } - - public static func register(_ biome: Biome) throws { - if shared.biomes.contains(biome) { - throw BiomeError.previouslyRegistered - } +extension Biome { + public static func load(fromPath url: URL?, using decoder: BridgedDecoder) throws -> Self { + guard let url = url else { throw BiomeError.missingResource } - if shared.count == 0 { - shared.current = biome - } - - shared.biomes.insert(biome) - } - - public static func remove(_ biome: Biome) throws { - guard shared.biomes.contains(biome) else { - throw BiomeError.notRegistered - } - - shared.biomes.remove(biome) - } - - public func clear() { - self.current = nil - self.biomes.removeAll() + let data = try Data(contentsOf: url) + let biome = try decoder.decode(Self.self, from: data) + return biome } } -// MARK: - -// MARK: Biome -public class Biome { - fileprivate var properties: [String: Any] = [:] - - public private(set) var name: String = "" - public var count: Int { get { return properties.keys.count } } - - public init(named biomeName: String) { - self.name = biomeName - } - - public init(with provider: BiomeProvider) { - self.name = provider.biomeName - self.properties = provider.mappedPropertyDictionary() - } - - public func get(_ key: String) -> Any? { - return self.properties[key] - } - - public func set(_ key: String, value: Any?) { - self.properties[key] = value - } -} +public typealias BiomeData = (identifier: String, keys: Int) // MARK: - -// MARK: Equatable -extension Biome: Equatable { - public static func ==(lhs: Biome, rhs: Biome) -> Bool { - return lhs.name == rhs.name - } -} +// MARK: BridgedDecoder -// MARK: - -// MARK: Hashable -extension Biome: Hashable { - public var hashValue: Int { get { return self.name.hash } } +public protocol BridgedDecoder { + func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable } + +extension JSONDecoder: BridgedDecoder { } +extension PropertyListDecoder: BridgedDecoder { } diff --git a/Biome/Classes/BiomeDataSource.swift b/Biome/Classes/BiomeDataSource.swift index 9e6cec4..7e714c7 100644 --- a/Biome/Classes/BiomeDataSource.swift +++ b/Biome/Classes/BiomeDataSource.swift @@ -11,71 +11,73 @@ import Foundation #if os(iOS) || os(watchOS) || os(tvOS) // MARK: - // MARK: BiomeDataSource -public class BiomeDataSource: NSObject { +public class BiomeDataSource: NSObject, UITableViewDataSource, UITableViewDelegate { internal weak var manager: BiomeManager? - internal var biomes: [Biome] + internal var biomeData: [BiomeData] = [] public init(biomeManager: BiomeManager) { self.manager = biomeManager - self.biomes = Array(biomeManager.biomes) + if let group = biomeManager.group(for: HashableType()) { + biomeData = group.biomeData + } } - fileprivate func biome(at index: Int) -> Biome? { - guard index >= 0 && index < self.biomes.count else { + fileprivate func data(at index: Int) -> BiomeData? { + guard index >= 0 && index < self.biomeData.count else { return nil } - return self.biomes[index] + return self.biomeData[index] } - fileprivate func viewModel(for biome: Biome) -> BiomeViewModel { - let isActive = self.manager?.current == biome - let string = biome.count == 1 ? "%d key" : "%d keys" - return BiomeViewModel(name: biome.name, - subtitle: String.init(format: string, biome.count), + fileprivate func viewModel(for data: BiomeData) -> BiomeViewModel { + let currentBiome = self.manager?.current(for: T.self) + let isActive = currentBiome?.identifier == data.identifier + + let string = 1 == data.keys ? "%d key" : "%d keys" + return BiomeViewModel(name: data.identifier, + subtitle: String.init(format: string, data.keys), active: isActive) } -} -// MARK: - -// MARK: UITableViewDataSource -extension BiomeDataSource: UITableViewDataSource { - + // MARK: - + // MARK: UITableViewDataSource public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.biomes.count + return self.biomeData.count } - + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = self.cell(tableView: tableView) - if let biome = self.biome(at: indexPath.row) { + if let biome = self.data(at: indexPath.row) { let viewModel = self.viewModel(for: biome) cell.update(with: viewModel) } - + return cell } - + private func cell(tableView: UITableView) -> UITableViewCell { var cell = tableView.dequeueReusableCell(withIdentifier: "cell") - + if cell == nil { cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell") } - + return cell! } -} -// MARK: - -// MARK: UITableViewDelegate -extension BiomeDataSource: UITableViewDelegate { + // MARK: - + // MARK: UITableViewDelegate public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let biome = self.biome(at: indexPath.row) - self.manager?.current = biome - + do { + try manager?.select(type: T.self, index: indexPath.row) + } catch let error { + print("ERROR: \(error)") + } + var indexSet = IndexSet() indexSet.insert(0) - + tableView.reloadSections(indexSet, with: .automatic) } } diff --git a/Biome/Classes/BiomeGroup.swift b/Biome/Classes/BiomeGroup.swift new file mode 100644 index 0000000..adc5873 --- /dev/null +++ b/Biome/Classes/BiomeGroup.swift @@ -0,0 +1,107 @@ +// +// BiomeGroup.swift +// Biome +// +// Created by Nick DiZazzo on 2018-06-13. +// Copyright © 2018 Nick DiZazzo. All rights reserved. +// + +import Foundation + +internal protocol AnyBiomeGroupProtocol { + var biomeType: Biome.Type { get } + var biomeCount: Int { get } + func erasedCurrent() -> Biome? + func switchTo(_ index: Int?) +} + +extension AnyBiomeGroupProtocol where Self: BiomeGroupProtocol { + var biomeType: Biome.Type { + return BiomeType.self + } +} + +internal protocol BiomeGroupProtocol: AnyBiomeGroupProtocol { + associatedtype BiomeType: Biome + func current() -> BiomeType? +} + +internal struct AnyBiomeGroup { + var _storage: AnyBiomeGroupProtocol + + init(group: Group) { + _storage = group + } + + func current() -> Biome? { + return _storage.erasedCurrent() + } + + func current() -> BiomeType? { + if BiomeType.self == _storage.biomeType { + return _storage.erasedCurrent() as! BiomeType? + } + + return nil + } +} + +/// A class that collects similarly-typed Biome objects together, and maintains a current selection for whichever one should be active. +internal final class BiomeGroup: BiomeGroupProtocol { + enum Error: LocalizedError { + case duplicateBiome + } + + private var biomes = SelectableArray() + var biomeCount: Int { return biomes.count } + var biomeData: [BiomeData] { + return biomes.map { + return (identifier: $0.identifier, keys: $0.keyCount) + } + } + + func insert(_ biome: BiomeType) throws { + let alreadyExists = biomes.index(where: { innerBiome -> Bool in + return innerBiome.identifier == biome.identifier + }) != nil + + guard !alreadyExists else { throw Error.duplicateBiome } + + biomes.insert(biome, at: biomes.endIndex) + } + + func insert(contentsOf collection: [BiomeType]) throws { + let identifierSet = collection.map { $0.identifier } + let existingBiomes = biomes.filter { identifierSet.contains($0.identifier) } + + guard existingBiomes.count == 0 else { throw Error.duplicateBiome } + + biomes.insert(contentsOf: collection, at: biomes.endIndex) + } + + func removeBiome(with identifier: String) { + guard let removalIndex = biomes.index(where: { biome -> Bool in + return biome.identifier == identifier + }) else { return } + + + _ = biomes.remove(at: removalIndex) + } + + func current() -> BiomeType? { + return biomes.current + } + + func erasedCurrent() -> Biome? { + return biomes.current + } + + func switchTo(_ index: Int?) { + guard let biomeIndex = index else { + biomes.deselect() + return + } + + biomes.select(index: biomeIndex) + } +} diff --git a/Biome/Classes/BiomeManager.swift b/Biome/Classes/BiomeManager.swift new file mode 100644 index 0000000..fc8af17 --- /dev/null +++ b/Biome/Classes/BiomeManager.swift @@ -0,0 +1,157 @@ +// +// BiomeManager.swift +// Biome +// +// Created by Nick DiZazzo on 2018-06-13. +// Copyright © 2018 Nick DiZazzo. All rights reserved. +// + +import Foundation + +public protocol BiomeManagerDelegate: AnyObject { + func switched(to biome: Biome?) +} + +// MARK: - +// MARK: BiomeManager + +/// A manager object that maintains a groups of active 'Biomes' created in your application. +public final class BiomeManager { + enum Error: LocalizedError { + /// Thrown when attempting to select a Biome at an index which is greater than the number of registered Biomes. + case invalidBiomeIndex + + /// Thrown when no containing group matching the type of Biome is found. + case groupNotRegistered + } + + /// A hashable list of groups tracked by the shared manager + private var groups: Dictionary = [:] + + /// A count of the number of different types this BiomeManager is managing. + public var groupCount: Int { return groups.count } + + /// The cumulative number of biomes managed. + public var biomeCount: Int { + return groups.reduce(0) { sum, group in + return sum + group.value._storage.biomeCount + } + } + + /// Any object conforming to this protocol will be notified when Biome switching occurs. + public weak var delegate: BiomeManagerDelegate? + + public init() { } + + // Shorthand for developers to get the current Biome matching the provided type. + public subscript(_ key: BiomeType.Type) -> BiomeType? { + let hashableKey: HashableType = createHashable() + return group(for: hashableKey)?.current() + } + + /// Fetches the BiomeGroup for the provided key. + /// + /// - Parameter key: A hashable type that biome groups are stored under, for later retrieval. + /// - Returns: A BiomeGroup object corresponding to the type of Biome desired to be retrieved. + internal func group(for key: HashableType) -> BiomeGroup? { + if let group = groups[key]?._storage as? BiomeGroup { + return group + } + + return nil + } + + /// Sets up the BiomeManager to track the provided type of Biomes, and adds each one to the group for the provided type. + /// + /// If the 'selected' Biome for the provided type is nil, the first biome provided is automatically + /// selected as the current one for that group. + /// + /// - Parameter xs: Any number of similarly-typed Biome objects. + public func register(_ xs: BiomeType...) throws { + let hashableBiome: HashableType = createHashable() + + if let group = groups[hashableBiome]?._storage as? BiomeGroup { + try group.insert(contentsOf: xs) + } else { + let group = BiomeGroup() + try group.insert(contentsOf: xs) + + let anyGroup = AnyBiomeGroup(group: group) + groups[hashableBiome] = anyGroup + } + + if current(for: BiomeType.self) == nil { + try select(type: BiomeType.self, index: 0) + } + } + + /// Removes an entire group of Biomes from being managed. + /// + /// - Parameter type: The type of Biomes to stop tracking. + public func unregister(type: BiomeType.Type) { + let hashableType: HashableType = createHashable() + + if let currentlySelectedType = self[type], createHashable(currentlySelectedType) == hashableType { + delegate?.switched(to: nil) + } + + groups[hashableType] = nil + } + + /// Removes a Biome using a specific identifier, from the type-specific group of Biomes. + /// + /// - Parameters: + /// - type: The type of Biome to remove. + /// - identifier: The unique identifier of the Biome to remove. + public func remove(with type: BiomeType.Type, identifier: String) { + let type = HashableType() + let group = self.group(for: type) + group?.removeBiome(with: identifier) + } + + + /// Convenience method to remove a provided Biome. + /// + /// The type and identifier used are inferred from the provided object. + /// + /// - Parameter biome: The biome to remove + public func remove(with biome: BiomeType) { + remove(with: BiomeType.self, identifier: biome.identifier) + } + + /// Removes all Biomes and BiomeGroup objects from the manager, notifying the delegate of the removals. + public func clear() { + groups.removeAll() + delegate?.switched(to: nil) + } + + /// Convenience method to fetch the currently selected Biome given the provided type. + /// + /// - Parameter type: The type of Biome to query for. + /// - Returns: The currently selected Biome, if any. + public func current(for type: BiomeType.Type) -> BiomeType? { + return self[type] + } + + /// Sets the Biome at the provided index as the currently selected item. + /// + /// - Parameters: + /// - type: The type of Biome to select. + /// - index: The index of the Biome in the group to select. + public func select(type: BiomeType.Type, index: Int) throws { + let hashableBiome: HashableType = createHashable() + + guard let group = group(for: hashableBiome) else { + throw Error.groupNotRegistered + } + + guard index < group.biomeCount else { + throw Error.invalidBiomeIndex + } + + group.switchTo(index) + let newCurrentBiome = group.current() + + delegate?.switched(to: newCurrentBiome) + } +} diff --git a/Biome/Classes/PlistBiomeProvider.swift b/Biome/Classes/PlistBiomeProvider.swift deleted file mode 100644 index 2859aa5..0000000 --- a/Biome/Classes/PlistBiomeProvider.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// PlistBiomeProvider.swift -// Biome -// -// Created by Nick DiZazzo on 2017-02-28. -// Copyright © 2017 Nick DiZazzo. All rights reserved. -// - -import Foundation - -public class PlistBiomeProvider: BiomeProvider { - - var plistFileName: String - var bundle: Bundle - - public var biomeName: String { get { return self.plistFileName } } - - public init(bundle: Bundle, filename: String) { - self.plistFileName = filename - self.bundle = bundle - } - - public func mappedPropertyDictionary() -> [String : Any] { - if let path = self.bundle.path(forResource: self.plistFileName, ofType: "plist"), - let dict = NSDictionary(contentsOfFile: path) as? [String: AnyObject] { - return dict - } - - return [:] - } -} diff --git a/Biome/Classes/SelectableArray.swift b/Biome/Classes/SelectableArray.swift new file mode 100644 index 0000000..24ee3b0 --- /dev/null +++ b/Biome/Classes/SelectableArray.swift @@ -0,0 +1,101 @@ +// +// SelectableCollection.swift +// Biome +// +// Created by Nick DiZazzo on 2018-06-13. +// Copyright © 2018 Nick DiZazzo. All rights reserved. +// + +import Foundation + +public protocol SelectableCollection { + associatedtype Index + associatedtype Element + + var currentIndex: Index? { get set } + var current: Element? { get } + mutating func select(index: Index) + mutating func deselect() +} + +public struct SelectableArray { + public typealias StorageType = [Element] + public typealias Index = StorageType.Index + public typealias Iterator = StorageType.Iterator + + fileprivate var _storage: StorageType + public var currentIndex: Index? + + public init() { + _storage = [] + } + + public init(_ array: [Element]) { + _storage = array + } +} + +extension SelectableArray: Collection { + public var startIndex: SelectableArray.StorageType.Index { + return _storage.startIndex + } + + public var endIndex: SelectableArray.StorageType.Index { + return _storage.endIndex + } + + public func makeIterator() -> IndexingIterator> { + return _storage.makeIterator() + } +} + +extension SelectableArray: MutableCollection { + public subscript(position: SelectableArray.StorageType.Index) -> Element { + get { return _storage[position] } + set(newValue) { _storage[position] = newValue } + } +} + +extension SelectableArray: SelectableCollection { + public var current: Element? { + guard let index = currentIndex else { return nil } + guard index < _storage.endIndex else { return nil } + return self[index] + } + + public mutating func insert(_ newElement: Element, at i: Index) { + // if i is lte the selected index, modifiy selected index to be +1 + if currentIndex != nil && i <= currentIndex! { currentIndex! += 1 } + + _storage.insert(newElement, at: i) + } + + public mutating func insert(contentsOf newElements: C, at i: Index) where C : Collection, Element == C.Element { + // if i is lte the selected index, modifiy selected index to be +n + if currentIndex != nil && i <= currentIndex! { currentIndex! += newElements.count } + + _storage.insert(contentsOf: newElements, at: i) + } + + public mutating func remove(at position: Index) -> Element { + // we removed the selected index + if currentIndex != nil && position == currentIndex! { currentIndex = nil } + + // if the selected index is gte position, modify selected index to be -1 + if currentIndex != nil && currentIndex! > position { currentIndex! -= 1 } + + return _storage.remove(at: position) + } + + public mutating func select(index: Index) { + currentIndex = index + } + + public mutating func deselect() { + currentIndex = nil + } +} + +extension SelectableArray: RandomAccessCollection { } + +extension SelectableArray: RangeReplaceableCollection { } diff --git a/Biome/HashableType.swift b/Biome/HashableType.swift new file mode 100644 index 0000000..78d83b6 --- /dev/null +++ b/Biome/HashableType.swift @@ -0,0 +1,37 @@ +// +// HashableType.swift +// Biome +// +// Created by Nick DiZazzo on 2018-06-06. +// Copyright © 2018 Nick DiZazzo. All rights reserved. +// + +import Foundation + +// MARK: - +// MARK: HashableType + +public struct HashableType: Hashable { + public var hashValue: Int { + return "\(type)".hashValue + } + + let type = Type.self + + init() {} + + public static func ==(lhs: HashableType, rhs: HashableType) -> Bool { + return lhs.type == rhs.type + } + + /// Swift 4.2 implementation of deprecated 'hashValue' + /// + /// - Parameter hasher: The hasher to use to combine values + //public func hash(into hasher: inout Hasher) { + // hasher.combine("\(type)") + //} +} + +internal func createHashable(_ x: Type? = nil) -> HashableType { + return HashableType() +} diff --git a/BiomeDemo/BiomeDemo.xcodeproj/project.pbxproj b/BiomeDemo/BiomeDemo.xcodeproj/project.pbxproj index b301d52..6b45de9 100644 --- a/BiomeDemo/BiomeDemo.xcodeproj/project.pbxproj +++ b/BiomeDemo/BiomeDemo.xcodeproj/project.pbxproj @@ -7,10 +7,10 @@ objects = { /* Begin PBXBuildFile section */ + C5340FCE20C7A2860045BA5B /* Development.json in Resources */ = {isa = PBXBuildFile; fileRef = C5340FCD20C7A2860045BA5B /* Development.json */; }; C57075AF1E6F15DF00F4C96E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C57075A31E6F15DF00F4C96E /* AppDelegate.swift */; }; C57075B01E6F15DF00F4C96E /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = C57075A51E6F15DF00F4C96E /* LaunchScreen.xib */; }; C57075B11E6F15DF00F4C96E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C57075A71E6F15DF00F4C96E /* Main.storyboard */; }; - C57075B21E6F15DF00F4C96E /* Development.plist in Resources */ = {isa = PBXBuildFile; fileRef = C57075A91E6F15DF00F4C96E /* Development.plist */; }; C57075B31E6F15DF00F4C96E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C57075AA1E6F15DF00F4C96E /* Images.xcassets */; }; C57075B51E6F15DF00F4C96E /* Production.plist in Resources */ = {isa = PBXBuildFile; fileRef = C57075AC1E6F15DF00F4C96E /* Production.plist */; }; C57075B61E6F15DF00F4C96E /* Staging.plist in Resources */ = {isa = PBXBuildFile; fileRef = C57075AD1E6F15DF00F4C96E /* Staging.plist */; }; @@ -58,11 +58,11 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + C5340FCD20C7A2860045BA5B /* Development.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Development.json; sourceTree = ""; }; C570758E1E6F15A500F4C96E /* BiomeDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BiomeDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; C57075A31E6F15DF00F4C96E /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C57075A61E6F15DF00F4C96E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = LaunchScreen.xib; sourceTree = ""; }; C57075A81E6F15DF00F4C96E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Main.storyboard; sourceTree = ""; }; - C57075A91E6F15DF00F4C96E /* Development.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Development.plist; sourceTree = ""; }; C57075AA1E6F15DF00F4C96E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; C57075AB1E6F15DF00F4C96E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C57075AC1E6F15DF00F4C96E /* Production.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Production.plist; sourceTree = ""; }; @@ -115,11 +115,11 @@ children = ( C57075A71E6F15DF00F4C96E /* Main.storyboard */, C57075A51E6F15DF00F4C96E /* LaunchScreen.xib */, - C57075A91E6F15DF00F4C96E /* Development.plist */, C57075AA1E6F15DF00F4C96E /* Images.xcassets */, C57075AB1E6F15DF00F4C96E /* Info.plist */, C57075AC1E6F15DF00F4C96E /* Production.plist */, C57075AD1E6F15DF00F4C96E /* Staging.plist */, + C5340FCD20C7A2860045BA5B /* Development.json */, ); name = "Supporting Files"; sourceTree = ""; @@ -175,6 +175,7 @@ TargetAttributes = { C570758D1E6F15A500F4C96E = { CreatedOnToolsVersion = 8.2.1; + DevelopmentTeam = KAQ9WEF9C7; LastSwiftMigration = 0820; ProvisioningStyle = Automatic; }; @@ -227,11 +228,11 @@ buildActionMask = 2147483647; files = ( C57075B01E6F15DF00F4C96E /* LaunchScreen.xib in Resources */, + C5340FCE20C7A2860045BA5B /* Development.json in Resources */, C57075B11E6F15DF00F4C96E /* Main.storyboard in Resources */, C57075B31E6F15DF00F4C96E /* Images.xcassets in Resources */, C57075B51E6F15DF00F4C96E /* Production.plist in Resources */, C57075B61E6F15DF00F4C96E /* Staging.plist in Resources */, - C57075B21E6F15DF00F4C96E /* Development.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -325,6 +326,7 @@ SDKROOT = iphoneos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -367,6 +369,7 @@ MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -378,12 +381,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = KAQ9WEF9C7; INFOPLIST_FILE = BiomeDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ndizazzo.BiomeDemo; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; }; name = Debug; }; @@ -393,11 +395,11 @@ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + DEVELOPMENT_TEAM = KAQ9WEF9C7; INFOPLIST_FILE = BiomeDemo/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.ndizazzo.BiomeDemo; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; }; name = Release; }; diff --git a/BiomeDemo/BiomeDemo/Development.plist b/BiomeDemo/BiomeDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 74% rename from BiomeDemo/BiomeDemo/Development.plist rename to BiomeDemo/BiomeDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist index 9cf7d28..18d9810 100644 --- a/BiomeDemo/BiomeDemo/Development.plist +++ b/BiomeDemo/BiomeDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -2,7 +2,7 @@ - api_url - http://development.api.com + IDEDidComputeMac32BitWarning + diff --git a/BiomeDemo/BiomeDemo/AppDelegate.swift b/BiomeDemo/BiomeDemo/AppDelegate.swift index b860a0f..f0f559a 100644 --- a/BiomeDemo/BiomeDemo/AppDelegate.swift +++ b/BiomeDemo/BiomeDemo/AppDelegate.swift @@ -9,29 +9,33 @@ import UIKit import Biome +let manager = BiomeManager() + @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { - self.createExampleBiomes() - return true } func createExampleBiomes() { - let devBiome = Biome(with: PlistBiomeProvider(bundle: Bundle.main, filename: "Development")) - let stagingBiome = Biome(with: PlistBiomeProvider(bundle: Bundle.main, filename: "Staging")) - let prodBiome = Biome(with: PlistBiomeProvider(bundle: Bundle.main, filename: "Production")) - + let plistDecoder = PropertyListDecoder() + let jsonDecoder = JSONDecoder() + do { - try BiomeManager.register(devBiome) - try BiomeManager.register(stagingBiome) - try BiomeManager.register(prodBiome) - } catch _ { - assert(false, "Couldn't create a Biome. Is a Plist file missing?") + let devPath = Bundle.main.url(forResource: "Development", withExtension: "json")! + let stagePath = Bundle.main.url(forResource: "Staging", withExtension: "plist")! + let prodPath = Bundle.main.url(forResource: "Production", withExtension: "plist")! + + let devAPI = try APIBiome.load(fromPath: devPath, using: jsonDecoder) + let stagingAPI = try APIBiome.load(fromPath: stagePath, using: plistDecoder) + let prodAPI = try APIBiome.load(fromPath: prodPath, using: plistDecoder) + + try manager.register(devAPI, stagingAPI, prodAPI) + } catch let error { + assert(false, "Couldn't create a Biome: \(error)") } } } - diff --git a/BiomeDemo/BiomeDemo/Development.json b/BiomeDemo/BiomeDemo/Development.json new file mode 100644 index 0000000..7ac7b0a --- /dev/null +++ b/BiomeDemo/BiomeDemo/Development.json @@ -0,0 +1,4 @@ +{ + "identifier": "development-api-biome", + "baseURL": "https://development.api.com" +} diff --git a/BiomeDemo/BiomeDemo/Production.plist b/BiomeDemo/BiomeDemo/Production.plist index 309b5ec..3ed367a 100644 --- a/BiomeDemo/BiomeDemo/Production.plist +++ b/BiomeDemo/BiomeDemo/Production.plist @@ -2,7 +2,9 @@ - api_url + baseURL http://production.api.com + identifier + production-api-biome diff --git a/BiomeDemo/BiomeDemo/Staging.plist b/BiomeDemo/BiomeDemo/Staging.plist index b7b7186..3749e3a 100644 --- a/BiomeDemo/BiomeDemo/Staging.plist +++ b/BiomeDemo/BiomeDemo/Staging.plist @@ -2,7 +2,9 @@ - api_url + baseURL http://staging.api.com + identifier + staging-api-biome diff --git a/BiomeDemo/BiomeDemo/ViewController.swift b/BiomeDemo/BiomeDemo/ViewController.swift index 46ee65a..0e05a6d 100644 --- a/BiomeDemo/BiomeDemo/ViewController.swift +++ b/BiomeDemo/BiomeDemo/ViewController.swift @@ -9,27 +9,32 @@ import UIKit import Biome +struct APIBiome: Biome { + var identifier: String + var keyCount: Int { return 1 } + + var baseURL: String +} + class ViewController: UIViewController { @IBOutlet weak var showBiomesButton: UIButton? - @IBOutlet weak var apiURLLabel: UILabel? - var dataSource: BiomeDataSource? + var dataSource: BiomeDataSource? override func viewDidLoad() { super.viewDidLoad() self.showBiomesButton?.layer.cornerRadius = 3.0 - let currentBiome = BiomeManager.shared.current - + guard let currentBiome = manager.current(for: APIBiome.self) else { return } self.setupBiomeInfo(biome: currentBiome) } @IBAction func showBiomes(_ sender: Any) { - self.dataSource = BiomeDataSource(biomeManager: BiomeManager.shared) + self.dataSource = BiomeDataSource(biomeManager: manager) - BiomeManager.shared.delegate = self + manager.delegate = self let tableViewController = UITableViewController(style: .plain) tableViewController.tableView.delegate = self.dataSource @@ -39,14 +44,14 @@ class ViewController: UIViewController { self.navigationController?.pushViewController(tableViewController, animated: true) } - func setupBiomeInfo(biome: Biome?) { - self.apiURLLabel?.text = biome?.get("api_url") as? String + func setupBiomeInfo(biome: APIBiome) { + self.apiURLLabel?.text = biome.baseURL } } extension ViewController: BiomeManagerDelegate { func switched(to biome: Biome?) { - self.setupBiomeInfo(biome: biome) + guard let currentBiome = manager.current(for: APIBiome.self) else { return } + self.setupBiomeInfo(biome: currentBiome) } } - diff --git a/BiomeTests/BiomeGroupTests.swift b/BiomeTests/BiomeGroupTests.swift new file mode 100644 index 0000000..b8be329 --- /dev/null +++ b/BiomeTests/BiomeGroupTests.swift @@ -0,0 +1,114 @@ +import XCTest +@testable import Biome + +class BiomeGroupTests: XCTestCase { + let jsonBiomeDecoder = JSONDecoder() + let plistBiomeDecoder = PropertyListDecoder() + let testBiome = TestBiome(identifier: "TestBiome", testItem1: "", testItem2: 0, testItem3: 0.0, testItem4: false) + + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testCreatingGroup() { + let group = BiomeGroup() + XCTAssertEqual(group.biomeCount, 0) + } + + func testInsertingBiome() { + let group = BiomeGroup() + + try! group.insert(testBiome) + XCTAssertEqual(group.biomeCount, 1) + } + + func testInsertingBiomes() { + let group = BiomeGroup() + + try! group.insert(contentsOf: [testBiome]) + XCTAssertEqual(group.biomeCount, 1) + } + + func testInsertingDuplicateBiomeThrows() { + let group = BiomeGroup() + + try! group.insert(testBiome) + XCTAssertEqual(group.biomeCount, 1) + + XCTAssertThrowsError(try group.insert(testBiome)) + } + + func testInsertingMultipleDuplicateBiomesThrows() { + let group = BiomeGroup() + + try! group.insert(testBiome) + XCTAssertEqual(group.biomeCount, 1) + + XCTAssertThrowsError(try group.insert(contentsOf: [testBiome])) + } + + func testRemovingBiome() { + let group = BiomeGroup() + try! group.insert(testBiome) + XCTAssertEqual(group.biomeCount, 1) + + group.removeBiome(with: "TestBiome") + XCTAssertEqual(group.biomeCount, 0) + } + + func testRemovingNonExistentBiome() { + let group = BiomeGroup() + group.removeBiome(with: "I don't exist") + } + + func testSelectingBiome() { + let group = BiomeGroup() + try! group.insert(testBiome) + group.switchTo(0) + + let current = group.current() + XCTAssertNotNil(current) + XCTAssertEqual(current!.identifier, testBiome.identifier) + } + + func testClearingSelection() { + let group = BiomeGroup() + try! group.insert(testBiome) + group.switchTo(0) + + let current = group.current() + XCTAssertNotNil(current) + + group.switchTo(nil) + XCTAssertNil(group.current()) + } + + func testErasedCurrent() { + let group = BiomeGroup() + try! group.insert(testBiome) + group.switchTo(0) + + XCTAssertNotNil(group.erasedCurrent()) + } + + func testAnyBiomeGroupWrapper() { + let group = BiomeGroup() + let anyGroup = AnyBiomeGroup(group: group) + try! group.insert(testBiome) + group.switchTo(0) + + XCTAssertNotNil(anyGroup.current()) + + let castedAnyBiomeGroup: TestBiome? = anyGroup.current() + XCTAssertNotNil(castedAnyBiomeGroup) + + group.switchTo(nil) + let improperlyCastAnyBiomeGroup: SecondTestBiome? = anyGroup.current() + XCTAssertNil(improperlyCastAnyBiomeGroup) + } +} + diff --git a/BiomeTests/BiomeManagerTests.swift b/BiomeTests/BiomeManagerTests.swift index 8683e5c..02be42f 100644 --- a/BiomeTests/BiomeManagerTests.swift +++ b/BiomeTests/BiomeManagerTests.swift @@ -1,94 +1,161 @@ import XCTest -import Biome +@testable import Biome + +final class TestBiomeDelegate: BiomeManagerDelegate { + var called: Bool = false + + func switched(to biome: Biome?) { + called = true + } +} class BiomeManagerTests: XCTestCase { - - var testBiome: Biome! - var biomeManager: BiomeManager! = BiomeManager.shared + var testBiome: TestBiome! + let jsonBiomeDecoder = JSONDecoder() + var manager: BiomeManager! + var biomeDelegate: TestBiomeDelegate! override func setUp() { super.setUp() - - biomeManager.clear() - - self.testBiome = Biome(named: "test") - self.testBiome.set("testKey", value: "testValue") + + biomeDelegate = TestBiomeDelegate() + manager = BiomeManager() + manager.delegate = biomeDelegate + + let biomeURL = Bundle(for: BiomeManagerTests.self).url(forResource: "testProperties", withExtension: "json") + testBiome = try! TestBiome.load(fromPath: biomeURL, using: jsonBiomeDecoder) } override func tearDown() { + manager.delegate = nil + manager = nil + super.tearDown() } func testRegisterBiome() { - XCTAssertEqual(biomeManager.count, 0) - - try! BiomeManager.register(testBiome) - XCTAssertEqual(biomeManager.count, 1) + try! manager.register(testBiome) + XCTAssertEqual(manager.groupCount, 1) + XCTAssertEqual(manager.biomeCount, 1) } - - func testRegisteringDuplicateBiome() { - XCTAssertEqual(biomeManager.count, 0) - - try! BiomeManager.register(testBiome) - XCTAssertEqual(biomeManager.count, 1) - - XCTAssertThrowsError( - try BiomeManager.register(testBiome), - "'register' didnt throw expected error", - { error in XCTAssertEqual(error as! BiomeError, .previouslyRegistered) } - ) + + func testRegisterBiomeSelectsFirstAdded() { + try! manager.register(testBiome) + XCTAssertNotNil(manager.current(for: TestBiome.self)) + } + + func testRegisterMultipleTypeBiomes() { + let testBiome2 = SecondTestBiome() + try! manager.register(testBiome!) + try! manager.register(testBiome2) + + XCTAssertEqual(manager.groupCount, 2) + XCTAssertEqual(manager.biomeCount, 2) } func testRemoveBiome() { - try! BiomeManager.register(testBiome) - XCTAssertEqual(biomeManager.count, 1) - - try! BiomeManager.remove(testBiome) - XCTAssertEqual(biomeManager.count, 0) + let testBiome2 = SecondTestBiome() + try! manager.register(testBiome!) + try! manager.register(testBiome2) + + XCTAssertEqual(manager.groupCount, 2) + XCTAssertEqual(manager.biomeCount, 2) + + manager.remove(with: testBiome) + manager.remove(with: SecondTestBiome.self, identifier: testBiome2.identifier) + + XCTAssertEqual(manager.groupCount, 2) + XCTAssertEqual(manager.biomeCount, 0) } - - func testRemovingNonexistantBiome() { - XCTAssertThrowsError( - try BiomeManager.remove(testBiome), - "'remove' didnt throw expected error", - { error in XCTAssertEqual(error as! BiomeError, .notRegistered) } - ) + + func testUnregisterGroup() { + let testBiome2 = SecondTestBiome() + try! manager.register(testBiome!) + try! manager.register(testBiome2) + + XCTAssertEqual(manager.groupCount, 2) + XCTAssertEqual(manager.biomeCount, 2) + + manager.unregister(type: SecondTestBiome.self) + manager.unregister(type: TestBiome.self) + + XCTAssertEqual(manager.groupCount, 0) + XCTAssertEqual(manager.biomeCount, 0) } - - func testClearingBiomes() { - try! BiomeManager.register(testBiome) - try! BiomeManager.register(Biome(named: "another one")) - XCTAssertEqual(biomeManager.count, 2) - - biomeManager.clear() - - XCTAssertEqual(biomeManager.count, 0) - XCTAssertEqual(biomeManager.current, nil) + + func testUnregisterGroupCallsDelegateIfSelected() { + try! manager.register(testBiome!) + let selectedBiome = manager.current(for: TestBiome.self) + XCTAssertNotNil(selectedBiome) + XCTAssertTrue(biomeDelegate.called) + + let newDelegate = TestBiomeDelegate() + manager.delegate = newDelegate + + manager.unregister(type: TestBiome.self) + let newSelectedBiome = manager.current(for: TestBiome.self) + XCTAssertNil(newSelectedBiome) + XCTAssertTrue(newDelegate.called) } - - func testGatheringKeysAcrossBiomes() { - let firstBiome = Biome(named: "first") - let secondBiome = Biome(named: "second") - - firstBiome.set("firstKey", value: 1) - secondBiome.set("secondKey", value: 2) - - try! BiomeManager.register(firstBiome) - try! BiomeManager.register(secondBiome) - - XCTAssertEqual(BiomeManager.shared.count, 2) - XCTAssertEqual(BiomeManager.shared.keys.count, 2) - XCTAssertEqual(Array(BiomeManager.shared.keys), ["firstKey", "secondKey"]) + + func testCurrentSubscript() { + try! manager.register(testBiome!) + + let current = manager[TestBiome.self] + XCTAssertNotNil(current) + XCTAssertEqual(current?.identifier, testBiome.identifier) } - - func testCurrentBiomeIsSetWhenRegistering() { - try! BiomeManager.register(testBiome) - XCTAssertEqual(biomeManager.current, testBiome) + + func testDelegateIsCalledWhenSwitchingBiomes() { + let testBiome1 = TestBiome(identifier: "first", testItem1: "", testItem2: 0, testItem3: 0.0, testItem4: false) + let testBiome2 = TestBiome(identifier: "second", testItem1: "", testItem2: 0, testItem3: 0.0, testItem4: false) + try! manager.register(testBiome1) + try! manager.register(testBiome2) + + let newDelegate = TestBiomeDelegate() + manager.delegate = newDelegate + + XCTAssertFalse(newDelegate.called) + + try! manager.select(type: TestBiome.self, index: 1) + + XCTAssertTrue(newDelegate.called) + } + + func testDelegateIsCalledWhenClearingBiome() { + let testBiome2 = SecondTestBiome() + try! manager.register(testBiome!) + try! manager.register(testBiome2) + + manager.clear() + + XCTAssertTrue(biomeDelegate.called) + } + + func testSwitchingBiomesChangesFromPreviousToNext() { + let testBiome2 = TestBiome(identifier: "new_identifier", testItem1: "test", testItem2: 1, testItem3: 4.0, testItem4: false) + try! manager.register(testBiome!) + try! manager.register(testBiome2) + + let selectedBiome: TestBiome? = manager.current(for: TestBiome.self) + XCTAssertNotNil(selectedBiome) + XCTAssertEqual(selectedBiome!.identifier, testBiome!.identifier) + + try! manager.select(type: TestBiome.self, index: 1) + + let newSelected = manager[TestBiome.self] + XCTAssertNotNil(newSelected) + XCTAssertEqual(newSelected!.identifier, testBiome2.identifier) } - func testCurrentBiomeRemainsSetWhenRegistering() { - try! BiomeManager.register(testBiome) - try! BiomeManager.register(Biome(named: "second biome")) - XCTAssertEqual(biomeManager.current, testBiome) + func testClearingBiomes() { + try! manager.register(testBiome) + XCTAssertEqual(manager.biomeCount, 1) + XCTAssertEqual(manager.groupCount, 1) + + manager.clear() + + XCTAssertEqual(manager.biomeCount, 0) + XCTAssertEqual(manager.groupCount, 0) } } diff --git a/BiomeTests/BiomeTests.swift b/BiomeTests/BiomeTests.swift index aaf5259..17a3e00 100644 --- a/BiomeTests/BiomeTests.swift +++ b/BiomeTests/BiomeTests.swift @@ -1,59 +1,53 @@ import XCTest import Biome -final class TestBiomeProvider: BiomeProvider { - var biomeName: String = "testBiome1" - - func mappedPropertyDictionary() -> [String : Any] { - return ["testProviderValue": 1] - } +struct TestBiome: Biome { + var identifier: String + var keyCount: Int { return 4 } + + var testItem1: String + var testItem2: Int + var testItem3: Double + var testItem4: Bool +} + +struct SecondTestBiome: Biome { + var identifier: String { return "SecondTestBiome" } + var keyCount: Int { return 0 } } class BiomeTests: XCTestCase { - var testBiome: Biome! - + let jsonBiomeDecoder = JSONDecoder() + let plistBiomeDecoder = PropertyListDecoder() + override func setUp() { super.setUp() - - self.testBiome = Biome(named: "test") - self.testBiome.set("testKey", value: "testValue") } - + override func tearDown() { super.tearDown() } - - func testBiomeName() { - XCTAssert(testBiome.name == "test") - } - - func testBiomeCount() { - XCTAssertEqual(self.testBiome.count, 1) - } - - func testGettingBiomeValue() { - try! BiomeManager.register(testBiome) - let testValue = testBiome.get("testKey") as! String - XCTAssertEqual(testValue, "testValue") - } - - func testBiomeEquality() { - let first = Biome(named: "first") - let second = Biome(named: "second") - let duplicate = Biome(named: "first") - - XCTAssertEqual(first, duplicate) - XCTAssertEqual(first, first) - XCTAssertNotEqual(first, second) + + func testCanLoadFromJSON() { + let biomeURL = Bundle(for: BiomeTests.self).url(forResource: "testProperties", withExtension: "json") + let biome = try! TestBiome.load(fromPath: biomeURL, using: jsonBiomeDecoder) + + XCTAssertNotNil(biome) + XCTAssertEqual(biome.testItem1, "item1") + XCTAssertEqual(biome.testItem2, 0) + XCTAssertEqual(biome.testItem3, 1.0) + XCTAssertFalse(biome.testItem4) } - - func testBiomeWithProvider() { - let biome = Biome(with: TestBiomeProvider()) - XCTAssertEqual(biome.name, "testBiome1") - XCTAssertEqual(biome.count, 1) - - let value = biome.get("testProviderValue") as! Int - XCTAssertEqual(value, 1) + + func testCanLoadFromPlist() { + let biomeURL = Bundle(for: BiomeTests.self).url(forResource: "testProperties", withExtension: "plist") + let biome = try! TestBiome.load(fromPath: biomeURL, using: plistBiomeDecoder) + + XCTAssertNotNil(biome) + XCTAssertEqual(biome.identifier, "TestPlistBiome") + XCTAssertEqual(biome.testItem1, "item1") + XCTAssertEqual(biome.testItem2, 0) + XCTAssertEqual(biome.testItem3, 1.0) + XCTAssertFalse(biome.testItem4) } - } diff --git a/BiomeTests/HashableTests.swift b/BiomeTests/HashableTests.swift new file mode 100644 index 0000000..931d4fc --- /dev/null +++ b/BiomeTests/HashableTests.swift @@ -0,0 +1,36 @@ +import XCTest +@testable import Biome + +class HashableTests: XCTestCase { + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testTypeCanBeConvertedToHashable() { + let myInt = 5 + let b = createHashable(myInt) + let aaa = type(of: b) == type(of: HashableType()) + XCTAssertTrue(aaa) + } + + // Re-enable for Swift 4.2 / Xcode 10 + func testHashableTypeHasher() { + let myInt = 5 + let hashable = createHashable(myInt) + let deterministicHash = hashable.hashValue + + // SWIFT_DETERMINISTIC_HASHING is ENABLED in the BiomeTests scheme for this to be consistent across runs. + XCTAssertEqual(deterministicHash, -5648306562266618760) + } + + func testEquality() { + let myInt = 5 + let hashable1 = createHashable(myInt) + let hashable2 = createHashable(myInt) + XCTAssertEqual(hashable1, hashable2) + } +} diff --git a/BiomeTests/Info.plist b/BiomeTests/Info.plist index ba72822..6c40a6c 100644 --- a/BiomeTests/Info.plist +++ b/BiomeTests/Info.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - en + $(DEVELOPMENT_LANGUAGE) CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -16,8 +16,6 @@ BNDL CFBundleShortVersionString 1.0 - CFBundleSignature - ???? CFBundleVersion 1 diff --git a/BiomeTests/PlistBiomeProviderTests.swift b/BiomeTests/PlistBiomeProviderTests.swift deleted file mode 100644 index 9e8be54..0000000 --- a/BiomeTests/PlistBiomeProviderTests.swift +++ /dev/null @@ -1,46 +0,0 @@ -import XCTest -import Biome - -class PlistBiomeTests: XCTestCase { - - var provider: PlistBiomeProvider! - - override func setUp() { - super.setUp() - - BiomeManager.shared.clear() - self.provider = PlistBiomeProvider(bundle: Bundle(for: BiomeTests.self), filename: "testProperties") - } - - override func tearDown() { - super.tearDown() - } - - func testProviderProperlyProvidesName() { - XCTAssertEqual(self.provider.biomeName, "testProperties") - } - - func testProviderMapsPlistProperties() { - let dict = self.provider.mappedPropertyDictionary() - - XCTAssertEqual(dict.count, 4) - - if let first = dict["test_item_1"] as? String, - let second = dict["test_item_2"] as? Int, - let third = dict["test_item_3"] as? Date, - let fourth = dict["test_item_4"] as? Bool { - XCTAssertEqual(first, "item1") - XCTAssertEqual(second, 0) - XCTAssertEqual(third, Date(timeIntervalSince1970: 1488337200)) - XCTAssertEqual(fourth, false) - } else { - XCTFail("One or more properties were not converted properly") - } - } - - func testProviderReturnsEmptyDictionary() { - let provider = PlistBiomeProvider(bundle: Bundle.main, filename: "doesnt_exist") - let dict = provider.mappedPropertyDictionary() - XCTAssert(dict.isEmpty) - } -} diff --git a/BiomeTests/SelectableArrayTests.swift b/BiomeTests/SelectableArrayTests.swift new file mode 100644 index 0000000..3c20df3 --- /dev/null +++ b/BiomeTests/SelectableArrayTests.swift @@ -0,0 +1,164 @@ +import XCTest +import Biome + +class SelectableArrayTests: XCTestCase { + override func setUp() { + super.setUp() + } + + override func tearDown() { + super.tearDown() + } + + func testCanConstructWithNothing() { + let testArray = SelectableArray() + XCTAssert(testArray.count == 0) + } + + func testCanConstructWithArray() { + let testArray = SelectableArray([1, 2, 3, 4]) + XCTAssert(testArray.count == 4) + } + + func testSubscript() { + var testArray = SelectableArray(["one", "two"]) + XCTAssertEqual(testArray[1], "two") + + testArray[1] = "two-two" + XCTAssertEqual(testArray[1], "two-two") + } + + func testIndexAfter() { + let testArray = SelectableArray([1, 2, 3, 4]) + XCTAssertEqual(testArray.index(after: 1), 2) + } + + func testStartIndex() { + let testArray = SelectableArray([2, 3, 4]) + XCTAssertEqual(testArray.startIndex, 0) + } + + func testEndIndex() { + let testArray = SelectableArray([4, 3, 2]) + XCTAssertEqual(testArray.endIndex, 3) + } + + func testMakeIterator() { + let testArray = SelectableArray(["aaa", "bbb"]) + let iterator = testArray.makeIterator() + XCTAssert(iterator.elementsEqual(["aaa", "bbb"])) + } + + func testSelectingElement() { + var testArray = SelectableArray([0.0, 1.0, 2.0]) + testArray.select(index: 1) + + let selectedElement = testArray.current + XCTAssertNotNil(selectedElement) + + XCTAssertEqual(selectedElement!, 1.0) + } + + func testNilCurrentWhenSelectingNothing() { + var testArray = SelectableArray([]) + testArray.select(index: 0) + + XCTAssertNil(testArray.current) + } + + func testDeselectingElement() { + var testArray = SelectableArray([0.0, 1.0, 2.0]) + testArray.select(index: 1) + + let selectedElement = testArray.current + XCTAssertNotNil(selectedElement) + + testArray.deselect() + XCTAssertNil(testArray.current) + } + + func testClearingWhenElementSelected() { + var testArray = SelectableArray([0.0, 1.0, 2.0]) + testArray.select(index: 1) + + let selectedElement = testArray.current + XCTAssertNotNil(selectedElement) + + testArray.removeAll() + + let shouldBeNil = testArray.current + XCTAssertNil(shouldBeNil) + } + + func testRemovingSelectedElement() { + var testArray = SelectableArray(["first", "second"]) + testArray.select(index: 1) + + let selectedElement = testArray.current + XCTAssertNotNil(selectedElement) + + _ = testArray.remove(at: 1) + + let shouldBeNil = testArray.current + XCTAssertNil(shouldBeNil) + } + + func testRemovingElementAfterSelected() { + var testArray = SelectableArray(["first", "second"]) + testArray.select(index: 0) + + let selectedElement = testArray.current + XCTAssertNotNil(selectedElement) + + _ = testArray.remove(at: 1) + + let shouldBeEqual = testArray.current + XCTAssertEqual(selectedElement, shouldBeEqual) + } + + func testRemovingElementBeforeSelected() { + var testArray = SelectableArray(["first", "second"]) + testArray.select(index: 1) + + let selectedElement = testArray.current + XCTAssertNotNil(selectedElement) + + _ = testArray.remove(at: 0) + + let shouldBeEqual = testArray.current + XCTAssertEqual(selectedElement, shouldBeEqual) + } + + func testInsertingBeforeSelected() { + var testArray = SelectableArray([9, 10]) + testArray.select(index: 1) + let selectedElement = testArray.current + + testArray.insert(8, at: 0) + let newSelectedElement = testArray.current + + XCTAssertEqual(selectedElement, newSelectedElement) + } + + func testInsertingAfterSelected() { + var testArray = SelectableArray([9, 10]) + testArray.select(index: 1) + let selectedElement = testArray.current + + testArray.insert(11, at: 2) + let newSelectedElement = testArray.current + + XCTAssertEqual(selectedElement, newSelectedElement) + } + + func testBulkInsertingBeforeSelected() { + var testArray = SelectableArray([9, 10]) + testArray.select(index: 0) + let selectedElement = testArray.current + + testArray.insert(contentsOf: [7, 8], at: 0) + let newSelectedElement = testArray.current + + XCTAssertEqual(selectedElement, newSelectedElement) + } +} diff --git a/BiomeTests/testProperties.json b/BiomeTests/testProperties.json new file mode 100644 index 0000000..edd5ad3 --- /dev/null +++ b/BiomeTests/testProperties.json @@ -0,0 +1,7 @@ +{ + "identifier": "TestJSONBiome", + "testItem1": "item1", + "testItem2": 0, + "testItem3": 1.0, + "testItem4": false +} diff --git a/BiomeTests/testProperties.plist b/BiomeTests/testProperties.plist index fb41657..47a0481 100644 --- a/BiomeTests/testProperties.plist +++ b/BiomeTests/testProperties.plist @@ -2,13 +2,15 @@ - test_item_1 + identifier + TestPlistBiome + testItem1 item1 - test_item_2 + testItem2 0 - test_item_3 - 2017-03-01T03:00:00Z - test_item_4 + testItem3 + 1 + testItem4 diff --git a/README.md b/README.md index 19334a9..45316f8 100644 --- a/README.md +++ b/README.md @@ -32,29 +32,31 @@ Applied for our purposes: ## Concept +A manager organizes similarly-typed Biomes into groups, where each group can have at most 1 "active" Biome. You can instruct the `BiomeManager` to switch to a different `Biome` within the same `BiomeGroup`, and the manager notifies the provided delegate. + ### Biome -The smallest conceptual unit - this object holds simple key / value pairs that define an environment. +The smallest conceptual unit in the framework - this object defines a set of properties whose values will be swapped out at any time with a different set of values. -### BiomeProvider +### BiomeGroup -A protocol that allows mappings from various types of data sets into Biome objects. Conformance to this protocol implies that Biomes will be able to be created from a given provider. +A container that manages similarly-typed Biome objects. A Biome group provides funtionality to store Biomes, as well as maintain one "currently active" Biome in that group. You do not need to create or manage groups yourself - the `BiomeManager` takes care of that automatically. ### BiomeManager -A centralized place to manage many Biomes. +A common interface to register and track Biome objects. ## Support -* Xcode 8.0 / Swift 3.0 +* Xcode 8.0 / Swift 4.0 * iOS >= 8.0 (Use as an **Embedded** Framework) * tvOS >= 9.0 -* macOS >= 10.10 (untested, but should still work in theory) +* macOS >= 10.10 ## Having trouble running the demo? * `BiomeDemo/BiomeDemo.xcodeproj` is the demo project for iOS * Make sure you are running a supported version of Xcode. -* Make sure that your project supports Swift 3.0 +* Make sure that your project supports Swift 4.0 # Installation @@ -85,8 +87,8 @@ and run `pod install` Biome includes Carthage prebuilt binaries. ```carthage -github "ndizazzo/Biome" == 1.0.0 -github "ndizazzo/Biome" ~> 1.0.0 +github "ndizazzo/Biome" == 2.0.0 +github "ndizazzo/Biome" ~> 2.0.0 ``` In order to build the binaries for a new release, use `carthage build --no-skip-current && carthage archive Biome`. @@ -96,56 +98,30 @@ In order to build the binaries for a new release, use `carthage build --no-skip- ## Implementing Biome in your Project To implement Biome, first `import Biome` into the relevant files where you want to use it. -Then, create a Biome by calling the `Biome()` constructor, or using the `PlistBiomeProvider(bundle: Bundle, filename: String)`. You can also write a `BiomeProvider` that will return a Biome object out of the provided data. Consider forking and 🔨 contributing back 🔨 to this project. Don't forget to update this README! +Then, create a Biome by defining a `struct` that conforms to the `Biome` protocol. Add as many fields as you like to your object, and implement the `keyCount` property to return the number of properties on the object. -Once you have a `Biome` object, use `BiomeManager.register()` to register it. This does a couple things: -1. Adds it to the set of available Biomes. One uniquely named Biome is allowed per manager. -1. Sets it as the `current` object on `BiomeManager`. This is a property tells you which tells you what Biome is currently active. +Once you have `Biome` object, use `BiomeManager.register(...)` to register them. This does a couple things: +1. Adds it to a managed group of similarly-typed Biomes. One uniquely identified Biome is allowed per group. +1. Sets it as the `current` Biome of the `BiomeGroup` managing it. This `current` property tells you which Biome is currently active. -Finally, extend one of your classes: +Finally, extend one of your classes to receive events when the Biome is switched: ```swift extension MyClass: BiomeManagerDelegate { - func switched(to: Biome) { + func switched(to biome: Biome?) { print("The active biome has been switched to '\(to.name)'") } } ``` -Use that delegate method to do anything, like clear a CoreData database and reload data into it from a different environment, re-query an API for new data, refresh a view controller's appearance, etc. - -In order to take full advantage of Biome, you'll want to follow the pattern: -```swift - - if let biome = BiomeManager.shared.current, let apiEndpoint = biome.get("api_url") as? String { - myAPIClient.get(apiEndpoint) { response in - // ... - } - } -``` -to gate your application's properties behind Biome so that when you switch environments, the most up-to-date values are always used. - -*NOTE* You are _not_ required to use the shared `BiomeManager`. You can create numerious instances of the class and store them where you wish. - -## Class Descriptions -Implementation of Biome is straightforward. Biome provides several classes and protocols for you to use or customize: -* `class BiomeManager` - A singleton manager object that maintains a list of active 'Biomes' created in your application. - -* `class Biome` - A simple wrapper object that dictates how to access values for keys. You are responsible for knowing the type of data going in, and coming out of a Biome. - -* `protocol BiomeProvider` - A protocol that defines how 'providers' should map keys and values to a Biome. Examples might include: plist files, JSON, XML, user defaults. - -* `class PlistBiomeProvider` - A biome provider that knows how to obtain values from a Plist file. +Use that delegate method to do anything: clear a database and reload data into it from a different environment, re-query an API for new data, refresh a view controller's appearance, etc. # Feature Roadmap -#### Biome Groups -* You may want to have multiple Biomes active at a given time, or mix and match combinations of variables. Groups would allow the developer to do something like: - * Have a set of variables for API endpoints - * Have a set of style colours to use for UI elements - * Have a set of logging levels +#### Display Biome Key names / values +* [SR-7897](https://bugs.swift.org/browse/SR-7897) suggests that synthesized `Codable` conformance should also optionally generate `CaseIterable` conformance. This would allow every `Biome` to have the `keyCount` automatically generated, and `allItems` reflect the actual properties of the Biome. - All without having to make `n!` distinct Biomes to switch between to achieve all possible permutations of variable configurations. +If this improvement moves to the proposal phase, Biome.framework could add support for it, allowing developers to do more powerful things with the library; where they might be listed out for display in some type of UICollectionView / UITableView. #### Run-time Property Modification * Sometimes a developer might need to modify a property at run-time instead of providing it before compile-time. This feature would allow developers to do things like tweak animation timings, for instance. @@ -180,9 +156,9 @@ If you have ideas or like what you see here and want to support the project, you If you are having questions or problems, you should: - - Make sure you are using the latest version of the library. Check the [**release-section**](https://github.com/danielgindi/Biome/releases). + - Make sure you are using the latest version of the library. Check the [**release-section**](https://github.com/ndizazzo/Biome/releases). - Search or open questions on [**stackoverflow**](http://stackoverflow.com/questions/tagged/ios-biome) with the `ios-biome` tag - - Search [**known issues**](https://github.com/danielgindi/Biome/issues) for your problem (open and closed) + - Search [**known issues**](https://github.com/ndizazzo/Biome/issues) for your problem (open and closed) - Create new issues (please **search known issues beforehand** and avoid creating duplicate issues) ## Documentation