From 3856c0e3bf8e7d9b2aad7a8fe67752cd177ecfcf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 22:45:06 +0000 Subject: [PATCH 1/3] Fix bugs and modernize for current Swift/Apple platform versions Bugs fixed: - 3-char hex (#FA4) now correctly expands to #FFAA44 per CSS convention - Int hex initializers now safely return nil on overflow instead of crashing - Remove always-true UInt32 range check (dead code) in 8-char branch Code quality: - Convert XColor from class to struct (value semantics, Swift 6 Sendable) - Make alpha immutable (let instead of var) - Remove redundant parentheses around conditions - Simplify validatedColor by merging sanitize+validate into one function Tests: - Update 3-char short hex test expectations to match corrected expansion - Fix misleading assertion message typos in UIColorTests Infrastructure: - Add Package.swift for Swift Package Manager support (iOS 16+, watchOS 9+, tvOS 16+, macOS 13+, visionOS 1+) - Update podspec: deployment targets, swift_version 5.9, version 0.4, add visionOS - Replace .travis.yml with GitHub Actions (macos-15/Xcode 16, arm64, SPM job) https://claude.ai/code/session_01EjH8goiJM4ynjw39WFr4QU --- .github/workflows/ci.yml | 55 ++++++++++++++++++++ Package.swift | 28 +++++++++++ Source/XColor.swift | 91 +++++++++++++++------------------- XColor.podspec | 13 ++--- XColorTests/UIColorTests.swift | 12 ++--- XColorTests/XColorTests.swift | 12 ++--- 6 files changed, 141 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 Package.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8c922eb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +jobs: + test-ios: + name: Test iOS + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Test + run: | + xcodebuild \ + -project XColor.xcodeproj \ + -scheme "XColor iOS" \ + -sdk iphonesimulator \ + -destination 'platform=iOS Simulator,name=iPhone 16' \ + clean test + + test-tvos: + name: Test tvOS + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Test + run: | + xcodebuild \ + -project XColor.xcodeproj \ + -scheme "XColor tvOS" \ + -destination 'platform=tvOS Simulator,name=Apple TV 4K (3rd generation)' \ + clean test + + test-macos: + name: Test macOS + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Test + run: | + xcodebuild \ + -project XColor.xcodeproj \ + -scheme "XColor macOS" \ + -destination 'platform=macOS,arch=arm64' \ + clean test + + test-spm: + name: Test SPM + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + - name: Build and Test + run: swift test diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..7a0809a --- /dev/null +++ b/Package.swift @@ -0,0 +1,28 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "XColor", + platforms: [ + .iOS(.v16), + .watchOS(.v9), + .tvOS(.v16), + .macOS(.v13), + .visionOS(.v1) + ], + products: [ + .library(name: "XColor", targets: ["XColor"]) + ], + targets: [ + .target( + name: "XColor", + path: "Source", + exclude: ["Supporting Files"] + ), + .testTarget( + name: "XColorTests", + dependencies: ["XColor"], + path: "XColorTests" + ) + ] +) diff --git a/Source/XColor.swift b/Source/XColor.swift index 5633bbd..f9a41a5 100644 --- a/Source/XColor.swift +++ b/Source/XColor.swift @@ -11,7 +11,7 @@ struct Components { let green: UInt8 let blue: UInt8 let alpha: Double - + init(red: UInt8, green: UInt8, blue: UInt8, alpha: Double = 1) { self.red = red self.green = green @@ -20,23 +20,15 @@ struct Components { } } -class XColor { - +struct XColor { + private let color: UInt32 - private var alpha: Double = 1 - + private let alpha: Double + init?(hexColor: String) { guard let color = XColor.validatedColor(hexString: hexColor) else { return nil } self.color = color - } - - init?(hexColor: Int) { - self.color = UInt32(hexColor) - } - - init?(hexColor: Int, alpha: Double) { - self.color = UInt32(hexColor) - self.alpha = alpha + self.alpha = 1.0 } init?(hexColor: String, alpha: Double) { @@ -44,57 +36,52 @@ class XColor { self.color = color self.alpha = alpha } - + + init?(hexColor: Int) { + guard let color = UInt32(exactly: hexColor) else { return nil } + self.color = color + self.alpha = 1.0 + } + + init?(hexColor: Int, alpha: Double) { + guard let color = UInt32(exactly: hexColor) else { return nil } + self.color = color + self.alpha = alpha + } + var components: Components? { - if (0x000 ... 0xFFF ~= color) { - let red = UInt8((color & 0xF00) >> 8) - let green = UInt8((color & 0x0F0) >> 4) - let blue = UInt8(color & 0x00F) - return Components(red: red, green: green, blue: blue, alpha: alpha) - } else if (0x000000 ... 0xFFFFFF ~= color) { + if 0x000 ... 0xFFF ~= color { + // 3-char shorthand: each nibble is doubled (e.g. #FA4 → #FFAA44) + let rn = UInt8((color & 0xF00) >> 8) + let gn = UInt8((color & 0x0F0) >> 4) + let bn = UInt8(color & 0x00F) + return Components( + red: (rn << 4) | rn, + green: (gn << 4) | gn, + blue: (bn << 4) | bn, + alpha: alpha + ) + } else if 0x000000 ... 0xFFFFFF ~= color { let red = UInt8((color & 0xFF0000) >> 16) let green = UInt8((color & 0x00FF00) >> 8) let blue = UInt8(color & 0x0000FF) return Components(red: red, green: green, blue: blue, alpha: alpha) - } else if (0x00000000 ... 0xFFFFFFFF ~= color) { + } else { + // 8-char RGBA: last byte is implicit alpha let red = UInt8((color & 0xFF000000) >> 24) let green = UInt8((color & 0x00FF0000) >> 16) let blue = UInt8((color & 0x0000FF00) >> 8) - let alpha = Double(UInt8(color & 0x000000FF)) / 255 - return Components(red: red, green: green, blue: blue, alpha: alpha) + let implicitAlpha = Double(color & 0x000000FF) / 255 + return Components(red: red, green: green, blue: blue, alpha: implicitAlpha) } - - return nil - } - - private static func validatedColor(hexString: String) -> UInt32? { - guard let str = XColor.sanitizedHexString(hexString: hexString) else { - return nil - } - - guard let color = UInt32(str, radix: 16) else { - return nil - } - - return color } - private static func sanitizedHexString(hexString: String) -> String? { - + private static func validatedColor(hexString: String) -> UInt32? { var str = hexString - - if (str.hasPrefix("#")) { + if str.hasPrefix("#") { str.remove(at: str.startIndex) } - - if (!valid(str)) { - return nil - } - - return str - } - - private static func valid(_ s: String) -> Bool { - return s.count == 3 || s.count == 6 || s.count == 8 + guard str.count == 3 || str.count == 6 || str.count == 8 else { return nil } + return UInt32(str, radix: 16) } } diff --git a/XColor.podspec b/XColor.podspec index 870f314..0decca7 100644 --- a/XColor.podspec +++ b/XColor.podspec @@ -1,18 +1,19 @@ Pod::Spec.new do |spec| spec.name = "XColor" - spec.version = "0.3" + spec.version = "0.4" spec.summary = "XColor is a color handling extension for UIColor written in Swift." spec.homepage = "https://github.com/jaumevn/XColor" spec.license = { :type => "MIT", :file => "LICENSE" } spec.source = { :git => "https://github.com/jaumevn/XColor.git", :tag => "#{spec.version}" } spec.author = { "Jaume Vinas Navas" => "jaumevn@icloud.com" } - spec.ios.deployment_target = '8.0' - spec.watchos.deployment_target = '2.0' - spec.tvos.deployment_target = '9.0' - spec.macos.deployment_target = '10.9' + spec.ios.deployment_target = '16.0' + spec.watchos.deployment_target = '9.0' + spec.tvos.deployment_target = '16.0' + spec.macos.deployment_target = '13.0' + spec.visionos.deployment_target = '1.0' - spec.swift_version = '5.0' + spec.swift_version = '5.9' spec.source_files = 'Source/*.swift' end diff --git a/XColorTests/UIColorTests.swift b/XColorTests/UIColorTests.swift index f8a06ff..56328ba 100644 --- a/XColorTests/UIColorTests.swift +++ b/XColorTests/UIColorTests.swift @@ -34,7 +34,7 @@ class UIColorTests: XCTestCase { XCTAssertEqual(components?.red, 0xAC / 255 , "Red component should be 0xAC") XCTAssertEqual(components?.green, 0x34 / 255 , "Green component should be 0x34") XCTAssertEqual(components?.blue, 0xCF / 255 , "Blue component should be 0xCF") - XCTAssertEqual(components?.alpha, 0xFF / 255 , "Alpha component should be 0xAC") + XCTAssertEqual(components?.alpha, 0xFF / 255 , "Alpha component should be 0xFF") } func testColorWithStringAndImplicitAlphaChannel() { @@ -69,9 +69,9 @@ class UIColorTests: XCTestCase { XCTAssertEqual(components?.red, 0xAC / 255 , "Red component should be 0xAC") XCTAssertEqual(components?.green, 0x34 / 255 , "Green component should be 0x34") XCTAssertEqual(components?.blue, 0xCF / 255 , "Blue component should be 0xCF") - XCTAssertEqual(components?.alpha, 0.3 , "Alpha component should be 0x88") + XCTAssertEqual(components?.alpha, 0.3 , "Alpha component should be 0.3") } - + func testColorWithNumber() { // GIVEN let hex = 0xAC34CF @@ -86,7 +86,7 @@ class UIColorTests: XCTestCase { XCTAssertEqual(components?.red, 0xAC / 255 , "Red component should be 0xAC") XCTAssertEqual(components?.green, 0x34 / 255 , "Green component should be 0x34") XCTAssertEqual(components?.blue, 0xCF / 255 , "Blue component should be 0xCF") - XCTAssertEqual(components?.alpha, 0xFF / 255 , "Alpha component should be 0x88") + XCTAssertEqual(components?.alpha, 0xFF / 255 , "Alpha component should be 0xFF") } func testColorWithNumberAndImplicitAlphaChannel() { @@ -121,9 +121,9 @@ class UIColorTests: XCTestCase { XCTAssertEqual(components?.red, 0xAC / 255 , "Red component should be 0xAC") XCTAssertEqual(components?.green, 0x34 / 255 , "Green component should be 0x34") XCTAssertEqual(components?.blue, 0xCF / 255 , "Blue component should be 0xCF") - XCTAssertEqual(components?.alpha, 0.3 , "Alpha component should be 0x88") + XCTAssertEqual(components?.alpha, 0.3 , "Alpha component should be 0.3") } - + func testColorWithInvalidString() { // GIVEN let hex = ".This is an invalid string!" diff --git a/XColorTests/XColorTests.swift b/XColorTests/XColorTests.swift index 5804a6b..7356f92 100644 --- a/XColorTests/XColorTests.swift +++ b/XColorTests/XColorTests.swift @@ -38,9 +38,9 @@ class XColorTests: XCTestCase { let color = XColor(hexColor: hexString) // Expect let components = color?.components - XCTAssertEqual(components?.red, 0xF) - XCTAssertEqual(components?.green, 0xA) - XCTAssertEqual(components?.blue, 0x4) + XCTAssertEqual(components?.red, 0xFF) + XCTAssertEqual(components?.green, 0xAA) + XCTAssertEqual(components?.blue, 0x44) XCTAssertEqual(components?.alpha, 1.0) } @@ -118,9 +118,9 @@ class XColorTests: XCTestCase { let color = XColor(hexColor: hexNumber) // Expect let components = color?.components - XCTAssertEqual(components?.red, 0xF) - XCTAssertEqual(components?.green, 0xA) - XCTAssertEqual(components?.blue, 0x4) + XCTAssertEqual(components?.red, 0xFF) + XCTAssertEqual(components?.green, 0xAA) + XCTAssertEqual(components?.blue, 0x44) XCTAssertEqual(components?.alpha, 1.0) } From a170e431d0ff08902486a17ec5696923135b49cf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 24 Mar 2026 23:12:06 +0000 Subject: [PATCH 2/3] Update Xcode project deployment targets to match podspec/SPM MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Xcode 16.4 dropped libarclite support below iOS 12, causing CI to fail with 'SDK does not contain libarclite'. Bump all framework targets: - iOS: 8.0 → 16.0 - tvOS: 9.0 → 16.0 - watchOS: 2.0 → 9.0 - macOS: 10.9 / 10.14 → 13.0 Test targets (iOS 12.1, tvOS 12.4) are already within range and unchanged. https://claude.ai/code/session_01EjH8goiJM4ynjw39WFr4QU --- XColor.xcodeproj/project.pbxproj | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/XColor.xcodeproj/project.pbxproj b/XColor.xcodeproj/project.pbxproj index 58b8f41..c69b4ff 100644 --- a/XColor.xcodeproj/project.pbxproj +++ b/XColor.xcodeproj/project.pbxproj @@ -604,7 +604,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = "com.jaumevn.XColor-macOS"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = macosx; @@ -631,7 +631,7 @@ "@executable_path/../Frameworks", "@loader_path/Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = "com.jaumevn.XColor-macOS"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = macosx; @@ -653,7 +653,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = "com.jaumevn.XColor-macOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -674,7 +674,7 @@ "@executable_path/../Frameworks", "@loader_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = "com.jaumevn.XColor-macOSTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; @@ -818,7 +818,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Source/Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -848,7 +848,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = "$(SRCROOT)/Source/Supporting Files/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -926,7 +926,7 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 2.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Debug; }; @@ -955,7 +955,7 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 2.0; + WATCHOS_DEPLOYMENT_TARGET = 9.0; }; name = Release; }; @@ -983,7 +983,7 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Debug; }; @@ -1011,7 +1011,7 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 9.0; + TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Release; }; From 1588e3df0e1db8b4480b9114b61136918503e9d4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 25 Mar 2026 09:04:05 +0000 Subject: [PATCH 3/3] Fix CI failures: test target deployment targets and SPM imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tvOS test target (XColor tvOSTests): - TVOS_DEPLOYMENT_TARGET 12.4 → 16.0 The test bundle must match the framework deployment target or the linker rejects the combination (exit code 65 build failure). iOS test target (XColorTests): - Add explicit IPHONEOS_DEPLOYMENT_TARGET = 16.0 to Debug and Release configs. Previously it inherited the project-level value of 12.1, creating a mismatch with the XColor iOS framework (16.0) that caused the test host to fail loading (exit code 70). SPM test imports (XColorTests.swift, UIColorTests.swift): - Add #if SWIFT_PACKAGE / @testable import XColor branch before the existing #if os(iOS) chain. The SPM module is named "XColor", not "XColor_iOS" / "XColor_macOS", so swift test on macOS was hitting @testable import XColor_macOS which does not exist (exit code 1). https://claude.ai/code/session_01EjH8goiJM4ynjw39WFr4QU --- XColor.xcodeproj/project.pbxproj | 6 ++++-- XColorTests/UIColorTests.swift | 5 ++++- XColorTests/XColorTests.swift | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/XColor.xcodeproj/project.pbxproj b/XColor.xcodeproj/project.pbxproj index c69b4ff..56f7384 100644 --- a/XColor.xcodeproj/project.pbxproj +++ b/XColor.xcodeproj/project.pbxproj @@ -870,6 +870,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = XColorTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -889,6 +890,7 @@ CODE_SIGN_STYLE = Automatic; DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = XColorTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1032,7 +1034,7 @@ SDKROOT = appletvos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.4; + TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Debug; }; @@ -1053,7 +1055,7 @@ SDKROOT = appletvos; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; - TVOS_DEPLOYMENT_TARGET = 12.4; + TVOS_DEPLOYMENT_TARGET = 16.0; }; name = Release; }; diff --git a/XColorTests/UIColorTests.swift b/XColorTests/UIColorTests.swift index 56328ba..6c8d0ad 100644 --- a/XColorTests/UIColorTests.swift +++ b/XColorTests/UIColorTests.swift @@ -8,7 +8,10 @@ import XCTest -#if os(iOS) +#if SWIFT_PACKAGE +import Cocoa +@testable import XColor +#elseif os(iOS) import UIKit @testable import XColor_iOS #elseif os(tvOS) diff --git a/XColorTests/XColorTests.swift b/XColorTests/XColorTests.swift index 7356f92..0b8ec0a 100644 --- a/XColorTests/XColorTests.swift +++ b/XColorTests/XColorTests.swift @@ -9,7 +9,9 @@ import XCTest import CoreGraphics -#if os(iOS) +#if SWIFT_PACKAGE +@testable import XColor +#elseif os(iOS) @testable import XColor_iOS #elseif os(tvOS) @testable import XColor_tvOS