diff --git a/Package.swift b/Package.swift index 64ab5e8..df82f3f 100644 --- a/Package.swift +++ b/Package.swift @@ -22,7 +22,8 @@ let package = Package( ), .testTarget( name: "YCoreUITests", - dependencies: ["YCoreUI"] + dependencies: ["YCoreUI"], + resources: [.process("Assets")] ) ] ) diff --git a/Sources/YCoreUI/Protocols/Colorable.swift b/Sources/YCoreUI/Protocols/Colorable.swift new file mode 100644 index 0000000..70e28a8 --- /dev/null +++ b/Sources/YCoreUI/Protocols/Colorable.swift @@ -0,0 +1,69 @@ +// +// Colorable.swift +// YCoreUI +// +// Created by Panchami Shenoy on 26/10/22. +// Copyright © 2022 Y Media Labs. All rights reserved. +// + +import UIKit + +/// Any named color asset that can be loaded from an asset catalog (primarily for use with string-based enums). +/// +/// All properties and functions have default implementations. At a minimum just have your string-based enum conform +/// to `Colorable` (and have an asset catalog with matching assets). If your enum and assets live inside a Swift +/// package, override `bundle` to return `.module`. If your assets are categorized within their asset catalog by +/// a namespace, then override `namespace` to return the proper string prefix. +public protocol Colorable: RawRepresentable where RawValue == String { + /// The bundle containing the color assets for this enum (default is `.main`) + static var bundle: Bundle { get } + + /// Optional namespace for the color assets (default is `nil`) + static var namespace: String? { get } + + /// Fallback color to use in case a color asset cannot be loaded (default is `.systemPink`) + static var fallbackColor: UIColor { get } + + /// Loads the named color. + /// + /// Default implementation uses `UIColor(named:in:compatibleWith:)` passing in the associated `namespace` + /// (prepended to `rawValue`) and `bundle`. + /// - Returns: The named color or else `nil` if the named asset cannot be loaded + func loadColor() -> UIColor? + + /// A color asset for this name value. + /// + /// Default implementation calls `loadColor` and nil-coalesces to `fallbackColor`. + var color: UIColor { get } +} + +extension Colorable { + /// The bundle containing the color assets for this enum (default is `.main`) + public static var bundle: Bundle { .main } + + /// Optional namespace for the color assets (default is `nil`) + public static var namespace: String? { nil } + + /// Fallback color to use in case a color asset cannot be loaded (default is `.systemPink`) + public static var fallbackColor: UIColor { .systemPink } + + /// Loads the named color. + /// + /// Default implementation uses `UIColor(named:in:compatibleWith:)` passing in the associated `namespace` + /// (prepended to `rawValue`) and `bundle`. + /// - Returns: The named color or else `nil` if the named asset cannot be loaded + public func loadColor() -> UIColor? { + let name: String + if let validNamespace = Self.namespace { + name = "\(validNamespace)/\(rawValue)" + } else { + name = rawValue + } + return UIColor(named: name, in: Self.bundle, compatibleWith: nil) + } + + /// A color asset for this name value. + /// + /// Default implementation calls `loadColor` and nil-coalesces to `fallbackColor`. + public var color: UIColor { loadColor() ?? Self.fallbackColor } +} diff --git a/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Contents.json b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/Contents.json b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/Contents.json new file mode 100644 index 0000000..6e96565 --- /dev/null +++ b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/error100.colorset/Contents.json b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/error100.colorset/Contents.json new file mode 100644 index 0000000..03e60a9 --- /dev/null +++ b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/error100.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD2", + "green" : "0xD4", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/error50.colorset/Contents.json b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/error50.colorset/Contents.json new file mode 100644 index 0000000..0e7a791 --- /dev/null +++ b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/Error/error50.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF0", + "green" : "0xF1", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/YCoreUITests/Assets/Colors/Media.xcassets/warning100.colorset/Contents.json b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/warning100.colorset/Contents.json new file mode 100644 index 0000000..99f657d --- /dev/null +++ b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/warning100.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xAF", + "green" : "0xC7", + "red" : "0xFA" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/YCoreUITests/Assets/Colors/Media.xcassets/warning50.colorset/Contents.json b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/warning50.colorset/Contents.json new file mode 100644 index 0000000..b5beed4 --- /dev/null +++ b/Tests/YCoreUITests/Assets/Colors/Media.xcassets/warning50.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xC6", + "green" : "0xD1", + "red" : "0xF1" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Tests/YCoreUITests/Protocols/ColorableTests.swift b/Tests/YCoreUITests/Protocols/ColorableTests.swift new file mode 100644 index 0000000..ca927a0 --- /dev/null +++ b/Tests/YCoreUITests/Protocols/ColorableTests.swift @@ -0,0 +1,73 @@ +// +// ColorableTests.swift +// YCoreUITests +// +// Created by Panchami Shenoy on 26/10/22. +// Copyright © 2022 Y Media Labs. All rights reserved. +// + +import XCTest +@testable import YCoreUI + +final class ColorableTests: XCTestCase { + func testBundle() { + XCTAssertEqual(WarningColors.bundle, .module) + XCTAssertEqual(ErrorColors.bundle, .module) + XCTAssertEqual(PrimaryColors.bundle, .main) + } + + func testNamespace() { + XCTAssertNil(WarningColors.namespace) + XCTAssertEqual(ErrorColors.namespace, "Error") + XCTAssertNil(PrimaryColors.namespace) + } + + func testFallbackColor() { + XCTAssertEqual(WarningColors.fallbackColor, .systemPink) + XCTAssertEqual(ErrorColors.fallbackColor, .systemPink) + XCTAssertEqual(PrimaryColors.fallbackColor, .systemPurple) + } + + func testLoadColorWithoutNamespace() { + WarningColors.allCases.forEach { + XCTAssertNotNil($0.loadColor()) + } + } + + func testLoadColorWithNamespace() { + ErrorColors.allCases.forEach { + XCTAssertNotNil($0.loadColor()) + } + } + + func testMissingColor() { + PrimaryColors.allCases.forEach { + XCTAssertNil($0.loadColor()) + XCTAssertEqual($0.color, PrimaryColors.fallbackColor) + } + } +} + +private extension ColorableTests { + enum ErrorColors: String, CaseIterable, Colorable { + case error50 + case error100 + + static var bundle: Bundle { .module } + static var namespace: String? { "Error" } + } + + enum WarningColors: String, CaseIterable, Colorable { + case warning50 + case warning100 + + static var bundle: Bundle { .module } + } + + enum PrimaryColors: String, CaseIterable, Colorable { + case primary50 + case primary100 + + static var fallbackColor: UIColor { .systemPurple } + } +}