diff --git a/Sources/Extensions/Foundation/String.swift b/Sources/Extensions/Foundation/String.swift index 58c0f90f..0935b55f 100644 --- a/Sources/Extensions/Foundation/String.swift +++ b/Sources/Extensions/Foundation/String.swift @@ -14,6 +14,10 @@ public extension String { func substring(with nsRange: NSRange) -> String { return nsString.substring(with: nsRange) as String } + + func prepending(_ other: String) -> String { + return other + self + } } public extension String { diff --git a/Sources/Extensions/UIKit/UIColor.swift b/Sources/Extensions/UIKit/UIColor.swift index 9f908632..7c9659c9 100644 --- a/Sources/Extensions/UIKit/UIColor.swift +++ b/Sources/Extensions/UIKit/UIColor.swift @@ -1,69 +1,63 @@ import UIKit public extension UIColor { - private static let divisor = CGFloat(255) - - private typealias Components = (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) - - convenience init(hex: String) { - let hex = hex.trimmingCharacters(in: .whitespacesAndNewlines) - .replacingOccurrences(of: "#", with: "") + convenience init(hexValue: String) throws { + let hex = hexValue + .filter { $0.isLetter || $0.isNumber } + .prepending("ff") + .suffix(8) + .asString + + guard hex.count == 8 else { + throw ColorError.invalidHexSize(hexValue) + } - var hexValue: UInt64 = 0 + var hexInt: UInt64 = 0 - guard Scanner(string: hex).scanHexInt64(&hexValue) else { - fatalError("😱 Cannot convert string into `UInt64`") + guard Scanner(string: hex).scanHexInt64(&hexInt) else { + throw ColorError.invalidHexValue(hexValue) } - let components: Components = { - switch hex.count { - case 6: return UIColor.components(fromHex6: hexValue) - case 8: return UIColor.components(fromHex8: hexValue) - default: fatalError("😱 hex size not supported 😇") - } - }() + let components = UIColor.components(hex: hexInt) self.init(red: components.red, green: components.green, blue: components.blue, alpha: components.alpha) } - var hexString: String { - - var components = UIColor.components(fromHex6: 0) - getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha) - - let r = Int(components.red * UIColor.divisor) - let g = Int(components.green * UIColor.divisor) - let b = Int(components.blue * UIColor.divisor) - let rgb: Int = r << 16 | g << 8 | b << 0 + convenience init(hex: String) { + do { + try self.init(hexValue: hex) + } catch { + fatalError(error.localizedDescription) + } + } - return String(format:"#%06x", rgb) + var hexString: String { + String(format:"#%06x", (asHexIntWithAlpha & 0x00FFFFFF)) } var hexStringWithAlpha: String { - - var components = UIColor.components(fromHex8: 0) - getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha) - - let a = Int(components.alpha * UIColor.divisor) - let r = Int(components.red * UIColor.divisor) - let g = Int(components.green * UIColor.divisor) - let b = Int(components.blue * UIColor.divisor) - let argb: Int = a << 24 | r << 16 | g << 8 | b << 0 - - return String(format:"#%08x", argb) + String(format:"#%08x", asHexIntWithAlpha) } +} - // MARK: - Private Methods +private extension UIColor { + typealias Components = (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) - private static func components(fromHex6 hex: UInt64) -> Components { - let red = CGFloat((hex & 0xFF0000) >> 16) / UIColor.divisor - let green = CGFloat((hex & 0x00FF00) >> 8) / UIColor.divisor - let blue = CGFloat(hex & 0x0000FF) / UIColor.divisor + enum ColorError: LocalizedError { + case invalidHexValue(String) + case invalidHexSize(String) - return (red, green, blue, 1.0) + var localizedDescription: String { + switch self { + case let .invalidHexValue(hex): "😱 Cannot convert `#\(hex)` into `UInt64`" + case let .invalidHexSize(hex): "😱 Hex size of `#\(hex)` not supported 😇" + } + } } - private static func components(fromHex8 hex: UInt64) -> Components { + static let divisor = CGFloat(255) + + static func components(hex: UInt64) -> Components { let alpha = CGFloat((hex & 0xFF000000) >> 24) / UIColor.divisor let red = CGFloat((hex & 0x00FF0000) >> 16) / UIColor.divisor @@ -72,4 +66,24 @@ public extension UIColor { return (red, green, blue, alpha) } + + var asHexIntWithAlpha: Int { + + var components = UIColor.components(hex: 0) + getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha) + + let a = Int(components.alpha * UIColor.divisor) + let r = Int(components.red * UIColor.divisor) + let g = Int(components.green * UIColor.divisor) + let b = Int(components.blue * UIColor.divisor) + let argb: Int = a << 24 | r << 16 | g << 8 | b << 0 + + return argb + } +} + +private extension String.SubSequence { + var asString: String { + String(self) + } } diff --git a/Tests/AlicerceTests/Extensions/UIKit/UIColorTestCase.swift b/Tests/AlicerceTests/Extensions/UIKit/UIColorTestCase.swift index ad5c008a..793dfe76 100644 --- a/Tests/AlicerceTests/Extensions/UIKit/UIColorTestCase.swift +++ b/Tests/AlicerceTests/Extensions/UIKit/UIColorTestCase.swift @@ -92,4 +92,25 @@ final class UIColorTestCase: XCTestCase { XCTAssertEqual(ciTransparentColor.blue, 1.0) XCTAssertEqual(ciTransparentColor.alpha, 0.0) } + + func testGibberishInput_WithValidColorHex_ShouldSucceed() throws { + let gibberish = "@ff&ff*ff%f#f" + + let color = try UIColor(hexValue: gibberish) + + let ciColor = CIColor(color: color) + + XCTAssertEqual(ciColor.red, 1.0) + XCTAssertEqual(ciColor.green, 1.0) + XCTAssertEqual(ciColor.blue, 1.0) + XCTAssertEqual(ciColor.alpha, 1.0) + } + + func testGibberishInput_WithInvalidColorHex_ShouldFail() { + let string = "random string" + + let color = try? UIColor(hexValue: string) + + XCTAssertNil(color) + } } diff --git a/Tests/HostAppRequiringTests/Extensions/StringTestCase.swift b/Tests/HostAppRequiringTests/Extensions/StringTestCase.swift index e17fd4a0..cb6399ed 100644 --- a/Tests/HostAppRequiringTests/Extensions/StringTestCase.swift +++ b/Tests/HostAppRequiringTests/Extensions/StringTestCase.swift @@ -60,4 +60,11 @@ class StringTestCase_Localizable: XCTestCase { XCTAssertNotEqual(localizedHelperTestWithArguments, resultString) } + + func testPrepending_ShouldSucceed() { + let world = "world!" + let hello = "Hello, " + + XCTAssertEqual(world.prepending(hello), "Hello, world!") + } }