├── .gitignore ├── LICENSE ├── MotifKit.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── MotifKit.xcscheme ├── MotifKit ├── Info.plist ├── Motif.swift ├── MotifKit.h ├── PrivateDataTypes.swift ├── PublicDataTypes.swift ├── TypeFunctions.swift └── Utils.swift ├── MotifKitTests ├── Info.plist ├── MotifKitTests.swift └── MotifTestUtils.swift ├── icon.png └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | GIT_BAK 2 | # Created by https://www.gitignore.io/api/xcode,swift 3 | 4 | ### Xcode ### 5 | # Xcode 6 | # 7 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 8 | 9 | ## Build generated 10 | build/ 11 | DerivedData/ 12 | 13 | ## Various settings 14 | *.pbxuser 15 | !default.pbxuser 16 | *.mode1v3 17 | !default.mode1v3 18 | *.mode2v3 19 | !default.mode2v3 20 | *.perspectivev3 21 | !default.perspectivev3 22 | xcuserdata/ 23 | 24 | ## Other 25 | *.moved-aside 26 | *.xccheckout 27 | *.xcscmblueprint 28 | 29 | 30 | ### Swift ### 31 | # Xcode 32 | # 33 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 34 | 35 | ## Build generated 36 | build/ 37 | DerivedData/ 38 | 39 | ## Various settings 40 | *.pbxuser 41 | !default.pbxuser 42 | *.mode1v3 43 | !default.mode1v3 44 | *.mode2v3 45 | !default.mode2v3 46 | *.perspectivev3 47 | !default.perspectivev3 48 | xcuserdata/ 49 | 50 | ## Other 51 | *.moved-aside 52 | *.xcuserstate 53 | 54 | ## Obj-C/Swift specific 55 | *.hmap 56 | *.ipa 57 | 58 | ## Playgrounds 59 | timeline.xctimeline 60 | playground.xcworkspace 61 | 62 | # Swift Package Manager 63 | # 64 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 65 | # Packages/ 66 | .build/ 67 | 68 | # CocoaPods 69 | # 70 | # We recommend against adding the Pods directory to your .gitignore. However 71 | # you should judge for yourself, the pros and cons are mentioned at: 72 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 73 | # 74 | # Pods/ 75 | 76 | # Carthage 77 | # 78 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 79 | # Carthage/Checkouts 80 | 81 | Carthage/Build 82 | 83 | # fastlane 84 | # 85 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 86 | # screenshots whenever they are needed. 87 | # For more information about the recommended setup visit: 88 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 89 | 90 | fastlane/report.xml 91 | fastlane/Preview.html 92 | fastlane/screenshots 93 | fastlane/test_output 94 | 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Refractal Development 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MotifKit.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AB6D04C81CDE9CB6003A5DA1 /* TypeFunctions.swift in Sources */ = {isa = PBXBuildFile; fileRef = AB6D04C71CDE9CB6003A5DA1 /* TypeFunctions.swift */; }; 11 | ABC750861CD5868900FD0103 /* MotifKit.h in Headers */ = {isa = PBXBuildFile; fileRef = ABC750851CD5868900FD0103 /* MotifKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12 | ABC7508D1CD5868900FD0103 /* MotifKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ABC750821CD5868800FD0103 /* MotifKit.framework */; }; 13 | ABC750921CD5868900FD0103 /* MotifKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC750911CD5868900FD0103 /* MotifKitTests.swift */; }; 14 | ABC7509D1CD595E400FD0103 /* Motif.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC7509C1CD595E400FD0103 /* Motif.swift */; }; 15 | ABC7509F1CD5964200FD0103 /* PublicDataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC7509E1CD5964200FD0103 /* PublicDataTypes.swift */; }; 16 | ABC750A11CD5968600FD0103 /* MotifTestUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC750A01CD5968600FD0103 /* MotifTestUtils.swift */; }; 17 | ABC750C71CD83C7100FD0103 /* PrivateDataTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC750C61CD83C7000FD0103 /* PrivateDataTypes.swift */; }; 18 | ABC750C91CD83D6500FD0103 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABC750C81CD83D6500FD0103 /* Utils.swift */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | ABC7508E1CD5868900FD0103 /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = ABC750791CD5868800FD0103 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = ABC750811CD5868800FD0103; 27 | remoteInfo = MotifKit; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | AB6D04C71CDE9CB6003A5DA1 /* TypeFunctions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypeFunctions.swift; sourceTree = ""; }; 33 | ABC750821CD5868800FD0103 /* MotifKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = MotifKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | ABC750851CD5868900FD0103 /* MotifKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MotifKit.h; sourceTree = ""; }; 35 | ABC750871CD5868900FD0103 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 36 | ABC7508C1CD5868900FD0103 /* MotifKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MotifKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 37 | ABC750911CD5868900FD0103 /* MotifKitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotifKitTests.swift; sourceTree = ""; }; 38 | ABC750931CD5868900FD0103 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | ABC7509C1CD595E400FD0103 /* Motif.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Motif.swift; sourceTree = ""; }; 40 | ABC7509E1CD5964200FD0103 /* PublicDataTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PublicDataTypes.swift; sourceTree = ""; }; 41 | ABC750A01CD5968600FD0103 /* MotifTestUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MotifTestUtils.swift; sourceTree = ""; }; 42 | ABC750C61CD83C7000FD0103 /* PrivateDataTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivateDataTypes.swift; sourceTree = ""; }; 43 | ABC750C81CD83D6500FD0103 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 44 | /* End PBXFileReference section */ 45 | 46 | /* Begin PBXFrameworksBuildPhase section */ 47 | ABC7507E1CD5868800FD0103 /* Frameworks */ = { 48 | isa = PBXFrameworksBuildPhase; 49 | buildActionMask = 2147483647; 50 | files = ( 51 | ); 52 | runOnlyForDeploymentPostprocessing = 0; 53 | }; 54 | ABC750891CD5868900FD0103 /* Frameworks */ = { 55 | isa = PBXFrameworksBuildPhase; 56 | buildActionMask = 2147483647; 57 | files = ( 58 | ABC7508D1CD5868900FD0103 /* MotifKit.framework in Frameworks */, 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | ABC3CF4F1CE3E92800B708BD /* Extras */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | ABC750851CD5868900FD0103 /* MotifKit.h */, 69 | ABC750871CD5868900FD0103 /* Info.plist */, 70 | ); 71 | name = Extras; 72 | sourceTree = ""; 73 | }; 74 | ABC750781CD5868800FD0103 = { 75 | isa = PBXGroup; 76 | children = ( 77 | ABC750841CD5868900FD0103 /* MotifKit */, 78 | ABC750901CD5868900FD0103 /* MotifKitTests */, 79 | ABC750831CD5868800FD0103 /* Products */, 80 | ); 81 | sourceTree = ""; 82 | }; 83 | ABC750831CD5868800FD0103 /* Products */ = { 84 | isa = PBXGroup; 85 | children = ( 86 | ABC750821CD5868800FD0103 /* MotifKit.framework */, 87 | ABC7508C1CD5868900FD0103 /* MotifKitTests.xctest */, 88 | ); 89 | name = Products; 90 | sourceTree = ""; 91 | }; 92 | ABC750841CD5868900FD0103 /* MotifKit */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | ABC7509C1CD595E400FD0103 /* Motif.swift */, 96 | ABC750C81CD83D6500FD0103 /* Utils.swift */, 97 | AB6D04C71CDE9CB6003A5DA1 /* TypeFunctions.swift */, 98 | ABC750C61CD83C7000FD0103 /* PrivateDataTypes.swift */, 99 | ABC7509E1CD5964200FD0103 /* PublicDataTypes.swift */, 100 | ABC3CF4F1CE3E92800B708BD /* Extras */, 101 | ); 102 | path = MotifKit; 103 | sourceTree = ""; 104 | }; 105 | ABC750901CD5868900FD0103 /* MotifKitTests */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | ABC750911CD5868900FD0103 /* MotifKitTests.swift */, 109 | ABC750931CD5868900FD0103 /* Info.plist */, 110 | ABC750A01CD5968600FD0103 /* MotifTestUtils.swift */, 111 | ); 112 | path = MotifKitTests; 113 | sourceTree = ""; 114 | }; 115 | /* End PBXGroup section */ 116 | 117 | /* Begin PBXHeadersBuildPhase section */ 118 | ABC7507F1CD5868800FD0103 /* Headers */ = { 119 | isa = PBXHeadersBuildPhase; 120 | buildActionMask = 2147483647; 121 | files = ( 122 | ABC750861CD5868900FD0103 /* MotifKit.h in Headers */, 123 | ); 124 | runOnlyForDeploymentPostprocessing = 0; 125 | }; 126 | /* End PBXHeadersBuildPhase section */ 127 | 128 | /* Begin PBXNativeTarget section */ 129 | ABC750811CD5868800FD0103 /* MotifKit */ = { 130 | isa = PBXNativeTarget; 131 | buildConfigurationList = ABC750961CD5868900FD0103 /* Build configuration list for PBXNativeTarget "MotifKit" */; 132 | buildPhases = ( 133 | ABC7507D1CD5868800FD0103 /* Sources */, 134 | ABC7507E1CD5868800FD0103 /* Frameworks */, 135 | ABC7507F1CD5868800FD0103 /* Headers */, 136 | ABC750801CD5868800FD0103 /* Resources */, 137 | ); 138 | buildRules = ( 139 | ); 140 | dependencies = ( 141 | ); 142 | name = MotifKit; 143 | productName = MotifKit; 144 | productReference = ABC750821CD5868800FD0103 /* MotifKit.framework */; 145 | productType = "com.apple.product-type.framework"; 146 | }; 147 | ABC7508B1CD5868900FD0103 /* MotifKitTests */ = { 148 | isa = PBXNativeTarget; 149 | buildConfigurationList = ABC750991CD5868900FD0103 /* Build configuration list for PBXNativeTarget "MotifKitTests" */; 150 | buildPhases = ( 151 | ABC750881CD5868900FD0103 /* Sources */, 152 | ABC750891CD5868900FD0103 /* Frameworks */, 153 | ABC7508A1CD5868900FD0103 /* Resources */, 154 | ); 155 | buildRules = ( 156 | ); 157 | dependencies = ( 158 | ABC7508F1CD5868900FD0103 /* PBXTargetDependency */, 159 | ); 160 | name = MotifKitTests; 161 | productName = MotifKitTests; 162 | productReference = ABC7508C1CD5868900FD0103 /* MotifKitTests.xctest */; 163 | productType = "com.apple.product-type.bundle.unit-test"; 164 | }; 165 | /* End PBXNativeTarget section */ 166 | 167 | /* Begin PBXProject section */ 168 | ABC750791CD5868800FD0103 /* Project object */ = { 169 | isa = PBXProject; 170 | attributes = { 171 | LastSwiftUpdateCheck = 0730; 172 | LastUpgradeCheck = 0730; 173 | ORGANIZATIONNAME = Refractal; 174 | TargetAttributes = { 175 | ABC750811CD5868800FD0103 = { 176 | CreatedOnToolsVersion = 7.3; 177 | }; 178 | ABC7508B1CD5868900FD0103 = { 179 | CreatedOnToolsVersion = 7.3; 180 | }; 181 | }; 182 | }; 183 | buildConfigurationList = ABC7507C1CD5868800FD0103 /* Build configuration list for PBXProject "MotifKit" */; 184 | compatibilityVersion = "Xcode 3.2"; 185 | developmentRegion = English; 186 | hasScannedForEncodings = 0; 187 | knownRegions = ( 188 | en, 189 | ); 190 | mainGroup = ABC750781CD5868800FD0103; 191 | productRefGroup = ABC750831CD5868800FD0103 /* Products */; 192 | projectDirPath = ""; 193 | projectRoot = ""; 194 | targets = ( 195 | ABC750811CD5868800FD0103 /* MotifKit */, 196 | ABC7508B1CD5868900FD0103 /* MotifKitTests */, 197 | ); 198 | }; 199 | /* End PBXProject section */ 200 | 201 | /* Begin PBXResourcesBuildPhase section */ 202 | ABC750801CD5868800FD0103 /* Resources */ = { 203 | isa = PBXResourcesBuildPhase; 204 | buildActionMask = 2147483647; 205 | files = ( 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | ABC7508A1CD5868900FD0103 /* Resources */ = { 210 | isa = PBXResourcesBuildPhase; 211 | buildActionMask = 2147483647; 212 | files = ( 213 | ); 214 | runOnlyForDeploymentPostprocessing = 0; 215 | }; 216 | /* End PBXResourcesBuildPhase section */ 217 | 218 | /* Begin PBXSourcesBuildPhase section */ 219 | ABC7507D1CD5868800FD0103 /* Sources */ = { 220 | isa = PBXSourcesBuildPhase; 221 | buildActionMask = 2147483647; 222 | files = ( 223 | AB6D04C81CDE9CB6003A5DA1 /* TypeFunctions.swift in Sources */, 224 | ABC750C71CD83C7100FD0103 /* PrivateDataTypes.swift in Sources */, 225 | ABC7509D1CD595E400FD0103 /* Motif.swift in Sources */, 226 | ABC7509F1CD5964200FD0103 /* PublicDataTypes.swift in Sources */, 227 | ABC750C91CD83D6500FD0103 /* Utils.swift in Sources */, 228 | ); 229 | runOnlyForDeploymentPostprocessing = 0; 230 | }; 231 | ABC750881CD5868900FD0103 /* Sources */ = { 232 | isa = PBXSourcesBuildPhase; 233 | buildActionMask = 2147483647; 234 | files = ( 235 | ABC750921CD5868900FD0103 /* MotifKitTests.swift in Sources */, 236 | ABC750A11CD5968600FD0103 /* MotifTestUtils.swift in Sources */, 237 | ); 238 | runOnlyForDeploymentPostprocessing = 0; 239 | }; 240 | /* End PBXSourcesBuildPhase section */ 241 | 242 | /* Begin PBXTargetDependency section */ 243 | ABC7508F1CD5868900FD0103 /* PBXTargetDependency */ = { 244 | isa = PBXTargetDependency; 245 | target = ABC750811CD5868800FD0103 /* MotifKit */; 246 | targetProxy = ABC7508E1CD5868900FD0103 /* PBXContainerItemProxy */; 247 | }; 248 | /* End PBXTargetDependency section */ 249 | 250 | /* Begin XCBuildConfiguration section */ 251 | ABC750941CD5868900FD0103 /* Debug */ = { 252 | isa = XCBuildConfiguration; 253 | buildSettings = { 254 | ALWAYS_SEARCH_USER_PATHS = NO; 255 | CLANG_ANALYZER_NONNULL = YES; 256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 257 | CLANG_CXX_LIBRARY = "libc++"; 258 | CLANG_ENABLE_MODULES = YES; 259 | CLANG_ENABLE_OBJC_ARC = YES; 260 | CLANG_WARN_BOOL_CONVERSION = YES; 261 | CLANG_WARN_CONSTANT_CONVERSION = YES; 262 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 263 | CLANG_WARN_EMPTY_BODY = YES; 264 | CLANG_WARN_ENUM_CONVERSION = YES; 265 | CLANG_WARN_INT_CONVERSION = YES; 266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 267 | CLANG_WARN_UNREACHABLE_CODE = YES; 268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 269 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 270 | COPY_PHASE_STRIP = NO; 271 | CURRENT_PROJECT_VERSION = 1; 272 | DEBUG_INFORMATION_FORMAT = dwarf; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | ENABLE_TESTABILITY = YES; 275 | GCC_C_LANGUAGE_STANDARD = gnu99; 276 | GCC_DYNAMIC_NO_PIC = NO; 277 | GCC_NO_COMMON_BLOCKS = YES; 278 | GCC_OPTIMIZATION_LEVEL = 0; 279 | GCC_PREPROCESSOR_DEFINITIONS = ( 280 | "DEBUG=1", 281 | "$(inherited)", 282 | ); 283 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 284 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 285 | GCC_WARN_UNDECLARED_SELECTOR = YES; 286 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 287 | GCC_WARN_UNUSED_FUNCTION = YES; 288 | GCC_WARN_UNUSED_VARIABLE = YES; 289 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 290 | MTL_ENABLE_DEBUG_INFO = YES; 291 | ONLY_ACTIVE_ARCH = YES; 292 | SDKROOT = iphoneos; 293 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 294 | TARGETED_DEVICE_FAMILY = "1,2"; 295 | VERSIONING_SYSTEM = "apple-generic"; 296 | VERSION_INFO_PREFIX = ""; 297 | }; 298 | name = Debug; 299 | }; 300 | ABC750951CD5868900FD0103 /* Release */ = { 301 | isa = XCBuildConfiguration; 302 | buildSettings = { 303 | ALWAYS_SEARCH_USER_PATHS = NO; 304 | CLANG_ANALYZER_NONNULL = YES; 305 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 306 | CLANG_CXX_LIBRARY = "libc++"; 307 | CLANG_ENABLE_MODULES = YES; 308 | CLANG_ENABLE_OBJC_ARC = YES; 309 | CLANG_WARN_BOOL_CONVERSION = YES; 310 | CLANG_WARN_CONSTANT_CONVERSION = YES; 311 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 312 | CLANG_WARN_EMPTY_BODY = YES; 313 | CLANG_WARN_ENUM_CONVERSION = YES; 314 | CLANG_WARN_INT_CONVERSION = YES; 315 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 316 | CLANG_WARN_UNREACHABLE_CODE = YES; 317 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 318 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 319 | COPY_PHASE_STRIP = NO; 320 | CURRENT_PROJECT_VERSION = 1; 321 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 322 | ENABLE_NS_ASSERTIONS = NO; 323 | ENABLE_STRICT_OBJC_MSGSEND = YES; 324 | GCC_C_LANGUAGE_STANDARD = gnu99; 325 | GCC_NO_COMMON_BLOCKS = YES; 326 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 327 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 328 | GCC_WARN_UNDECLARED_SELECTOR = YES; 329 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 330 | GCC_WARN_UNUSED_FUNCTION = YES; 331 | GCC_WARN_UNUSED_VARIABLE = YES; 332 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 333 | MTL_ENABLE_DEBUG_INFO = NO; 334 | SDKROOT = iphoneos; 335 | TARGETED_DEVICE_FAMILY = "1,2"; 336 | VALIDATE_PRODUCT = YES; 337 | VERSIONING_SYSTEM = "apple-generic"; 338 | VERSION_INFO_PREFIX = ""; 339 | }; 340 | name = Release; 341 | }; 342 | ABC750971CD5868900FD0103 /* Debug */ = { 343 | isa = XCBuildConfiguration; 344 | buildSettings = { 345 | CLANG_ENABLE_MODULES = YES; 346 | DEFINES_MODULE = YES; 347 | DYLIB_COMPATIBILITY_VERSION = 1; 348 | DYLIB_CURRENT_VERSION = 1; 349 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 350 | INFOPLIST_FILE = MotifKit/Info.plist; 351 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 352 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 353 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 354 | PRODUCT_BUNDLE_IDENTIFIER = co.refractal.MotifKit; 355 | PRODUCT_NAME = "$(TARGET_NAME)"; 356 | SKIP_INSTALL = YES; 357 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 358 | }; 359 | name = Debug; 360 | }; 361 | ABC750981CD5868900FD0103 /* Release */ = { 362 | isa = XCBuildConfiguration; 363 | buildSettings = { 364 | CLANG_ENABLE_MODULES = YES; 365 | DEFINES_MODULE = YES; 366 | DYLIB_COMPATIBILITY_VERSION = 1; 367 | DYLIB_CURRENT_VERSION = 1; 368 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 369 | INFOPLIST_FILE = MotifKit/Info.plist; 370 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 371 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 372 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 373 | PRODUCT_BUNDLE_IDENTIFIER = co.refractal.MotifKit; 374 | PRODUCT_NAME = "$(TARGET_NAME)"; 375 | SKIP_INSTALL = YES; 376 | }; 377 | name = Release; 378 | }; 379 | ABC7509A1CD5868900FD0103 /* Debug */ = { 380 | isa = XCBuildConfiguration; 381 | buildSettings = { 382 | INFOPLIST_FILE = MotifKitTests/Info.plist; 383 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 384 | PRODUCT_BUNDLE_IDENTIFIER = co.refractal.MotifKitTests; 385 | PRODUCT_NAME = "$(TARGET_NAME)"; 386 | }; 387 | name = Debug; 388 | }; 389 | ABC7509B1CD5868900FD0103 /* Release */ = { 390 | isa = XCBuildConfiguration; 391 | buildSettings = { 392 | INFOPLIST_FILE = MotifKitTests/Info.plist; 393 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 394 | PRODUCT_BUNDLE_IDENTIFIER = co.refractal.MotifKitTests; 395 | PRODUCT_NAME = "$(TARGET_NAME)"; 396 | }; 397 | name = Release; 398 | }; 399 | /* End XCBuildConfiguration section */ 400 | 401 | /* Begin XCConfigurationList section */ 402 | ABC7507C1CD5868800FD0103 /* Build configuration list for PBXProject "MotifKit" */ = { 403 | isa = XCConfigurationList; 404 | buildConfigurations = ( 405 | ABC750941CD5868900FD0103 /* Debug */, 406 | ABC750951CD5868900FD0103 /* Release */, 407 | ); 408 | defaultConfigurationIsVisible = 0; 409 | defaultConfigurationName = Release; 410 | }; 411 | ABC750961CD5868900FD0103 /* Build configuration list for PBXNativeTarget "MotifKit" */ = { 412 | isa = XCConfigurationList; 413 | buildConfigurations = ( 414 | ABC750971CD5868900FD0103 /* Debug */, 415 | ABC750981CD5868900FD0103 /* Release */, 416 | ); 417 | defaultConfigurationIsVisible = 0; 418 | defaultConfigurationName = Release; 419 | }; 420 | ABC750991CD5868900FD0103 /* Build configuration list for PBXNativeTarget "MotifKitTests" */ = { 421 | isa = XCConfigurationList; 422 | buildConfigurations = ( 423 | ABC7509A1CD5868900FD0103 /* Debug */, 424 | ABC7509B1CD5868900FD0103 /* Release */, 425 | ); 426 | defaultConfigurationIsVisible = 0; 427 | defaultConfigurationName = Release; 428 | }; 429 | /* End XCConfigurationList section */ 430 | }; 431 | rootObject = ABC750791CD5868800FD0103 /* Project object */; 432 | } 433 | -------------------------------------------------------------------------------- /MotifKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MotifKit.xcodeproj/xcshareddata/xcschemes/MotifKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | 76 | 82 | 83 | 89 | 90 | 91 | 92 | 94 | 95 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /MotifKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.9 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /MotifKit/Motif.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotifKit.swift 3 | // MotifKit 4 | // 5 | // Created by Jonathan Kingsley on 01/05/2016. 6 | // Copyright © 2016 Refractal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public class Motif { 12 | // This prevents others from using the default '()' initializer for this class. 13 | private init() {} 14 | 15 | // Setup our private variable stack 16 | static let sharedInstance = Motif() 17 | 18 | var currentTheme: String? = nil 19 | var themes = Set() 20 | var updateEvents = [UpdateEvent]() 21 | 22 | public class func addTheme(theme: MotifTheme) -> Bool { 23 | // First, parse our theme class 24 | let parseData: (String, ThemeData)? 25 | let error: ErrorType? 26 | 27 | do { 28 | parseData = try Utils.parseTheme(theme) 29 | error = nil 30 | } catch let thrownError { 31 | parseData = nil 32 | error = thrownError 33 | } 34 | 35 | // Double-check we don't have an invalid theme 36 | guard let parsedTheme: (name: String, data: ThemeData) = parseData else { 37 | fatalError("MotifKit Error: INVALID_THEME (\(error!))") 38 | } 39 | 40 | let parsedThemeObject = ParsedTheme(parsedTheme) 41 | 42 | if(sharedInstance.themes.contains(parsedThemeObject)) { 43 | return false 44 | } 45 | 46 | // First, store the theme 47 | sharedInstance.themes.insert(parsedThemeObject) 48 | 49 | // Second, set the default if we need to 50 | if sharedInstance.currentTheme == nil { 51 | sharedInstance.currentTheme = parsedTheme.name 52 | } 53 | 54 | return true 55 | } 56 | 57 | public class func getThemes() -> [String] { 58 | // Return a string list of the currently loaded themes 59 | return sharedInstance.themes.map({$0.name}) 60 | } 61 | 62 | public class func getCurrentTheme() -> String? { 63 | return sharedInstance.currentTheme 64 | } 65 | 66 | public class func resetThemes() { 67 | // Reset all our currently loaded themes 68 | sharedInstance.themes.removeAll() 69 | sharedInstance.currentTheme = nil 70 | } 71 | 72 | public class func setTheme(key: String) -> Bool { 73 | // If the theme doesn't exist, return false 74 | guard sharedInstance.themes.contains({ $0.name == key }) else { return false} 75 | 76 | // Otherwise, set the theme and update all views 77 | sharedInstance.currentTheme = key 78 | 79 | for event in sharedInstance.updateEvents { 80 | event() 81 | } 82 | 83 | return true 84 | } 85 | 86 | public class func setEnum(type: T.Type, key: String, target: NSObject..., variable: String, file: String = #file) { 87 | for passedClass in target { 88 | sharedInstance.setObject(type, key: key, file: file, completion: { object in 89 | // If it's an enum, work around that and set the rawValue 90 | passedClass.setValue(object.rawValue as? AnyObject, forKey: variable) 91 | }) 92 | } 93 | } 94 | 95 | public class func setObject(type: T.Type, key: String, file: String = #file, completion: (T) -> Void) { 96 | sharedInstance.setObject(type, key: key, file: file, completion: completion) 97 | } 98 | 99 | public class func setObjects(type: T.Type, keys: [String], file: String = #file, completion: ([T]) -> Void) { 100 | func applyObjects() { 101 | var data = [T]() 102 | 103 | for key in keys { 104 | sharedInstance.setObjectOnce(type, key: key, file: file, completion: { result in 105 | data.append(result) 106 | }) 107 | } 108 | 109 | return completion(data) 110 | } 111 | 112 | applyObjects() 113 | 114 | sharedInstance.updateEvents.append({ 115 | applyObjects() 116 | }) 117 | } 118 | 119 | public class func setObject(type: T.Type, key: String, target: NSObject..., variable: String, file: String = #file) { 120 | for passedClass in target { 121 | 122 | sharedInstance.setObject(type, key: key, file: file, completion: { object in 123 | // If it's an object, handle it as an object 124 | passedClass.setValue(object as? AnyObject, forKey: variable) 125 | }) 126 | } 127 | } 128 | 129 | public class func onThemeChange(completion: () -> Void) { 130 | sharedInstance.updateEvents.append(completion) 131 | } 132 | 133 | private func setObject(type: T.Type, key: String, file: String, completion: (T) -> Void) { 134 | // Remove any spaces from the file string, and get the name of the calling class from its file path 135 | let patchedFile = file.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet())! 136 | let className = (NSURL(string: patchedFile)!.URLByDeletingPathExtension?.lastPathComponent)! 137 | 138 | func applyObject() { 139 | do { 140 | let object: T = try Utils.getRelevantObject(className, key: key) 141 | 142 | completion(object) 143 | } catch let error { 144 | fatalError("MotifKit Error: SET_OBJECT (\(error))") 145 | } 146 | } 147 | 148 | // Initially set the object using our helper function 149 | applyObject() 150 | 151 | // And then add a handler to set the object and add it to our update event stack 152 | updateEvents.append({ 153 | applyObject() 154 | }) 155 | } 156 | 157 | private func setObjectOnce(type: T.Type, key: String, file: String, completion: (T) -> Void) { 158 | // Remove any spaces from the file string, and get the name of the calling class from its file path 159 | let patchedFile = file.stringByAddingPercentEncodingWithAllowedCharacters(.URLQueryAllowedCharacterSet())! 160 | let className = (NSURL(string: patchedFile)!.URLByDeletingPathExtension?.lastPathComponent)! 161 | 162 | do { 163 | let object: T = try Utils.getRelevantObject(className, key: key) 164 | 165 | completion(object) 166 | } catch let error { 167 | fatalError("MotifKit Error: SET_OBJECT (\(error))") 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /MotifKit/MotifKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // MotifKit.h 3 | // MotifKit 4 | // 5 | // Created by Jonathan Kingsley on 01/05/2016. 6 | // Copyright © 2016 Refractal. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for MotifKit. 12 | FOUNDATION_EXPORT double MotifKitVersionNumber; 13 | 14 | //! Project version string for MotifKit. 15 | FOUNDATION_EXPORT const unsigned char MotifKitVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /MotifKit/PrivateDataTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotifDataTypes.swift 3 | // MotifKit 4 | // 5 | // Created by Jonathan Kingsley on 03/05/2016. 6 | // Copyright © 2016 Refractal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // These types are used to define and store the types of information Motif can handle 12 | 13 | // [Class: [Item: Value]] 14 | typealias ThemeData = [String: [String: Any]] 15 | 16 | // This event is called to update our variables 17 | typealias UpdateEvent = () -> Void 18 | 19 | // Wrap our basic parsed theme info in a Hashable struct to store in a Set 20 | struct ParsedTheme: Hashable { 21 | var name: String 22 | var data: ThemeData 23 | 24 | init(_ tuple: (name: String, data: ThemeData)) { 25 | name = tuple.name 26 | data = tuple.data 27 | } 28 | 29 | var hashValue: Int { 30 | return self.name.hashValue 31 | } 32 | } 33 | 34 | func ==(lhs: ParsedTheme, rhs: ParsedTheme) -> Bool { 35 | return lhs.name == rhs.name 36 | } -------------------------------------------------------------------------------- /MotifKit/PublicDataTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotifTheme.swift 3 | // MotifKit 4 | // 5 | // Created by Jonathan Kingsley on 01/05/2016. 6 | // Copyright © 2016 Refractal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // This will be our main protocol all Themes have to conform to. 12 | // Most other info is up to the user, but at a minimum we should have a defined name 13 | 14 | public protocol MotifTheme { 15 | var classes: [MotifClass] { get } 16 | } 17 | 18 | // This protocol is what all our individual class data structs will conform to 19 | // This allows us to iterate and store these classes without weirdness in typecasting 20 | 21 | public protocol MotifClass { 22 | 23 | } 24 | 25 | // This enum handles all of our Error types and general throws 26 | // This allows us to quickly and efficiently provide error data while conforming to Swift 2's error handling 27 | 28 | public enum MotifError: ErrorType { 29 | // We only support structs 30 | case StructRequired 31 | // The class does not exist in the struct 32 | case MissingClassDefinition(name: String) 33 | // The entity does not contain a default data class 34 | case NoDefaultClass 35 | // There is no currently loaded template 36 | case NoLoadedTemplate 37 | // The provided key cannot be found in any theme class 38 | case InvalidKey(name: String) 39 | // The provided object does not match the type defined 40 | case TypeMismatch(name: String, type: String) 41 | } -------------------------------------------------------------------------------- /MotifKit/TypeFunctions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotifTypeFunctions.swift 3 | // MotifKit 4 | // 5 | // Created by Jonathan Kingsley on 07/05/2016. 6 | // Copyright © 2016 Refractal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Motif { 12 | 13 | // Define our completion handler versions 14 | 15 | public class func setColor(key: String, file: String = #file, completion: (UIColor) -> Void) { 16 | setObject(UIColor.self, key: key, file: file, completion: completion) 17 | } 18 | 19 | public class func setAttributes(key: String, file: String = #file, completion: ([String: AnyObject]) -> Void) { 20 | setObject([String: AnyObject].self, key: key, file: file, completion: completion) 21 | } 22 | 23 | public class func setFont(key: String, size: CGFloat, file: String = #file, completion completionHandler: (UIFont) -> Void) { 24 | setObject(UIFont.self, key: key, file: file, completion: { (font: UIFont) in 25 | completionHandler(font.fontWithSize(size)) 26 | }) 27 | } 28 | 29 | public class func setColors(keys: [String], file: String = #file, completion: ([UIColor]) -> Void) { 30 | setObjects(UIColor.self, keys: keys, file: file, completion: completion) 31 | } 32 | 33 | public class func setAttributes(keys: [String], file: String = #file, completion: ([[String: AnyObject]]) -> Void) { 34 | setObjects([String: AnyObject].self, keys: keys, file: file, completion: completion) 35 | } 36 | 37 | public class func setFonts(keys: [String], sizes: [CGFloat], file: String = #file, completion completionHandler: ([UIFont]) -> Void) { 38 | setObjects(UIFont.self, keys: keys, file: file, completion: { (fonts: [UIFont]) in 39 | var newResult = [UIFont]() 40 | 41 | for (index, font) in fonts.enumerate() { 42 | newResult.append(font.fontWithSize(sizes[index])) 43 | } 44 | 45 | return completionHandler(newResult) 46 | }) 47 | } 48 | 49 | // Define our object specific versions 50 | 51 | public class func setColor(key: String, target: NSObject..., variable: String, file: String = #file) { 52 | for passedClass in target { 53 | setObject(UIColor.self, key: key, target: passedClass, variable: variable, file: file) 54 | } 55 | } 56 | 57 | public class func setAttributes(key: String, target: NSObject..., variable: String, file: String = #file) { 58 | for passedClass in target { 59 | setObject([String: AnyObject].self, key: key, target: passedClass, variable: variable, file: file) 60 | } 61 | } 62 | 63 | public class func setFont(key: String, target: NSObject..., variable: String, size: CGFloat, file: String = #file) { 64 | for passedClass in target { 65 | setObject(UIFont.self, key: key, file: file, completion: { (font: UIFont) in 66 | passedClass.setValue(font.fontWithSize(size), forKey: variable) 67 | }) 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /MotifKit/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotifUtils.swift 3 | // MotifKit 4 | // 5 | // Created by Jonathan Kingsley on 03/05/2016. 6 | // Copyright © 2016 Refractal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class Utils { 12 | class func parseTheme(theme: MotifTheme) throws -> (String, ThemeData) { 13 | // First, get the theme name 14 | let themeName = String(theme.dynamicType) 15 | 16 | // Reflect the theme so we can manipulate it 17 | let theme = Mirror(reflecting: theme) 18 | 19 | // Make sure we're analyzing a struct 20 | guard theme.displayStyle == .Struct else { throw MotifError.StructRequired } 21 | 22 | // Get the index of our classes array 23 | let classesIndex = theme.children.indexOf({ child in 24 | return child.label == "classes" 25 | }) 26 | 27 | // Now, extract our classes array and start to parse it 28 | let classesArray = theme.children[classesIndex!].value as! [MotifClass] 29 | var finalClassesArray = ThemeData() 30 | 31 | for (themeClass) in classesArray { 32 | let className = String(themeClass.dynamicType) 33 | let classMirror = Mirror(reflecting: themeClass) 34 | 35 | var classProperties = [String: Any]() 36 | 37 | // Interate through our object properties 38 | for (label, value) in classMirror.children { 39 | // If the property has a value and a label we're good 40 | if let propertyName = label { 41 | classProperties[propertyName] = value 42 | } 43 | } 44 | // Set the class properties for the class 45 | finalClassesArray[className] = classProperties 46 | } 47 | 48 | // Double check we have a Default object 49 | guard finalClassesArray.keys.contains("Default") else { throw MotifError.NoDefaultClass } 50 | 51 | // And finally return all our data 52 | return (themeName, finalClassesArray) 53 | } 54 | 55 | class func getRelevantObject(file: String, key: String) throws -> T { 56 | // First, get our current theme name 57 | guard let currentThemeKey = Motif.sharedInstance.currentTheme else { throw MotifError.NoLoadedTemplate } 58 | 59 | // Second, acquire its index in the stack 60 | let currentThemeIndex = Motif.sharedInstance.themes.indexOf({ object in 61 | return object.name == currentThemeKey 62 | }) 63 | 64 | // Third, acquire the actual object 65 | let currentTheme = Motif.sharedInstance.themes[currentThemeIndex!].data 66 | 67 | // Then check the Class-specific data 68 | let classData: [String: Any]? = currentTheme[file] 69 | // Second, check the generic data.. 70 | let defaultData: [String: Any]? = currentTheme["Default"] 71 | 72 | var objectData: Any? 73 | 74 | // Finally, check both sets for a matching key 75 | if (classData != nil) { 76 | objectData = classData![key] 77 | } 78 | 79 | if (defaultData != nil && objectData == nil) { 80 | objectData = defaultData![key] 81 | } 82 | 83 | // If there's no match, and you don't have a class definition, throw a missing class error 84 | if (objectData == nil && classData == nil) { 85 | throw MotifError.MissingClassDefinition(name: file) 86 | } 87 | 88 | // If we don't match either, throw an invalid key error 89 | guard (objectData != nil) else { throw MotifError.InvalidKey(name: key) } 90 | 91 | /// If our given type doesn't match, then return an error 92 | guard objectData is T else { throw MotifError.TypeMismatch(name: key, type: String(T.self)) } 93 | 94 | // And then return our lovely value 95 | return objectData as! T 96 | } 97 | } -------------------------------------------------------------------------------- /MotifKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /MotifKitTests/MotifKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotifKitTests.swift 3 | // MotifKitTests 4 | // 5 | // Created by Jonathan Kingsley on 01/05/2016. 6 | // Copyright © 2016 Refractal. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import MotifKit 11 | 12 | class MotifKitTests: XCTestCase { 13 | 14 | let fontToCompare = UIFont(name: "Avenir", size: 13.2) 15 | let attributesToCompare: [String : AnyObject] = [ 16 | NSForegroundColorAttributeName: UIColor.cyanColor(), 17 | NSFontAttributeName : UIFont(name: "Avenir-BlackOblique", size: 15)! 18 | ] 19 | 20 | override func setUp() { 21 | super.setUp() 22 | // Put setup code here. This method is called before the invocation of each test method in the class. 23 | } 24 | 25 | override func tearDown() { 26 | // Put teardown code here. This method is called after the invocation of each test method in the class. 27 | 28 | Motif.resetThemes() 29 | 30 | super.tearDown() 31 | } 32 | 33 | func testAddingTheme() { 34 | // Ensure there are no themes loaded initially 35 | XCTAssertNil(Motif.getCurrentTheme()) 36 | XCTAssertTrue(Motif.getThemes().isEmpty) 37 | 38 | // Add a theme to the stack 39 | let result = Motif.addTheme(DarkTheme()) 40 | 41 | XCTAssertTrue(result) 42 | 43 | // Check the theme is loaded 44 | XCTAssertEqual(Motif.getCurrentTheme(), "DarkTheme") 45 | XCTAssertEqual(Motif.getThemes(), ["DarkTheme"]) 46 | 47 | // Wipe the theme 48 | Motif.resetThemes() 49 | 50 | // Check all themes are reset 51 | XCTAssertNil(Motif.getCurrentTheme()) 52 | XCTAssertTrue(Motif.getThemes().isEmpty) 53 | } 54 | 55 | func testAddingThemes() { 56 | // Ensure there are no themes loaded initially 57 | XCTAssertNil(Motif.getCurrentTheme()) 58 | XCTAssertTrue(Motif.getThemes().isEmpty) 59 | 60 | // Add a theme to the stack 61 | let firstResult = Motif.addTheme(DarkTheme()) 62 | 63 | XCTAssertTrue(firstResult) 64 | 65 | // Check the theme is loaded 66 | XCTAssertEqual(Motif.getCurrentTheme(), "DarkTheme") 67 | XCTAssertEqual(Motif.getThemes(), ["DarkTheme"]) 68 | 69 | // Add a second theme to the stack 70 | let secondResult = Motif.addTheme(LightTheme()) 71 | 72 | XCTAssertTrue(secondResult) 73 | 74 | // Check the theme is loaded 75 | XCTAssertEqual(Motif.getCurrentTheme(), "DarkTheme") 76 | XCTAssertEqual(Motif.getThemes(), ["LightTheme", "DarkTheme"]) 77 | 78 | // Wipe the theme 79 | Motif.resetThemes() 80 | 81 | // Check all themes are reset 82 | XCTAssertNil(Motif.getCurrentTheme()) 83 | XCTAssertTrue(Motif.getThemes().isEmpty) 84 | } 85 | 86 | func testDuplicateThemes() { 87 | // Add a theme to the stack 88 | let firstResult = Motif.addTheme(DarkTheme()) 89 | 90 | XCTAssertTrue(firstResult) 91 | 92 | // Check the theme is loaded 93 | XCTAssertEqual(Motif.getCurrentTheme(), "DarkTheme") 94 | XCTAssertEqual(Motif.getThemes(), ["DarkTheme"]) 95 | 96 | // Then, add the same theme again 97 | let secondResult = Motif.addTheme(DarkTheme()) 98 | 99 | XCTAssertFalse(secondResult) 100 | 101 | // Confirm no changes were made 102 | XCTAssertEqual(Motif.getCurrentTheme(), "DarkTheme") 103 | XCTAssertEqual(Motif.getThemes(), ["DarkTheme"]) 104 | } 105 | 106 | func testGettingObjectsFromCompletion() { 107 | let expectations = [ 108 | expectationWithDescription("TestColor should be Green"), 109 | expectationWithDescription("TestFont should be Avenir at 13.2pt"), 110 | expectationWithDescription("TestFont should be Avenir at 12pt, and TestFont2 should be AvenirBO at 20pt"), 111 | expectationWithDescription("TestAttributes should match our test set"), 112 | expectationWithDescription("TestObject should match the static data of TestObjectClass"), 113 | expectationWithDescription("TestEnum should match TestEnumType.Two"), 114 | expectationWithDescription("String1, String2, String3 should match 'This is a string', 'So is this' and 'this is too'") 115 | ] 116 | 117 | Motif.addTheme(DarkTheme()) 118 | 119 | Motif.setColor("TestColor", completion: { color in 120 | // Confirm it matches 121 | XCTAssertEqual(color, UIColor.greenColor()) 122 | expectations[0].fulfill() 123 | }) 124 | 125 | Motif.setFont("TestFont", size: 13.2, completion: { font in 126 | let fontToCompare = UIFont(name: "Avenir", size: 13.2) 127 | 128 | XCTAssertEqual(font, fontToCompare) 129 | expectations[1].fulfill() 130 | }) 131 | 132 | Motif.setFonts(["TestFont", "TestFont2"], sizes: [12, 22], completion: { fonts in 133 | let fontsToCompare = [UIFont(name: "Avenir", size: 12)!, UIFont(name: "Avenir-BlackOblique", size: 22)!] 134 | XCTAssertEqual(fonts, fontsToCompare) 135 | expectations[2].fulfill() 136 | }) 137 | 138 | Motif.setAttributes("TestAttributes", completion: { attributes in 139 | XCTAssertTrue(attributes == self.attributesToCompare) 140 | expectations[3].fulfill() 141 | }) 142 | 143 | Motif.setObject(TestObjectClass.self, key: "TestObject", completion: { object in 144 | XCTAssertEqual(object.data, TestObjectClass().data) 145 | expectations[4].fulfill() 146 | }) 147 | 148 | Motif.setObject(TestEnumType.self, key: "TestEnum", completion: { object in 149 | XCTAssertEqual(object, TestEnumType.Two) 150 | expectations[5].fulfill() 151 | }) 152 | 153 | Motif.setObjects(String.self, keys: ["String1", "String2", "String3"], completion: { strings in 154 | let compareTo = ["This is a string", "So is this", "and this is too"] 155 | 156 | XCTAssertEqual(strings, compareTo) 157 | expectations[6].fulfill() 158 | }) 159 | 160 | waitForExpectationsWithTimeout(2) { error in 161 | if let error = error { 162 | print("Error: \(error.localizedDescription)") 163 | } 164 | } 165 | } 166 | 167 | func testReloadingTheme() { 168 | let firstExpectation = expectationWithDescription("TestColor should be Green") 169 | let secondExpectation = expectationWithDescription("TestColor should be Blue") 170 | var count = 0 171 | 172 | Motif.addTheme(DarkTheme()) 173 | Motif.addTheme(LightTheme()) 174 | 175 | Motif.setColor("TestColor", completion: { color in 176 | // Confirm it matches 177 | if count > 0 { 178 | XCTAssertEqual(color, UIColor.blueColor()) 179 | firstExpectation.fulfill() 180 | } else { 181 | XCTAssertEqual(color, UIColor.greenColor()) 182 | secondExpectation.fulfill() 183 | } 184 | 185 | count += 1 186 | }) 187 | 188 | Motif.setTheme("LightTheme") 189 | 190 | waitForExpectationsWithTimeout(2) { error in 191 | if let error = error { 192 | print("Error: \(error.localizedDescription)") 193 | } 194 | } 195 | } 196 | 197 | func testSettingObjectsWithData() { 198 | 199 | let objectOne = UILabel(frame: CGRectZero) 200 | let objectTwo = UILabel(frame: CGRectZero) 201 | let dataObject = TestObjectClass() 202 | 203 | Motif.addTheme(DarkTheme()) 204 | 205 | Motif.setColor("TestColor", target: [objectOne, objectTwo], variable: "textColor") 206 | 207 | XCTAssertEqual(objectOne.textColor, UIColor.greenColor()) 208 | XCTAssertEqual(objectTwo.textColor, UIColor.greenColor()) 209 | 210 | Motif.setFont("TestFont", target: objectOne, variable: "font", size: 13.2) 211 | 212 | XCTAssertEqual(objectOne.font, fontToCompare) 213 | 214 | Motif.setAttributes("TestAttributes", target: dataObject, variable: "attribute") 215 | 216 | XCTAssertTrue(dataObject.attribute == attributesToCompare) 217 | 218 | Motif.setEnum(TestEnumType.self, key: "TestEnum", target: dataObject, variable: "enumerator") 219 | 220 | XCTAssertEqual(dataObject.enumerator, TestEnumType.Two) 221 | 222 | Motif.setObject(String.self, key: "TestString", target: dataObject, variable: "data") 223 | 224 | XCTAssertEqual(dataObject.data, "Testing") 225 | } 226 | 227 | func testGettingDefaultObject() { 228 | Motif.addTheme(DarkTheme()) 229 | 230 | Motif.setColor("TestDefaultColor", completion: { color in 231 | // Confirm it matches 232 | XCTAssertEqual(color, UIColor.purpleColor()) 233 | }) 234 | } 235 | } -------------------------------------------------------------------------------- /MotifKitTests/MotifTestUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MotifTestThemes.swift 3 | // MotifKit 4 | // 5 | // Created by Jonathan Kingsley on 01/05/2016. 6 | // Copyright © 2016 Refractal. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import MotifKit 11 | 12 | enum TestEnumType: Int { 13 | case One 14 | case Two 15 | case Three 16 | case Four 17 | } 18 | 19 | class TestObjectClass: NSObject { 20 | var data: String = "stuff" 21 | var attribute = [String: AnyObject]() 22 | var enumerator = TestEnumType.One 23 | 24 | override func setValue(value: AnyObject?, forKey key: String) { 25 | if(value == nil) { 26 | return 27 | } 28 | 29 | switch key { 30 | case "enumerator": 31 | enumerator = TestEnumType(rawValue: value as! Int)! 32 | break 33 | case "data": 34 | data = value as! String 35 | break 36 | case "attribute": 37 | attribute = value as! [String: AnyObject] 38 | break 39 | default: 40 | break 41 | } 42 | } 43 | } 44 | 45 | struct DarkTheme: MotifTheme { 46 | let classes: [MotifClass] = [Default(), MotifKitTests()] 47 | 48 | struct Default: MotifClass { 49 | let TestDefaultColor = UIColor.purpleColor() 50 | } 51 | 52 | struct MotifKitTests: MotifClass { 53 | let TestColor = UIColor.greenColor() 54 | let TestFont = UIFont(name: "Avenir", size: 1) 55 | let TestFont2 = UIFont(name: "Avenir-BlackOblique", size: 1) 56 | let TestAttributes: [String : AnyObject] = [ 57 | NSForegroundColorAttributeName: UIColor.cyanColor(), 58 | NSFontAttributeName : UIFont(name: "Avenir-BlackOblique", size: 15)! 59 | ] 60 | let TestEnum = TestEnumType.Two 61 | let TestObject = TestObjectClass() 62 | let TestString = "Testing" 63 | 64 | let String1 = "This is a string" 65 | let String2 = "So is this" 66 | let String3 = "and this is too" 67 | } 68 | } 69 | 70 | struct LightTheme: MotifTheme { 71 | let classes: [MotifClass] = [Default(), MotifKitTests()] 72 | 73 | struct Default: MotifClass { 74 | let TestDefaultColor = UIColor.purpleColor() 75 | } 76 | 77 | struct MotifKitTests: MotifClass { 78 | let TestColor = UIColor.blueColor() 79 | } 80 | } 81 | 82 | 83 | public func ==(lhs: [String: AnyObject], rhs: [String: AnyObject] ) -> Bool { 84 | return NSDictionary(dictionary: lhs).isEqualToDictionary(rhs) 85 | } -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RefractalDev/Motif/6573809bdd4b5c61e79f4d3133b4dee672e1d2a2/icon.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![MotifKit](icon.png) 2 | 3 |

4 | Platform: iOS 8+ 5 | Language: Swift 2 6 | Carthage compatible 7 | License: MIT 8 |

9 | 10 |

11 | Installation 12 | • Issues 13 | • License 14 |

15 | 16 | Motif is a lightweight way to handle, reload and centralise all of your Colors, Fonts and more. 17 | 18 | ```swift 19 | struct SomeTheme: MotifTheme { 20 | let classes: [MotifClass] = [SomeMenu()] 21 | 22 | struct SomeMenu: MotifClass { 23 | let TextColor = UIColor.greenColor() 24 | } 25 | } 26 | 27 | Motif.addTheme(SomeTheme()) 28 | 29 | ... later ... 30 | 31 | class SomeMenu { 32 | 33 | let someView = SomeView(...) 34 | 35 | Motif.setColor("TextColor", target: someView, variable: "textColor") 36 | 37 | // someView's textColor should now be Green 38 | } 39 | 40 | ... later ... 41 | 42 | Motif.setTheme("NewTheme") 43 | 44 | // someView's textColor variable now equals NewTheme's TextColor 45 | 46 | ``` 47 | 48 | You can store: 49 | * [x] Colors 50 | * [x] Fonts 51 | * [x] Arrays of attributes 52 | * [x] Strings 53 | * [x] Any object (With some manual labor) 54 | 55 | ## Compatibility 56 | 57 | Motif requires iOS 8+ and is compatible with **Swift 2** projects. Objective-C support is unlikely. 58 | 59 | ## Installation 60 | 61 | Installation for [Carthage](https://github.com/Carthage/Carthage) is extremely simple: 62 | 63 | `github "refractaldev/Motif"` 64 | 65 | As for [CocoaPods](https://cocoapods.org), we currently don't support the platform. 66 | However, any Pull Requests adding support will be gladly accepted. 67 | 68 | ## Usage 69 | 70 | ### Basic setup 71 | Pantry provides serialization of most basic UI customisation types (`UIColor`, `UIFont`, `[String: AnyObject]`) with no setup. You can use it to create a theme for your app like this: 72 | 73 | ```swift 74 | struct DarkTheme: MotifTheme { 75 | let classes: [MotifClass] = [Default(), SomeMenu()] 76 | 77 | // Optionally, you can setup a Default class which variables can fallback to 78 | struct Default: MotifClass { 79 | let TextColor = UIColor.darkGrayColor() 80 | let ThemeDescription: String = "This is a description for our theme, it could be shown in a setting view." 81 | } 82 | 83 | struct SomeMenu: MotifClass { 84 | let TextColor = UIColor.lightGrayColor() 85 | let TextFont = UIFont(name: "Avenir", size: 1)! 86 | let TextAttributes: [String: AnyObject] = [ 87 | NSStrikethroughStyleAttributeName: 1 88 | ] 89 | } 90 | } 91 | 92 | Motif.addTheme(DarkTheme()) 93 | ``` 94 | 95 | You can then apply attributes from your newly created theme to a view or group of views like this: 96 | 97 | ```swift 98 | // Apply to a single object 99 | Motif.setColor( 100 | testLabel!, 101 | variable: "textColor", 102 | key: "TextColor" 103 | ) 104 | 105 | // Apply to a group of objects 106 | Motif.setFont( 107 | [testLabel1!, textLabel2!], 108 | variable: "textColor", 109 | key: "TextColor", 110 | size: 20 111 | ) 112 | 113 | // Apply to an object using a completion handler 114 | Motif.setAttributes("TextAttributes", completion: { attributes in 115 | self.testLabel!.attributedText = NSAttributedString(string: "This is a test", attributes: attributes) 116 | }) 117 | 118 | //Want to load a custom attribute? You can do that too.. 119 | Motif.setObject(String.self, key: "ThemeDescription", completion: { string in 120 | print(string) 121 | }) 122 | ``` 123 | 124 | 125 | ### Automagic Theme Swapping 126 | Let's say we've now added a second theme, called LightTheme, with a new set of colors, fonts and even attributes. How do we set all our variables to this new theme's definitions? 127 | 128 | Simple: One line of code. 129 | 130 | ```swift 131 | Motif.setTheme("LightTheme") 132 | ``` 133 | 134 | It's literally that easy. 135 | 136 | Just set the new theme, and all your UI using the theme's attributes should automagically reload to match your new selection, just like that. 137 | 138 | ## License 139 | 140 | Motif uses the MIT license. Please file an issue if you have any questions or if you'd like to share how you're using this tool. 141 | 142 | ## Attribution 143 | 144 | Motif "paint can icon" by [Drew Ellis from the Noun Project](https://thenounproject.com/term/paint-can/6073/) 145 | --------------------------------------------------------------------------------