From 3f5d251b43d52f9f123319510b89ca78e3cb27af Mon Sep 17 00:00:00 2001 From: Daniel Avram Date: Tue, 17 Jun 2025 17:03:42 +0300 Subject: [PATCH 1/7] add fail-able UIColor init --- Sources/Extensions/UIKit/UIColor.swift | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/Sources/Extensions/UIKit/UIColor.swift b/Sources/Extensions/UIKit/UIColor.swift index 9f90863..df824bf 100644 --- a/Sources/Extensions/UIKit/UIColor.swift +++ b/Sources/Extensions/UIKit/UIColor.swift @@ -1,33 +1,45 @@ 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) { + private enum ColorError: String, LocalizedError { + case invalidHexValue = "😱 Cannot convert string into `UInt64`" + case invalidHexSize = "😱 hex size not supported 😇" + } + + private static let divisor = CGFloat(255) + + convenience init(hexValue hex: String) throws { let hex = hex.trimmingCharacters(in: .whitespacesAndNewlines) .replacingOccurrences(of: "#", with: "") var hexValue: UInt64 = 0 guard Scanner(string: hex).scanHexInt64(&hexValue) else { - fatalError("😱 Cannot convert string into `UInt64`") + throw ColorError.invalidHexValue } - let components: Components = { + let components: Components = try { switch hex.count { case 6: return UIColor.components(fromHex6: hexValue) case 8: return UIColor.components(fromHex8: hexValue) - default: fatalError("😱 hex size not supported 😇") + default: throw ColorError.invalidHexSize } }() self.init(red: components.red, green: components.green, blue: components.blue, alpha: components.alpha) } - var hexString: String { + convenience init(hex: String) { + do { + try self.init(hexValue: hex) + } catch { + fatalError(error.localizedDescription) + } + } + var hexString: String { var components = UIColor.components(fromHex6: 0) getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha) @@ -40,7 +52,6 @@ public extension UIColor { } var hexStringWithAlpha: String { - var components = UIColor.components(fromHex8: 0) getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha) @@ -64,7 +75,6 @@ public extension UIColor { } private static func components(fromHex8 hex: UInt64) -> Components { - let alpha = CGFloat((hex & 0xFF000000) >> 24) / UIColor.divisor let red = CGFloat((hex & 0x00FF0000) >> 16) / UIColor.divisor let green = CGFloat((hex & 0x0000FF00) >> 8) / UIColor.divisor From b1eab85eb25fa48fa9c3905293edd231bd2eb749 Mon Sep 17 00:00:00 2001 From: Daniel Avram Date: Tue, 17 Jun 2025 17:03:54 +0300 Subject: [PATCH 2/7] fix compilation --- Sources/Persistence/DiskMemoryPersistenceStack.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Persistence/DiskMemoryPersistenceStack.swift b/Sources/Persistence/DiskMemoryPersistenceStack.swift index 40883a7..102a3ec 100644 --- a/Sources/Persistence/DiskMemoryPersistenceStack.swift +++ b/Sources/Persistence/DiskMemoryPersistenceStack.swift @@ -501,7 +501,7 @@ extension Persistence.DiskMemoryPersistenceStack: NSCacheDelegate { } } -private final class DiskMemoryBlockOperation: BlockOperation { +private final class DiskMemoryBlockOperation: BlockOperation, @unchecked Sendable { required init(qos: QualityOfService = .default, block: @escaping () -> Swift.Void) { super.init() From 73f4cfd1ce1c3919698cafadbd3cc10c8a56e3c6 Mon Sep 17 00:00:00 2001 From: Daniel Avram Date: Tue, 17 Jun 2025 17:05:19 +0300 Subject: [PATCH 3/7] fix localizedDescription --- Sources/Extensions/UIKit/UIColor.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Extensions/UIKit/UIColor.swift b/Sources/Extensions/UIKit/UIColor.swift index df824bf..d6b242e 100644 --- a/Sources/Extensions/UIKit/UIColor.swift +++ b/Sources/Extensions/UIKit/UIColor.swift @@ -6,6 +6,8 @@ public extension UIColor { private enum ColorError: String, LocalizedError { case invalidHexValue = "😱 Cannot convert string into `UInt64`" case invalidHexSize = "😱 hex size not supported 😇" + + var localizedDescription: String { rawValue } } private static let divisor = CGFloat(255) From 5d61c9acabc16e660f766c460429eddee7116048 Mon Sep 17 00:00:00 2001 From: Daniel Avram Date: Mon, 28 Jul 2025 19:56:09 +0300 Subject: [PATCH 4/7] Revert "fix compilation" This reverts commit b1eab85eb25fa48fa9c3905293edd231bd2eb749. --- Sources/Persistence/DiskMemoryPersistenceStack.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Persistence/DiskMemoryPersistenceStack.swift b/Sources/Persistence/DiskMemoryPersistenceStack.swift index 102a3ec..40883a7 100644 --- a/Sources/Persistence/DiskMemoryPersistenceStack.swift +++ b/Sources/Persistence/DiskMemoryPersistenceStack.swift @@ -501,7 +501,7 @@ extension Persistence.DiskMemoryPersistenceStack: NSCacheDelegate { } } -private final class DiskMemoryBlockOperation: BlockOperation, @unchecked Sendable { +private final class DiskMemoryBlockOperation: BlockOperation { required init(qos: QualityOfService = .default, block: @escaping () -> Swift.Void) { super.init() From 3994cba7750a982d1a977aba53ed2b11623c2e51 Mon Sep 17 00:00:00 2001 From: Daniel Avram Date: Mon, 28 Jul 2025 19:56:38 +0300 Subject: [PATCH 5/7] mr comments --- Sources/Extensions/UIKit/UIColor.swift | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Sources/Extensions/UIKit/UIColor.swift b/Sources/Extensions/UIKit/UIColor.swift index d6b242e..d791397 100644 --- a/Sources/Extensions/UIKit/UIColor.swift +++ b/Sources/Extensions/UIKit/UIColor.swift @@ -3,11 +3,16 @@ import UIKit public extension UIColor { private typealias Components = (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) - private enum ColorError: String, LocalizedError { - case invalidHexValue = "😱 Cannot convert string into `UInt64`" - case invalidHexSize = "😱 hex size not supported 😇" - - var localizedDescription: String { rawValue } + private enum ColorError: LocalizedError { + case invalidHexValue(String) + case invalidHexSize(String) + + 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 let divisor = CGFloat(255) @@ -19,14 +24,14 @@ public extension UIColor { var hexValue: UInt64 = 0 guard Scanner(string: hex).scanHexInt64(&hexValue) else { - throw ColorError.invalidHexValue + throw ColorError.invalidHexValue(hex) } let components: Components = try { switch hex.count { case 6: return UIColor.components(fromHex6: hexValue) case 8: return UIColor.components(fromHex8: hexValue) - default: throw ColorError.invalidHexSize + default: throw ColorError.invalidHexSize(hex) } }() @@ -42,6 +47,7 @@ public extension UIColor { } var hexString: String { + var components = UIColor.components(fromHex6: 0) getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha) @@ -54,6 +60,7 @@ public extension UIColor { } var hexStringWithAlpha: String { + var components = UIColor.components(fromHex8: 0) getRed(&components.red, green: &components.green, blue: &components.blue, alpha: &components.alpha) @@ -69,6 +76,7 @@ public extension UIColor { // MARK: - Private Methods 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 @@ -77,6 +85,7 @@ public extension UIColor { } private static func components(fromHex8 hex: UInt64) -> Components { + let alpha = CGFloat((hex & 0xFF000000) >> 24) / UIColor.divisor let red = CGFloat((hex & 0x00FF0000) >> 16) / UIColor.divisor let green = CGFloat((hex & 0x0000FF00) >> 8) / UIColor.divisor From 0c920647d92b1652064f9424118a7e85eafa4f0d Mon Sep 17 00:00:00 2001 From: Daniel Avram Date: Mon, 28 Jul 2025 21:26:58 +0300 Subject: [PATCH 6/7] cleanup --- Sources/Extensions/Foundation/String.swift | 4 + Sources/Extensions/UIKit/UIColor.swift | 107 ++++++++++----------- 2 files changed, 54 insertions(+), 57 deletions(-) diff --git a/Sources/Extensions/Foundation/String.swift b/Sources/Extensions/Foundation/String.swift index 58c0f90..5e7237f 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 prepeding(_ other: String) -> String { + return other + self + } } public extension String { diff --git a/Sources/Extensions/UIKit/UIColor.swift b/Sources/Extensions/UIKit/UIColor.swift index d791397..49bfeb3 100644 --- a/Sources/Extensions/UIKit/UIColor.swift +++ b/Sources/Extensions/UIKit/UIColor.swift @@ -1,39 +1,24 @@ import UIKit public extension UIColor { - private typealias Components = (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) - - private enum ColorError: LocalizedError { - case invalidHexValue(String) - case invalidHexSize(String) - - var localizedDescription: String { - switch self { - case let .invalidHexValue(hex): "😱 Cannot convert `#\(hex)` into `UInt64`" - case let .invalidHexSize(hex): "😱 Hex size of `#\(hex)` not supported 😇" - } + convenience init(hexValue: String) throws { + let hex = hexValue + .filter { $0.isLetter || $0.isNumber } + .prepeding("ff") + .suffix(8) + .asString + + guard hex.count == 8 else { + throw ColorError.invalidHexSize(hexValue) } - } - - private static let divisor = CGFloat(255) - convenience init(hexValue hex: String) throws { - let hex = hex.trimmingCharacters(in: .whitespacesAndNewlines) - .replacingOccurrences(of: "#", with: "") + var hexInt: UInt64 = 0 - var hexValue: UInt64 = 0 - - guard Scanner(string: hex).scanHexInt64(&hexValue) else { - throw ColorError.invalidHexValue(hex) + guard Scanner(string: hex).scanHexInt64(&hexInt) else { + throw ColorError.invalidHexValue(hexValue) } - let components: Components = try { - switch hex.count { - case 6: return UIColor.components(fromHex6: hexValue) - case 8: return UIColor.components(fromHex8: hexValue) - default: throw ColorError.invalidHexSize(hex) - } - }() + let components = UIColor.components(hex: hexInt) self.init(red: components.red, green: components.green, blue: components.blue, alpha: components.alpha) } @@ -47,44 +32,32 @@ public extension UIColor { } 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 - - return String(format:"#%06x", rgb) + 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 static func components(fromHex6 hex: UInt64) -> Components { +private extension UIColor { + typealias Components = (red: CGFloat, green: CGFloat, blue: CGFloat, alpha: CGFloat) - 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 @@ -93,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) + } } From aafd2b887cc3878e24330bb521c47d691eb65a4a Mon Sep 17 00:00:00 2001 From: Daniel Avram Date: Thu, 31 Jul 2025 11:24:50 +0300 Subject: [PATCH 7/7] mr comments + add some unit tests --- Sources/Extensions/Foundation/String.swift | 2 +- Sources/Extensions/UIKit/UIColor.swift | 2 +- .../Extensions/UIKit/UIColorTestCase.swift | 21 +++++++++++++++++++ .../Extensions/StringTestCase.swift | 7 +++++++ 4 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Sources/Extensions/Foundation/String.swift b/Sources/Extensions/Foundation/String.swift index 5e7237f..0935b55 100644 --- a/Sources/Extensions/Foundation/String.swift +++ b/Sources/Extensions/Foundation/String.swift @@ -15,7 +15,7 @@ public extension String { return nsString.substring(with: nsRange) as String } - func prepeding(_ other: String) -> String { + func prepending(_ other: String) -> String { return other + self } } diff --git a/Sources/Extensions/UIKit/UIColor.swift b/Sources/Extensions/UIKit/UIColor.swift index 49bfeb3..7c9659c 100644 --- a/Sources/Extensions/UIKit/UIColor.swift +++ b/Sources/Extensions/UIKit/UIColor.swift @@ -4,7 +4,7 @@ public extension UIColor { convenience init(hexValue: String) throws { let hex = hexValue .filter { $0.isLetter || $0.isNumber } - .prepeding("ff") + .prepending("ff") .suffix(8) .asString diff --git a/Tests/AlicerceTests/Extensions/UIKit/UIColorTestCase.swift b/Tests/AlicerceTests/Extensions/UIKit/UIColorTestCase.swift index ad5c008..793dfe7 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 e17fd4a..cb6399e 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!") + } }