├── .github ├── data │ ├── iOS.png │ ├── tvOS.png │ ├── banner.png │ ├── iPadOS.png │ ├── visionOS.png │ ├── dark-mode.png │ └── light-mode.png ├── workflows │ ├── pr-title-checker.yml │ └── static-site.yml └── pr-title-checker-config.json ├── .gitignore ├── .spi.yml ├── Sources └── ContrastKit │ ├── Public │ ├── AgnosticColor.swift │ ├── Color+Convenience.swift │ ├── ContrastLevel.swift │ ├── Color+Ext.swift │ └── ColorLevel.swift │ ├── Internal │ ├── UIColor+Ext.swift │ └── Color+Helpers.swift │ └── UITesting │ ├── Example+SwiftUI.swift │ └── PreviewTesting.swift ├── Package.swift ├── LICENCE ├── Tests └── ContrastKitTests │ └── ContrastKitTests.swift └── README.md /.github/data/iOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markbattistella/ContrastKit/HEAD/.github/data/iOS.png -------------------------------------------------------------------------------- /.github/data/tvOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markbattistella/ContrastKit/HEAD/.github/data/tvOS.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | /.swiftpm 7 | .netrc 8 | -------------------------------------------------------------------------------- /.github/data/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markbattistella/ContrastKit/HEAD/.github/data/banner.png -------------------------------------------------------------------------------- /.github/data/iPadOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markbattistella/ContrastKit/HEAD/.github/data/iPadOS.png -------------------------------------------------------------------------------- /.github/data/visionOS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markbattistella/ContrastKit/HEAD/.github/data/visionOS.png -------------------------------------------------------------------------------- /.github/data/dark-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markbattistella/ContrastKit/HEAD/.github/data/dark-mode.png -------------------------------------------------------------------------------- /.github/data/light-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/markbattistella/ContrastKit/HEAD/.github/data/light-mode.png -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [ContrastKit] 5 | platform: ios 6 | -------------------------------------------------------------------------------- /.github/workflows/pr-title-checker.yml: -------------------------------------------------------------------------------- 1 | name: PR title checker 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | 9 | jobs: 10 | 11 | pr-validator: 12 | name: Validate PR title 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: thehanimo/pr-title-checker@v1.4.1 16 | with: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/pr-title-checker-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "LABEL": { 3 | "name": "Bad PR title format", 4 | "color": "EEEEEE" 5 | }, 6 | "CHECKS": { 7 | "prefixes": [], 8 | "regexp": "^(19|20)\\d{2}(\/|-)(0[1-9]|1[1,2])(\/|-)(0[1-9]|[12][0-9]|3[01])", 9 | "regexpFlags": "", 10 | "ignoreLabels": [ 11 | "ignore-title" 12 | ] 13 | }, 14 | "MESSAGES": { 15 | "success": "All OK", 16 | "failure": "Failing CI test", 17 | "notice": "" 18 | } 19 | } -------------------------------------------------------------------------------- /Sources/ContrastKit/Public/AgnosticColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | // Provides a platform-independent typealias for color representation. 8 | // This file ensures that the same code can be used seamlessly across 9 | // UIKit (iOS) and AppKit (macOS). 10 | 11 | #if canImport(UIKit) 12 | 13 | import UIKit.UIColor 14 | 15 | @available(iOS 14.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 16 | public typealias AgnosticColor = UIColor 17 | 18 | #else 19 | 20 | import AppKit.NSColor 21 | 22 | @available(macOS 11.0, *) 23 | public typealias AgnosticColor = NSColor 24 | 25 | #endif 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "ContrastKit", 7 | platforms: [ 8 | .iOS(.v14), 9 | .macOS(.v11), 10 | .macCatalyst(.v14), 11 | .tvOS(.v14), 12 | .watchOS(.v7), 13 | .visionOS(.v1) 14 | ], 15 | products: [ 16 | .library( 17 | name: "ContrastKit", 18 | targets: ["ContrastKit"] 19 | ) 20 | ], 21 | targets: [ 22 | .target( 23 | name: "ContrastKit", 24 | dependencies: [], 25 | path: "Sources" 26 | ), 27 | .testTarget( 28 | name: "ContrastKitTests", 29 | dependencies: ["ContrastKit"], 30 | path: "Tests" 31 | ) 32 | ] 33 | ) 34 | -------------------------------------------------------------------------------- /.github/workflows/static-site.yml: -------------------------------------------------------------------------------- 1 | name: Deploy website 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: "pages" 11 | cancel-in-progress: true 12 | 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | jobs: 19 | 20 | deploy: 21 | name: Deploy the website 22 | runs-on: ubuntu-latest 23 | 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | 32 | - name: Setup Pages 33 | uses: actions/configure-pages@v4 34 | 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v3 37 | with: 38 | path: './Documentation' 39 | 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v4 43 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Mark Battistella 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 | -------------------------------------------------------------------------------- /Tests/ContrastKitTests/ContrastKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | import XCTest 8 | @testable import ContrastKit 9 | 10 | final class ContrastKitTests: XCTestCase { 11 | 12 | /// Tests the luminance calculation for the black color. 13 | func testBlackLuminanceCalculation() { 14 | let black = AgnosticColor.black 15 | XCTAssertEqual(black.luminance(), 0.0, "Luminance of black should be 0.0") 16 | } 17 | 18 | /// Tests the luminance calculation for the white color. 19 | func testWhiteLuminanceCalculation() { 20 | let white = AgnosticColor.white 21 | XCTAssertEqual(white.luminance(), 1.0, "Luminance of white should be 1.0") 22 | } 23 | 24 | /// Tests the contrast ratio calculation between black and white colors. 25 | func testContrastRatio() { 26 | let black = AgnosticColor.black 27 | let white = AgnosticColor.white 28 | let result = black.contrastRatio(with: white) 29 | XCTAssertEqual(result.ratio, 21.0, "Contrast ratio between black and white should be 21.0") 30 | XCTAssertEqual(result.rating, "AAA", "Contrast rating should be AAA") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/ContrastKit/Public/Color+Convenience.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | import SwiftUI 8 | 9 | // MARK: - Concenience APIs 10 | 11 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 12 | extension Color { 13 | 14 | /// Returns a color at level 50 brightness. 15 | public var level50: Color { self.level(.level50) } 16 | 17 | /// Returns a color at level 100 brightness. 18 | public var level100: Color { self.level(.level100) } 19 | 20 | /// Returns a color at level 200 brightness. 21 | public var level200: Color { self.level(.level200) } 22 | 23 | /// Returns a color at level 300 brightness. 24 | public var level300: Color { self.level(.level300) } 25 | 26 | /// Returns a color at level 400 brightness. 27 | public var level400: Color { self.level(.level400) } 28 | 29 | /// Returns a color at level 500 brightness. 30 | public var level500: Color { self.level(.level500) } 31 | 32 | /// Returns a color at level 600 brightness. 33 | public var level600: Color { self.level(.level600) } 34 | 35 | /// Returns a color at level 700 brightness. 36 | public var level700: Color { self.level(.level700) } 37 | 38 | /// Returns a color at level 800 brightness. 39 | public var level800: Color { self.level(.level800) } 40 | 41 | /// Returns a color at level 900 brightness. 42 | public var level900: Color { self.level(.level900) } 43 | 44 | /// Returns a color at level 950 brightness. 45 | public var level950: Color { self.level(.level950) } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/ContrastKit/Public/ContrastLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | import Foundation 8 | 9 | /// Defines the contrast levels used to ensure accessibility in user interfaces. 10 | /// 11 | /// This enum provides named contrast levels corresponding to the WCAG accessibility guidelines: 12 | /// - AA Large: Minimum contrast for large text. 13 | /// - AA: Standard minimum contrast for normal text. 14 | /// - AAA: Enhanced contrast for improved readability. 15 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 16 | public enum ContrastLevel: String { 17 | 18 | /// Contrast level for large text, typically for visual accessibility. 19 | case aaLarge = "AA Large" 20 | 21 | /// Standard contrast level for normal text size, ensuring clear readability. 22 | case aa = "AA" 23 | 24 | /// Highest contrast level, recommended for the best readability in all contexts. 25 | case aaa = "AAA" 26 | 27 | /// Provides the minimum and maximum contrast ratios for each contrast level. 28 | /// 29 | /// These values define the acceptable range of contrast ratios to meet or exceed specific 30 | /// accessibility requirements: 31 | /// - For `aaLarge`, the range is suitable for large text. 32 | /// - For `aa`, the range is set for normal text. 33 | /// - For `aaa`, the range encompasses the highest contrast ratio, suitable for all types 34 | /// of text to ensure optimal readability. 35 | var ratio: (min: CGFloat, max: CGFloat) { 36 | switch self { 37 | case .aaLarge: 38 | return (3.0, 4.49) 39 | case .aa: 40 | return (4.5, 6.99) 41 | case .aaa: 42 | return (7.0, CGFloat.infinity) 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/ContrastKit/Public/Color+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | import SwiftUI 8 | 9 | // MARK: - Public APIs 10 | 11 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 12 | extension Color { 13 | 14 | /// Adjusts the color based on the specified level and optionally adjusts for the 15 | /// specified color scheme. 16 | /// 17 | /// - Parameters: 18 | /// - level: The specified `ColorLevel`. 19 | /// - scheme: Optional `ColorScheme` to adjust for light or dark mode. 20 | /// - Returns: A `Color` adjusted for the specified level and color scheme. 21 | public func level(_ level: ColorLevel, scheme: ColorScheme? = nil) -> Color { 22 | let adjustedLevel = scheme == .dark ? level.correspondingDarkModeLevel : level 23 | let components = self.uiColorComponents() 24 | return Color.hsl( 25 | hue: components.hue * 360, 26 | saturation: components.saturation, 27 | lightness: adjustedLevel.lightness 28 | ) 29 | } 30 | 31 | /// Returns the color that achieves the highest possible contrast. 32 | /// 33 | /// This property calculates the maximum contrast color available without any upper limit 34 | /// on the contrast ratio, aiming to enhance readability and accessibility. 35 | public var highestRatedContrastLevel: Color { 36 | calculateBestContrast(for: .aaa, maxRatio: CGFloat.infinity) 37 | } 38 | 39 | /// Direct access to a color that complies with the AAA contrast level requirements. 40 | public var aaaContrastLevel: Color { 41 | calculateBestContrast(for: .aaa) 42 | } 43 | 44 | /// Direct access to a color that complies with the AA contrast level requirements. 45 | public var aaContrastLevel: Color { 46 | calculateBestContrast(for: .aa) 47 | } 48 | 49 | /// Direct access to a color that complies with the AA Large contrast level requirements. 50 | public var aaLargeContrastLevel: Color { 51 | calculateBestContrast(for: .aaLarge) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/ContrastKit/Public/ColorLevel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | import Foundation 8 | 9 | /// Enum defining specific lightness levels for colors, useful for generating color themes or 10 | /// ensuring accessible contrast. 11 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 12 | public enum ColorLevel: CaseIterable { 13 | 14 | case level50, level100, level200, level300, level400, level500, 15 | level600, level700, level800, level900, level950 16 | 17 | /// Provides the lightness value associated with each color level. 18 | var lightness: CGFloat { 19 | switch self { 20 | 21 | // Very light shade, almost white. 22 | case .level50: return 0.95 23 | 24 | // Very light shade. 25 | case .level100: return 0.90 26 | 27 | // Lighter shade, for subtle backgrounds. 28 | case .level200: return 0.80 29 | 30 | // Light shade, good for hover states or secondary buttons. 31 | case .level300: return 0.70 32 | 33 | // Medium light shade. 34 | case .level400: return 0.60 35 | 36 | // Neutral base shade, often used for the primary variant of a color. 37 | case .level500: return 0.50 38 | 39 | // Medium dark shade. 40 | case .level600: return 0.40 41 | 42 | // Darker shade, suitable for text. 43 | case .level700: return 0.30 44 | 45 | // Very dark shade, often used for text or active elements. 46 | case .level800: return 0.20 47 | 48 | // Very dark, closer to black. 49 | case .level900: return 0.10 50 | 51 | // Extremely dark, almost black. 52 | case .level950: return 0.05 53 | } 54 | } 55 | 56 | /// Returns the corresponding level for dark mode. 57 | var correspondingDarkModeLevel: ColorLevel { 58 | switch self { 59 | case .level50: return .level950 60 | case .level100: return .level900 61 | case .level200: return .level800 62 | case .level300: return .level700 63 | case .level400: return .level600 64 | case .level500: return .level500 65 | case .level600: return .level400 66 | case .level700: return .level300 67 | case .level800: return .level200 68 | case .level900: return .level100 69 | case .level950: return .level50 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/ContrastKit/Internal/UIColor+Ext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | #if canImport(UIKit) 8 | import UIKit.UIColor 9 | #else 10 | import AppKit.NSColor 11 | #endif 12 | 13 | // MARK: - Internal API 14 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 15 | extension AgnosticColor { 16 | 17 | /// Computes the perceived luminance of the color using the sRGB formula. 18 | /// This method handles different color spaces including RGB and grayscale. 19 | /// 20 | /// - Returns: A CGFloat representing the luminance of the color. 21 | internal func luminance() -> CGFloat { 22 | guard let components = cgColor.components else { return 0 } 23 | let numberOfComponents = components.count 24 | let r: CGFloat, g: CGFloat, b: CGFloat 25 | 26 | switch numberOfComponents { 27 | case 4: // RGBA 28 | r = components[0] 29 | g = components[1] 30 | b = components[2] 31 | case 2, 1: // Grayscale 32 | r = components[0] 33 | g = components[0] 34 | b = components[0] 35 | default: // Unexpected color space 36 | r = 0 37 | g = 0 38 | b = 0 39 | } 40 | 41 | return calculateLuminance(red: r, green: g, blue: b) 42 | } 43 | 44 | /// Helper function to calculate luminance using the standard sRGB luminance formula. 45 | /// 46 | /// - Parameters: 47 | /// - red: The red component of the color. 48 | /// - green: The green component of the color. 49 | /// - blue: The blue component of the color. 50 | /// - Returns: The calculated luminance as a CGFloat. 51 | private func calculateLuminance(red: CGFloat, green: CGFloat, blue: CGFloat) -> CGFloat { 52 | let rL = adjustedLuminance(component: red) 53 | let gL = adjustedLuminance(component: green) 54 | let bL = adjustedLuminance(component: blue) 55 | 56 | let redCoefficient: CGFloat = 0.2126 57 | let greenCoefficient: CGFloat = 0.7152 58 | let blueCoefficient: CGFloat = 0.0722 59 | 60 | return (redCoefficient * rL) + (greenCoefficient * gL) + (blueCoefficient * bL) 61 | } 62 | 63 | /// Adjusts the component value based on the sRGB luminance formula. 64 | /// 65 | /// - Parameter component: The color component value. 66 | /// - Returns: The adjusted luminance component. 67 | private func adjustedLuminance(component: CGFloat) -> CGFloat { 68 | let kLowRGBLuminanceMultiplier: CGFloat = 12.92 69 | let kHighRGBLuminanceMultiplier: CGFloat = 1.055 70 | let kHighRGBLuminanceAddend: CGFloat = 0.055 71 | let kLuminancePower: CGFloat = 2.4 72 | 73 | return component < 0.03928 ? 74 | component / kLowRGBLuminanceMultiplier : 75 | pow((component + kHighRGBLuminanceAddend) / kHighRGBLuminanceMultiplier, kLuminancePower) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Sources/ContrastKit/UITesting/Example+SwiftUI.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | #if DEBUG && canImport(SwiftUI) 8 | 9 | import SwiftUI 10 | 11 | // MARK: - UI Preview 12 | 13 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 14 | internal struct StandardShadeView: View { 15 | 16 | private let baseColor = Color.pink 17 | 18 | var body: some View { 19 | HStack { 20 | VStack(spacing: 0) { 21 | ForEach(ColorLevel.allCases, id: \.self) { level in 22 | let shadeColor = baseColor.level(level) 23 | let contrastColor = shadeColor.highestRatedContrastLevel 24 | let contrastInfo = shadeColor.contrastInfo(with: contrastColor) 25 | ShadeCell(level, shade: shadeColor, contrast: contrastColor, info: contrastInfo) 26 | } 27 | } 28 | VStack(spacing: 0) { 29 | ForEach(ColorLevel.allCases, id: \.self) { level in 30 | let shadeColor = baseColor.level(level) 31 | let contrastColor = shadeColor.aaLargeContrastLevel 32 | let contrastInfo = shadeColor.contrastInfo(with: contrastColor, for: .aaLarge) 33 | ShadeCell(level, shade: shadeColor, contrast: contrastColor, info: contrastInfo) 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 41 | internal struct EnvironmentShadeView: View { 42 | 43 | @Environment(\.colorScheme) 44 | private var colorScheme 45 | 46 | private let baseColor = Color.pink 47 | 48 | var body: some View { 49 | HStack { 50 | VStack(spacing: 0) { 51 | ForEach(ColorLevel.allCases, id: \.self) { level in 52 | let shadeColor = baseColor.level(level, scheme: colorScheme) 53 | let contrastColor = shadeColor.highestRatedContrastLevel 54 | let contrastInfo = shadeColor.contrastInfo(with: contrastColor) 55 | ShadeCell(level, shade: shadeColor, contrast: contrastColor, info: contrastInfo) 56 | } 57 | } 58 | VStack(spacing: 0) { 59 | ForEach(ColorLevel.allCases, id: \.self) { level in 60 | let shadeColor = baseColor.level(level, scheme: colorScheme) 61 | let contrastColor = shadeColor.aaLargeContrastLevel 62 | let contrastInfo = shadeColor.contrastInfo(with: contrastColor, for: .aaLarge) 63 | ShadeCell(level, shade: shadeColor, contrast: contrastColor, info: contrastInfo) 64 | } 65 | } 66 | VStack { 67 | baseColor.level(.level100, scheme: colorScheme) 68 | baseColor.level(.level900, scheme: colorScheme) 69 | 70 | baseColor.level(.level100, scheme: colorScheme).level900 71 | baseColor.level(.level900, scheme: colorScheme).level100 72 | } 73 | } 74 | } 75 | } 76 | 77 | #Preview("ColorScheme: Fixed") { StandardShadeView() } 78 | #Preview("ColorScheme: Environment") { EnvironmentShadeView() } 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /Sources/ContrastKit/UITesting/PreviewTesting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | #if DEBUG 8 | 9 | import SwiftUI 10 | 11 | /// An enumeration defining accessibility ratings based on contrast ratios. 12 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 13 | internal enum AccessibilityRating: String { 14 | case fail = "Fail" 15 | case aaLarge = "AA Large" 16 | case aa = "AA" 17 | case aaa = "AAA" 18 | } 19 | 20 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 21 | extension Color { 22 | 23 | /// Returns a string describing the contrast information between this color and another. 24 | /// Includes the enforced contrast level. 25 | /// 26 | /// - Parameters: 27 | /// - color: The other color to compare. 28 | /// - level: The contrast level being enforced. 29 | /// - Returns: A string containing the contrast ratio, its corresponding accessibility 30 | /// rating, and the enforced level. 31 | internal func contrastInfo(with color: Color, for level: ContrastLevel? = nil) -> String { 32 | let selfUIColor = AgnosticColor(self) 33 | let otherUIColor = AgnosticColor(color) 34 | let contrastDetails = selfUIColor.contrastRatio(with: otherUIColor) 35 | return String(format: "%.2f: %@\n(Enforced: %@)", contrastDetails.ratio, contrastDetails.rating, level?.rawValue ?? "None") 36 | } 37 | } 38 | 39 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 40 | extension AgnosticColor { 41 | 42 | /// Calculates the contrast ratio between this color and another specified color and determines 43 | /// the accessibility rating based on that ratio. 44 | /// 45 | /// - Parameter otherColor: The other color to compare with. 46 | /// - Returns: A tuple containing the contrast ratio and the accessibility rating as a string. 47 | public func contrastRatio(with otherColor: AgnosticColor) -> (ratio: CGFloat, rating: String) { 48 | let luminance1 = self.luminance() 49 | let luminance2 = otherColor.luminance() 50 | let ratio = (max(luminance1, luminance2) + 0.05) / (min(luminance1, luminance2) + 0.05) 51 | 52 | return (ratio, rating(for: ratio)) 53 | } 54 | 55 | /// Determines the accessibility rating based on the contrast ratio. 56 | /// 57 | /// - Parameter ratio: The contrast ratio. 58 | /// - Returns: The corresponding accessibility rating as a string. 59 | private func rating(for ratio: CGFloat) -> String { 60 | switch ratio { 61 | case ..<3: 62 | return AccessibilityRating.fail.rawValue 63 | case ..<4.5: 64 | return AccessibilityRating.aaLarge.rawValue 65 | case ..<7: 66 | return AccessibilityRating.aa.rawValue 67 | default: 68 | return AccessibilityRating.aaa.rawValue 69 | } 70 | } 71 | } 72 | 73 | /// A view component that displays a color shade along with its contrast information. 74 | /// 75 | /// This view constructs a cell that visually represents a color shade (`shade`) and its 76 | /// contrast color (`contrast`), along with informational text (`info`) that describes the 77 | /// contrast details. It is designed to facilitate easy visualization of color contrasts in 78 | /// different UI contexts, especially useful in settings where visual accessibility is tested. 79 | /// 80 | /// - Parameters: 81 | /// - level: The color level associated with the shade. Used to label the view. 82 | /// - shade: The primary color of the view's background. 83 | /// - contrast: The color used for text foreground to ensure it stands out against the `shade`. 84 | /// - info: A string containing detailed information about the contrast ratio and/or other 85 | /// relevant data. 86 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 87 | internal struct ShadeCell: View { 88 | private let level: ColorLevel 89 | private let shade: Color 90 | private let contrast: Color 91 | private let info: String 92 | 93 | /// Initializes a new instance of `ShadeCell`. 94 | /// 95 | /// - Parameters: 96 | /// - level: The color level for which this cell is being displayed. 97 | /// - shade: The background color representing the specific color shade of the level. 98 | /// - contrast: The color used for text that contrasts with the background shade. 99 | /// - info: Textual information about the contrast properties or other relevant details. 100 | init(_ level: ColorLevel, shade: Color, contrast: Color, info: String) { 101 | self.level = level 102 | self.shade = shade 103 | self.contrast = contrast 104 | self.info = info 105 | } 106 | 107 | /// The content and layout body of the view. 108 | var body: some View { 109 | ZStack { 110 | shade 111 | VStack { 112 | Text("\(level)") 113 | Text(info) 114 | } 115 | .foregroundColor(contrast) 116 | .font(.caption.bold()) 117 | .multilineTextAlignment(.center) 118 | } 119 | } 120 | } 121 | 122 | #endif 123 | -------------------------------------------------------------------------------- /Sources/ContrastKit/Internal/Color+Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Project: ContrastKit 3 | // Author: Mark Battistella 4 | // Website: https://markbattistella.com 5 | // 6 | 7 | import SwiftUI 8 | 9 | // MARK: - Internal APIs 10 | 11 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 12 | extension Color { 13 | 14 | /// Creates a color from specified HSL (Hue, Saturation, Lightness) components. 15 | /// 16 | /// The method calculates the corresponding RGB values using the HSL color model and 17 | /// returns a SwiftUI Color. 18 | /// 19 | /// - Parameters: 20 | /// - hue: The hue component of the color, specified in degrees. 21 | /// - saturation: The saturation of the color, specified as a fraction from 0.0 to 1.0. 22 | /// - lightness: The lightness of the color, specified as a fraction from 0.0 to 1.0. 23 | /// - Returns: A SwiftUI Color object with the specified HSL values. 24 | internal static func hsl( 25 | hue: CGFloat, 26 | saturation: CGFloat, 27 | lightness: CGFloat 28 | ) -> Color { 29 | let c = (1 - abs(2 * lightness - 1)) * saturation 30 | let x = c * (1 - abs(fmod(hue / 60.0, 2) - 1)) 31 | let m = lightness - c / 2 32 | 33 | let (r, g, b) = hueToRGB(hue: hue, c: c, x: x, m: m) 34 | return Color(red: Double(r), green: Double(g), blue: Double(b)) 35 | } 36 | 37 | /// Retrieves and returns the HSL components of the color. 38 | /// 39 | /// This method uses the UIKit method to extract hue, saturation, and brightness 40 | /// values, then returns them as a tuple. Note that `brightness` in UIKit corresponds 41 | /// to `lightness` in HSL. 42 | /// 43 | /// - Returns: A tuple containing the hue, saturation, and lightness (brightness in UIKit) 44 | /// of the color. 45 | internal func uiColorComponents() -> (hue: CGFloat, saturation: CGFloat, lightness: CGFloat) { 46 | var hue: CGFloat = 0 47 | var saturation: CGFloat = 0 48 | var brightness: CGFloat = 0 49 | 50 | #if os(macOS) 51 | let color = AgnosticColor(self).usingColorSpace(.sRGB) ?? AgnosticColor.white 52 | #else 53 | let color = AgnosticColor(self) 54 | #endif 55 | 56 | color.getHue(&hue, 57 | saturation: &saturation, 58 | brightness: &brightness, 59 | alpha: nil) 60 | return (hue, saturation, brightness) 61 | } 62 | 63 | /// Calculates the color that provides the best contrast according to the specified 64 | /// contrast level settings. 65 | /// 66 | /// This method calculates the best contrast by evaluating all potential candidate colors 67 | /// and selecting the one that offers the highest contrast without exceeding the specified 68 | /// maximum ratio. 69 | /// 70 | /// - Parameters: 71 | /// - level: The contrast level to calculate. 72 | /// - maxRatio: The optional maximum contrast ratio to consider. If nil, there's no 73 | /// upper limit. 74 | /// - Returns: The `Color` that provides the best contrast. 75 | internal func calculateBestContrast( 76 | for level: ContrastLevel, 77 | maxRatio: CGFloat? = nil 78 | ) -> Color { 79 | let backgroundLuminance = AgnosticColor(self).luminance() 80 | var bestContrastColor: Color = .white 81 | var bestContrastRatio: CGFloat = 0 82 | let candidateColors = [Color.white, Color.black] + ColorLevel.allCases.map { self.level($0) } 83 | let (minRatio, upperLimit) = (level.ratio.min, maxRatio ?? level.ratio.max) 84 | for candidate in candidateColors { 85 | let candidateLuminance = AgnosticColor(candidate).luminance() 86 | let contrastRatio = (max(backgroundLuminance, candidateLuminance) + 0.05) / 87 | (min(backgroundLuminance, candidateLuminance) + 0.05) 88 | if contrastRatio >= minRatio 89 | && contrastRatio <= upperLimit 90 | && contrastRatio > bestContrastRatio { 91 | bestContrastRatio = contrastRatio 92 | bestContrastColor = candidate 93 | } 94 | } 95 | return bestContrastColor 96 | } 97 | } 98 | 99 | // MARK: - Private APIs 100 | 101 | @available(iOS 14.0, macOS 11.0, macCatalyst 14.0, tvOS 14.0, watchOS 7.0, visionOS 1.0, *) 102 | extension Color { 103 | 104 | /// Converts hue value to RGB components. 105 | /// 106 | /// This function supports the conversion by determining which segment 107 | /// of the color wheel the hue lies within, and adjusts the RGB values accordingly. 108 | /// 109 | /// - Parameters: 110 | /// - hue: The hue component of the color. 111 | /// - c: The color value. 112 | /// - x: The secondary color value. 113 | /// - m: The base value for RGB components. 114 | /// - Returns: The RGB components as a tuple. 115 | private static func hueToRGB( 116 | hue: CGFloat, 117 | c: CGFloat, 118 | x: CGFloat, 119 | m: CGFloat 120 | ) -> (CGFloat, CGFloat, CGFloat) { 121 | switch Int(hue / 60.0) % 6 { 122 | case 0: (c + m, x + m, m) 123 | case 1: (x + m, c + m, m) 124 | case 2: (m, c + m, x + m) 125 | case 3: (m, x + m, c + m) 126 | case 4: (x + m, m, c + m) 127 | case 5: (c + m, m, x + m) 128 | default: (m, m, m) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | ![SwiftUI colours in a shaded spectrum from dark tint to light tint](https://raw.githubusercontent.com/markbattistella/ContrastKit/main/.github/data/banner.png) 5 | 6 | # ContrastKit 7 | 8 | ![Swift Versions](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmarkbattistella%2FContrastKit%2Fbadge%3Ftype%3Dswift-versions) 9 | 10 | ![Platforms](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fmarkbattistella%2FContrastKit%2Fbadge%3Ftype%3Dplatforms) 11 | 12 | ![Licence](https://img.shields.io/badge/Licence-MIT-white?labelColor=blue&style=flat) 13 |
14 | 15 | **ContrastKit** is a Swift library designed to facilitate colour contrast handling within iOS, iPadOS, macOS, and tvOS applications. 16 | 17 | It provides developers with tools to automatically generate colour shades from any base colour and determine the most readable contrast colours according to established accessibility standards (AA Large, AA, and AAA). 18 | 19 | This package is particularly useful for UI/UX designers and developers focusing on accessibility and readability in their applications. 20 | 21 | ## Table of Contents 22 | 23 | - [Installation](#installation) 24 | - [Usage](#usage) 25 | - [Basic Usage](#basic-usage) 26 | - [In-app Usage](#in-app-usage) 27 | - [SwiftUI](#swiftui) 28 | - [UIKit](#uikit) 29 | - [AppKit](#appkit) 30 | - [Provided Levels](#provided-levels) 31 | - [Colour Range](#colour-range) 32 | - [Contrast](#contrast) 33 | - [Dark Mode](#dark-mode-optional) 34 | - [UITesting folder](#uitesting-folder) 35 | - [Debugging and Visualisation](#debugging-and-visualisation) 36 | - [Usage in Xcode](#usage-in-xcode) 37 | - [Integration with SwiftUI](#integration-with-swiftui) 38 | - [Why Use PreviewTesting?](#why-use-previewtesting) 39 | - [Contributing](#contributing) 40 | - [License](#license) 41 | 42 | ## Installation 43 | 44 | The ContrastKit package uses Swift Package Manager (SPM) for easy addition. Follow these steps to add it to your project: 45 | 46 | 1. In Xcode, click `File -> Swift Packages -> Add Package Dependency`. 47 | 2. In the search bar, type `https://github.com/markbattistella/ContrastKit` and click `Next`. 48 | 3. Specify the version you want to use. You can select the exact version, use the latest one, or set a version range, and then click `Next`. 49 | 4. Finally, select the target in which you want to use `ContrastKit` and click `Finish`. 50 | 51 | ## Usage 52 | 53 | Remember to import the `ContrastKit` module: 54 | 55 | ```swift 56 | import ContrastKit 57 | ``` 58 | 59 | ### Basic Usage 60 | 61 | The default usage of the ContrastKit package is quite straightforward. Here's an example: 62 | 63 | ```swift 64 | import ContrastKit 65 | 66 | // Example usage in SwiftUI 67 | let baseColor = Color.purple 68 | let shadeColor = baseColor.level(.level100) 69 | let contrastColor = shadeColor.highestRatedContrastLevel 70 | let fixedContrastColor = shadeColor.aaLargeContrastLevel 71 | 72 | // Example usage in UIKit 73 | let uiColor = UIColor(contrastColor) // Convert from SwiftUI Color to UIColor 74 | ``` 75 | 76 | The main call sites are designed for SwiftUI - but you can call `UIColor(color:)` too. 77 | 78 | > [!NOTE] 79 | > Using `UIColor(color:)` has been available from `iOS 14.0+`, `Mac Catalyst 14.0+`, `tvOS 14.0+`, `watchOS 7.0+`, `visionOS 1.0+`, `Xcode 12.0+` so we're fairly okay, I hope... 80 | 81 | ### In-app usage 82 | 83 | #### SwiftUI 84 | 85 | ```swift 86 | import SwiftUI 87 | import ContrastKit 88 | 89 | struct ContentView: View { 90 | private let baseColor = Color.green 91 | private let contrastColor = baseColor.highestRatedContrastLevel 92 | var body: some View { 93 | Text("High Contrast Text") 94 | .foregroundColor(contrastColor) 95 | .background(baseColor) 96 | } 97 | } 98 | ``` 99 | 100 | Or if you want to select a specific shade in the UI (suppose you've built a consistent theme): 101 | 102 | ```swift 103 | import SwiftUI 104 | import ContrastKit 105 | 106 | struct ContentView: View { 107 | private let baseColor = Color.green 108 | var body: some View { 109 | Text("Set themed text") 110 | .foregroundColor(baseColor) 111 | .background(baseColor.level50) 112 | } 113 | } 114 | ``` 115 | 116 | #### UIKit 117 | 118 | ```swift 119 | import UIKit 120 | import ContrastKit 121 | 122 | class ViewController: UIViewController { 123 | override func viewDidLoad() { 124 | super.viewDidLoad() 125 | let baseUIColor = UIColor.systemBlue 126 | let contrastKitColor = Color(baseUIColor) 127 | let highContrastUIColor = UIColor(contrastKitColor.highestRatedContrastLevel) 128 | 129 | let label = UILabel() 130 | label.text = "High Contrast Label" 131 | label.textColor = highContrastUIColor 132 | label.backgroundColor = baseUIColor 133 | label.frame = CGRect(x: 20, y: 100, width: 300, height: 50) 134 | label.textAlignment = .center 135 | 136 | self.view.addSubview(label) 137 | } 138 | } 139 | ``` 140 | 141 | #### AppKit 142 | 143 | ```swift 144 | import AppKit 145 | import ContrastKit 146 | 147 | class ViewController: NSViewController { 148 | override func loadView() { 149 | self.view = NSView() 150 | let baseNSColor = NSColor.systemRed 151 | let contrastKitColor = Color(baseNSColor) 152 | let highContrastNSColor = NSColor(contrastKitColor.highestRatedContrastLevel) 153 | 154 | let textField = NSTextField(labelWithString: "High Contrast Text") 155 | textField.textColor = highContrastNSColor 156 | textField.backgroundColor = baseNSColor 157 | textField.frame = CGRect(x: 20, y: 20, width: 300, height: 50) 158 | textField.isBezeled = false 159 | textField.drawsBackground = true 160 | 161 | self.view.addSubview(textField) 162 | } 163 | } 164 | ``` 165 | 166 | ## Provided Levels 167 | 168 | ContrastKit provides two core enums to assist in designing UIs with appropriate colour contrast: `ColorLevel` and `ContrastLevel`. These enums help developers standardise the visual accessibility of their applications. 169 | 170 | ### Colour Range 171 | 172 | The `ColorLevel` enum defines different levels of lightness for colours, which can be used to generate various shades from a single base colour. These shades range from very light (near white) to very dark (near black), suitable for different UI elements like backgrounds, text, and interactive elements. 173 | 174 | | Level | Lightness | Description | 175 | |------------|-----------|---------------------------------------------------------------------| 176 | | `level50` | `0.95` | Very light shade, almost white. | 177 | | `level100` | `0.90` | Very light shade. | 178 | | `level200` | `0.80` | Lighter shade, for subtle backgrounds. | 179 | | `level300` | `0.70` | Light shade, good for hover states or secondary buttons. | 180 | | `level400` | `0.60` | Medium light shade. | 181 | | `level500` | `0.50` | Neutral base shade, often used for the primary variant of a colour. | 182 | | `level600` | `0.40` | Medium dark shade. | 183 | | `level700` | `0.30` | Darker shade, suitable for text. | 184 | | `level800` | `0.20` | Very dark shade, often used for text or active elements. | 185 | | `level900` | `0.10` | Very dark, closer to black. | 186 | | `level950` | `0.05` | Extremely dark, almost black. | 187 | 188 | | Light mode | Dark mode | 189 | |:-:|:-:| 190 | | ![Light Mode range of shades](https://raw.githubusercontent.com/markbattistella/ContrastKit/main/.github/data/light-mode.png) | ![Dark Mode range of shades](https://raw.githubusercontent.com/markbattistella/ContrastKit/main/.github/data/dark-mode.png) | 191 | 192 | ### Contrast 193 | 194 | The `ContrastLevel` enum specifies minimum and maximum contrast ratios for three accessibility standards: AA Large, AA, and AAA. These levels are based on the WCAG guidelines to ensure that text and interactive elements are readable and accessible. 195 | 196 | | Level | Minimum Ratio | Maximum Ratio | Description | 197 | |----------|---------------|---------------|-------------| 198 | | AA Large | `3.0` | `4.49` | Suitable for large text, offering basic readability. | 199 | | AA | `4.5` | `6.99` | Standard level for normal text size, providing clear readability. | 200 | | AAA | `7.0` | `.infinity` | Highest contrast level, recommended for the best readability across all contexts. | 201 | 202 | ### Dark mode (optional) 203 | 204 | ContrastKit also provides an optional dark mode (as pictured above) if you need the colours to adapt dynamically with your UI. 205 | 206 | You can do this by passing the `@Environment(\.colorScheme)` directly into the `level(_:scheme:)` function. 207 | 208 | ```swift 209 | struct EnvironmentShadeView: View { 210 | @Environment(\.colorScheme) private var colorScheme 211 | var body: some View { 212 | VStack { 213 | Color.red.level(.level100, scheme: colorScheme) 214 | Color.red.level(.level900, scheme: colorScheme) 215 | } 216 | } 217 | } 218 | ``` 219 | 220 | When the device switches colour schemes, the `ColorLevel` will flip: 221 | 222 | | Level | Light mode | Dark mode equivalent | 223 | |------------|------------|----------------------| 224 | | `level50` | `0.95` | `0.05` | 225 | | `level100` | `0.90` | `0.10` | 226 | | `level200` | `0.80` | `0.20` | 227 | | `level300` | `0.70` | `0.30` | 228 | | `level400` | `0.60` | `0.40` | 229 | | `level500` | `0.50` | `0.50` | 230 | | `level600` | `0.40` | `0.60` | 231 | | `level700` | `0.30` | `0.70` | 232 | | `level800` | `0.20` | `0.80` | 233 | | `level900` | `0.10` | `0.90` | 234 | | `level950` | `0.05` | `0.95` | 235 | 236 | ## UITesting folder 237 | 238 | The `PreviewTesting` file is a vital part of the ContrastKit package, designed exclusively for debugging and visual inspection purposes during the development process. 239 | 240 | It allows developers to quickly view and evaluate the contrast levels and colour shades generated by the library directly within the SwiftUI Preview environment. 241 | 242 | This file contains sample views and configurations that demonstrate the practical application of ContrastKit. 243 | 244 | ### Debugging and Visualisation 245 | 246 | The file includes an `Example+SwiftUI` file that utilises ContrastKit functionalities to display a series of colours with varying levels of contrast. This visual representation helps in understanding how different colours and their respective contrast levels appear in a UI context. 247 | 248 | There are two previews available - `StandardShadeView` and `EnvironmentShadeView`. 249 | 250 | The first is an example, where the shades of colours and their contrast colours are fixed regardless of the device colour scheme. 251 | 252 | In the `EnvironmentShadeView` example, you can see how to pass in the `@Environment(\.colorScheme)` to the `level(_:scheme:)` function and it will update the colours automatically for you. 253 | 254 | ### Usage in Xcode 255 | 256 | To use this file effectively: 257 | 258 | 1. Open your Xcode project containing the ContrastKit. 259 | 1. Navigate to the PreviewTesting file. 260 | 1. Ensure your environment is set to Debug mode to activate the #if DEBUG condition. 261 | 1. Open the SwiftUI Preview pane to see the results. 262 | 263 | > [!CAUTION] 264 | > This file is intended for development use only and should not be included in the production build of the application. 265 | 266 | It provides a straightforward and effective way to visually inspect the accessibility features provided by ContrastKit, ensuring that the colour contrasts meet the required standards before the application is deployed. 267 | 268 | ### Integration with SwiftUI 269 | 270 | The `PreviewTesting` file demonstrates the integration of ContrastKit with SwiftUI, showing how easily developers can implement and test colour contrasts in their UI designs. 271 | 272 | By modifying the `baseColor` or the `ColorLevel`, developers can experiment with different combinations to find the optimal settings for their specific needs. 273 | 274 | ### Why Use PreviewTesting? 275 | 276 | - **Immediate Feedback:** Allows developers to see how changes in colour levels affect accessibility and readability in real-time. 277 | - **Accessibility Testing:** Helps in ensuring that the UI meets accessibility standards, particularly when creating inclusive applications. 278 | - **Ease of Use:** Simplifies the process of testing multiple colour schemes without needing to deploy the app or navigate through different screens. 279 | 280 | The `PreviewTesting` file is a crucial tool for developers who are serious about integrating effective contrast handling in their applications, making ContrastKit a practical choice for enhancing UI accessibility. 281 | 282 | ## Examples 283 | 284 | | iOS | iPadOS | 285 | |:-:|:-:| 286 | | ![iOS Shades example](https://raw.githubusercontent.com/markbattistella/ContrastKit/main/.github/data/iOS.png) | ![iPad Shades example](https://raw.githubusercontent.com/markbattistella/ContrastKit/main/.github/data/iPadOS.png) | 287 | 288 | | tvOS | visionOS | 289 | |:-:|:-:| 290 | | ![tvOS Shades example](https://raw.githubusercontent.com/markbattistella/ContrastKit/main/.github/data/tvOS.png) | ![visionOS Shades example](https://raw.githubusercontent.com/markbattistella/ContrastKit/main/.github/data/visionOS.png) | 291 | 292 | ## Contributing 293 | 294 | Contributions are highly encouraged, as they help make ContrastKit even more useful to the developer community. Feel free to fork the project, submit pull requests, or send suggestions via GitHub issues. 295 | 296 | ## License 297 | 298 | ContrastKit is available under the MIT license, allowing for widespread use and modification in both personal and commercial projects. See the [LICENSE](https://raw.githubusercontent.com/markbattistella/ContrastKit/main/LICENCE) file included in the repository for full details. 299 | --------------------------------------------------------------------------------