GradientEditor

1.4.0

An open-source Swift package for editing Gradients.
JoshuaSullivan/GradientEditor

What's New

GradientEditor v1.4.0 - Custom Color Selection

2025-11-11T15:33:43Z

๐ŸŽจ Custom Color Provider Support

This release adds the ability for developers to replace the system color picker with their own custom color selection UI through the new ColorProvider protocol.

โœจ New Features

ColorProvider Protocol

Enable complete customization of color selection UI:

  • ColorProvider protocol for providing custom color selection views
  • ColorEditContext provides rich context (color index, stop type, accessibility labels)
  • DefaultColorProvider wraps the system ColorPicker (used by default)
  • Non-breaking API with optional parameter and default value

Example Implementation

The demo app includes a working example:

  • HueSliderColorProvider demonstrates custom implementation
  • Square color preview with 3pt rounded corners
  • Slider-based hue selection (0-1 range with S and B fixed at 1.0)
  • Sheet presentation with color preview and Save/Cancel buttons
  • Full accessibility support

๐Ÿ“– Usage Example

// Define a custom color provider
struct MyCustomColorProvider: ColorProvider {
    func colorView(
        currentColor: CGColor,
        onColorChange: @escaping @MainActor @Sendable (CGColor) -> Void,
        context: ColorEditContext
    ) -> AnyView {
        AnyView(MyCustomColorPicker(
            color: currentColor,
            onChange: onColorChange
        ))
    }
}

// Use in your gradient editor
let viewModel = GradientEditViewModel(
    scheme: myScheme,
    colorProvider: MyCustomColorProvider()
)

๐ŸŽฏ Use Cases

  • โœ… Brand-specific color palettes
  • โœ… Limited color selection (design system compliance)
  • โœ… Specialized color pickers (hue slider, hex input, etc.)
  • โœ… Integration with existing color management systems
  • โœ… Custom accessibility features
  • โœ… Alternative UI paradigms (swatches, presets, etc.)

๐Ÿงช Testing

  • Added 15 new tests (174 โ†’ 178 tests)
  • 178/178 tests passing (100% pass rate)
  • Zero compiler warnings
  • Comprehensive unit and integration test coverage

๐Ÿ“ Documentation

  • Module-level DocC documentation added to GradientEditor.swift
  • Complete API documentation for all new types
  • Updated TechnicalDesign.md with ColorProvider section
  • Working example in demo app
  • Comprehensive CHANGELOG entry

๐Ÿ”ง Technical Details

  • Concurrency: @mainactor isolated, @sendable compliant (Swift 6)
  • Architecture: Callback-based color change pattern
  • Context: Separate calls for each color in dual stops
  • Accessibility: Full context provided (color index, stop type, labels)
  • Compatibility: Non-breaking change with backward compatibility

๐Ÿ“ฆ What's Included

New Files:

  • Sources/GradientEditor/Protocols/ColorProvider.swift
  • Sources/GradientEditor/Protocols/DefaultColorProvider.swift
  • Tests/GradientEditorTests/DefaultColorProviderTests.swift
  • Examples/GradientEditorExample/HueSliderColorProvider.swift

Updated Files:

  • Sources/GradientEditor/GradientEditor.swift (module documentation)
  • Sources/GradientEditor/ViewModels/GradientEditViewModel.swift
  • Sources/GradientEditor/Views/ColorStopEditorView.swift
  • Sources/GradientEditor/Views/GradientEditView.swift
  • Example app files for demo integration
  • Test files updated for new parameter
  • Complete documentation updates

๐Ÿš€ Breaking Changes

None - This is a backward-compatible release. The colorProvider parameter has a default value of DefaultColorProvider(), so existing code continues to work without modifications.

๐Ÿ“‹ Requirements

  • iOS 18.0+ / macOS 15.0+ / visionOS 2.0+
  • Swift 6.0+
  • Xcode 16.0+

GradientEditor

A SwiftUI package for editing color gradients with an intuitive, gesture-driven interface.

Swift 6.0 Platforms License

Screenshots

Main gradient editor interface Color stop editor Gradient settings

Features

  • โœจ Interactive Gradient Editing - Drag color stops, adjust positions, modify colors
  • ๐ŸŽจ Single & Dual-Color Stops - Create smooth gradients or hard color transitions
  • ๐Ÿ” Zoom & Pan - Zoom up to 4x for precise stop positioning
  • ๐Ÿ“ฑ Adaptive Layout - Automatic adaptation to device size and orientation
  • โ™ฟ๏ธ Fully Accessible - Complete VoiceOver and Dynamic Type support
  • ๐ŸŒ Localized - Ready for internationalization with string catalog
  • ๐Ÿงช Thoroughly Tested - 163 tests with 100% pass rate
  • ๐ŸŽฏ Swift 6 Strict Concurrency - Thread-safe with @MainActor isolation

Quick Start

Installation

Add GradientEditor to your project using Swift Package Manager:

dependencies: [
    .package(url: "https://github.com/JoshuaSullivan/GradientEditor.git", from: "1.3.0")
]

Basic Usage

import SwiftUI
import GradientEditor

struct ContentView: View {
    @State private var viewModel: GradientEditViewModel

    init() {
        // Create view model with a preset gradient
        viewModel = GradientEditViewModel(scheme: .wakeIsland) { result in
            switch result {
            case .saved(let scheme):
                print("Gradient '\(scheme.name)' saved with \(scheme.colorMap.stops.count) stops")
                // Save the gradient scheme to your app's storage
            case .cancelled:
                print("Editing cancelled")
            }
        }
    }

    var body: some View {
        GradientEditView(viewModel: viewModel)
    }
}

UIKit Usage (iOS/visionOS)

For UIKit-based apps, use GradientEditorViewController:

import UIKit
import GradientEditor

class MyViewController: UIViewController {
    func presentGradientEditor() {
        let editor = GradientEditorViewController(scheme: .wakeIsland) { result in
            switch result {
            case .saved(let scheme):
                self.saveGradient(scheme)
            case .cancelled:
                print("User cancelled")
            }
            self.dismiss(animated: true)
        }

        // Present modally with navigation controller
        let nav = UINavigationController(rootViewController: editor)
        present(nav, animated: true)
    }
}

Delegate Pattern:

class MyViewController: UIViewController, GradientEditorDelegate {
    func presentGradientEditor() {
        let editor = GradientEditorViewController(scheme: .wakeIsland)
        editor.delegate = self

        let nav = UINavigationController(rootViewController: editor)
        present(nav, animated: true)
    }

    func gradientEditor(_ editor: GradientEditorViewController, didSaveScheme scheme: GradientColorScheme) {
        saveGradient(scheme)
        dismiss(animated: true)
    }

    func gradientEditorDidCancel(_ editor: GradientEditorViewController) {
        dismiss(animated: true)
    }
}

AppKit Usage (macOS)

For AppKit-based Mac apps, use the AppKit GradientEditorViewController:

import AppKit
import GradientEditor

class MyViewController: NSViewController {
    func presentGradientEditor() {
        let editor = GradientEditorViewController(scheme: .wakeIsland) { result in
            switch result {
            case .saved(let scheme):
                self.saveGradient(scheme)
            case .cancelled:
                print("User cancelled")
            }
            self.dismiss(editor)
        }

        // Present as sheet
        presentAsSheet(editor)
    }
}

Delegate Pattern:

class MyViewController: NSViewController, GradientEditorDelegate {
    func presentGradientEditor() {
        let editor = GradientEditorViewController(scheme: .wakeIsland)
        editor.delegate = self
        presentAsSheet(editor)
    }

    func gradientEditor(_ editor: GradientEditorViewController, didSaveScheme scheme: GradientColorScheme) {
        saveGradient(scheme)
        dismiss(editor)
    }

    func gradientEditorDidCancel(_ editor: GradientEditorViewController) {
        dismiss(editor)
    }
}

Key Components

GradientEditView

The main SwiftUI view for gradient editing. Provides:

  • Interactive gradient preview with draggable color stops
  • Zoom (1x-4x) and pan gestures for precise editing
  • Adaptive layout for compact and regular size classes
  • Built-in controls for adding and editing color stops

GradientEditViewModel

The view model managing gradient editing state:

  • Color stop management (add, delete, duplicate, modify)
  • Zoom and pan state
  • Selection and editing state
  • Completion callbacks for save/cancel

Data Models

  • GradientColorScheme - A named gradient with metadata
  • ColorMap - Collection of color stops defining a gradient
  • ColorStop - Single color or dual-color at a position (0.0-1.0)
  • ColorStopType - .single(CGColor) or .dual(CGColor, CGColor)

Example: Custom Gradient

// Create a custom gradient
let customGradient = GradientColorScheme(
    name: "Ocean Depths",
    description: "Deep blues fading to black",
    colorMap: ColorMap(stops: [
        ColorStop(position: 0.0, type: .single(.blue)),
        ColorStop(position: 0.7, type: .dual(.cyan, .black)),
        ColorStop(position: 1.0, type: .single(.black))
    ])
)

// Use it in the editor
let viewModel = GradientEditViewModel(scheme: customGradient) { result in
    // Handle result
}

Built-in Presets

GradientEditor includes several preset gradients:

  • Black & White - Simple two-color gradient
  • Wake Island - Tropical island colors
  • Neon Ripples - Abstract neon lines
  • Apple ][ River - Retro computing green
  • Electoral Map - Red vs. blue
  • Topographic - Map-inspired contours

Access all presets: GradientColorScheme.allPresets

Using Gradients in Your App

After editing a gradient, you can easily convert it to platform-specific gradient types:

SwiftUI Gradients

case .saved(let scheme):
    // Linear gradient (horizontal by default)
    let linear = scheme.linearGradient()
    Rectangle().fill(linear)

    // Vertical gradient
    let vertical = scheme.linearGradient(startPoint: .top, endPoint: .bottom)
    Rectangle().fill(vertical)

    // Radial gradient
    let radial = scheme.radialGradient(
        center: .center,
        startRadius: 0,
        endRadius: 200
    )
    Circle().fill(radial)

    // Angular/Conic gradient
    let angular = scheme.angularGradient(center: .center)
    Circle().fill(angular)

UIKit - CAGradientLayer

case .saved(let scheme):
    // Create gradient layer for UIView
    let gradientLayer = scheme.caGradientLayer(
        frame: view.bounds,
        type: .axial,
        startPoint: CGPoint(x: 0, y: 0),    // top-left
        endPoint: CGPoint(x: 1, y: 1)       // bottom-right
    )
    view.layer.insertSublayer(gradientLayer, at: 0)

    // Radial gradient
    let radial = scheme.caGradientLayer(
        frame: view.bounds,
        type: .radial,
        startPoint: CGPoint(x: 0.5, y: 0.5),
        endPoint: CGPoint(x: 1, y: 1)
    )

    // Conic gradient
    let conic = scheme.caGradientLayer(
        frame: view.bounds,
        type: .conic,
        startPoint: CGPoint(x: 0.5, y: 0.5)
    )

AppKit - NSGradient

case .saved(let scheme):
    guard let gradient = scheme.nsGradient() else { return }

    // Draw linear gradient in NSView
    let startPoint = NSPoint(x: 0, y: bounds.height)
    let endPoint = NSPoint(x: bounds.width, y: bounds.height)
    gradient.draw(from: startPoint, to: endPoint, options: [])

    // Draw radial gradient
    let center = NSPoint(x: bounds.midX, y: bounds.midY)
    gradient.draw(
        fromCenter: center,
        radius: 0,
        toCenter: center,
        radius: bounds.width / 2,
        options: []
    )

Note: All conversion methods work on both GradientColorScheme and ColorMap. Dual-color stops create hard transitions in SwiftUI and CAGradientLayer. For NSGradient, hard transitions are approximated by placing both colors extremely close together (0.0001 apart).

Advanced: Component Accessors

For advanced use cases, you can access the raw color/location arrays:

// SwiftUI - get raw Gradient.Stop array
let stops = scheme.gradientStops()
let customGradient = LinearGradient(stops: stops, startPoint: .topLeading, endPoint: .bottomTrailing)

// UIKit - get raw CGColor and NSNumber arrays
let (colors, locations) = scheme.caGradientComponents()
let layer = CAGradientLayer()
layer.colors = colors
layer.locations = locations
layer.type = .conic  // Apply custom configuration

// AppKit - get raw NSColor and CGFloat arrays
if let (colors, locations) = scheme.nsGradientComponents() {
    let gradient = NSGradient(colors: colors, atLocations: locations, colorSpace: .sRGB)
}

Gestures

Gradient Preview

  • Tap - Select a color stop for editing
  • Drag - Move a color stop along the gradient
  • Pinch - Zoom in/out (1x to 4x)
  • Two-finger drag - Pan when zoomed in

Color Stop Editor

  • Color Picker - Change stop colors
  • Position Field - Enter precise position value
  • Type Picker - Switch between single/dual color
  • Prev/Next - Navigate between stops
  • Duplicate - Create a copy at midpoint
  • Delete - Remove stop (minimum 2 stops)

Adaptive Layout

Compact Width (iPhone Portrait)

  • Editor appears in a modal sheet
  • Controls hidden during editing
  • .presentationDetents([.medium, .large])

Regular Width (iPad, iPhone Landscape)

  • Side-by-side layout
  • Editor panel on right (300pt)
  • Controls remain visible

Accessibility

  • VoiceOver Labels - All interactive elements labeled
  • Accessibility Hints - Contextual action descriptions
  • Dynamic Type - Scales with text size preferences
  • Accessibility Identifiers - For UI testing
  • Gesture Accessibility - VoiceOver-compatible actions

Localization

All user-facing strings are localized via Localizable.xcstrings. The package is ready for additional language translations.

Requirements

  • iOS 18.0+ / visionOS 2.0+ / macOS 15.0+
  • Swift 6.0+
  • Xcode 16.0+

Architecture

  • MVVM Pattern - Clear separation of concerns
  • SwiftUI - Modern declarative UI
  • @Observable - State management
  • Combine - Action publishers for view model communication
  • Swift 6 Strict Concurrency - Thread-safe with @MainActor

Testing

Run tests with:

swift test

Test Coverage:

  • 163 tests across 10 suites
  • Models, view models, geometry, views, integration, platform conversions
  • 100% pass rate
  • ~93% coverage of business logic

Documentation

Full DocC documentation is available. Build documentation in Xcode:

  1. Product โ†’ Build Documentation
  2. View in Documentation Viewer

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Credits

Created by Joshua Sullivan


Made with โค๏ธ using SwiftUI

Description

  • Swift Tools 6.0.0
View More Packages from this Author

Dependencies

  • None
Last updated: Thu Apr 09 2026 07:14:21 GMT-0900 (Hawaii-Aleutian Daylight Time)