A SwiftUI package for editing color gradients with an intuitive, gesture-driven interface.
- โจ 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
@MainActorisolation
Add GradientEditor to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/JoshuaSullivan/GradientEditor.git", from: "1.3.0")
]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)
}
}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)
}
}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)
}
}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
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
GradientColorScheme- A named gradient with metadataColorMap- Collection of color stops defining a gradientColorStop- Single color or dual-color at a position (0.0-1.0)ColorStopType-.single(CGColor)or.dual(CGColor, CGColor)
// 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
}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
After editing a gradient, you can easily convert it to platform-specific gradient types:
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)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)
)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).
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)
}- 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 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)
- Editor appears in a modal sheet
- Controls hidden during editing
.presentationDetents([.medium, .large])
- Side-by-side layout
- Editor panel on right (300pt)
- Controls remain visible
- 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
All user-facing strings are localized via Localizable.xcstrings. The package is ready for additional language translations.
- iOS 18.0+ / visionOS 2.0+ / macOS 15.0+
- Swift 6.0+
- Xcode 16.0+
- 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
Run tests with:
swift testTest Coverage:
- 163 tests across 10 suites
- Models, view models, geometry, views, integration, platform conversions
- 100% pass rate
- ~93% coverage of business logic
Full DocC documentation is available. Build documentation in Xcode:
- Product โ Build Documentation
- View in Documentation Viewer
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
Created by Joshua Sullivan
Made with โค๏ธ using SwiftUI


