A modern, ratio-based image cropper for SwiftUI.
The crop frame stays fixed while the user pans and zooms the image underneath —
just like iOS Photos or Instagram.
- Pan & pinch-to-zoom with anchor at finger midpoint
- Rubber-band elastic drag beyond crop bounds with animated snap-back
- Multiple grid styles — rule of thirds, golden ratio, diagonal, crosshair
- Crop shapes — rectangle, circle, rounded rectangle
- Double-tap/click to toggle zoom
- Programmatic reset & image export via binding triggers
- Haptic feedback on iOS when dragging past bounds
- Preset aspect ratios —
1:1,3:2,4:3,16:9(or define your own) - Normalized
CGRectoutput (0–1 range) for easy image processing - Fully configurable via SwiftUI modifiers — all options are opt-in
- Cross-platform — iOS 13+ and macOS 10.15+
Add the package to your Xcode project:
File → Add Package Dependencies → paste the URL:
https://github.com/marshallino16/ImageCropper
Or add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/marshallino16/ImageCropper", from: "1.0.0")
]import ImageCropper
ImageCropperView(image: UIImage(named: "photo")!, ratio: .r_1_1)
.onCropChanged { cropRect in
print(cropRect) // normalized CGRect (0–1)
}import ImageCropper
ImageCropperView(image: NSImage(named: "photo")!, ratio: .r_1_1)
.onCropChanged { cropRect in
print(cropRect)
}ImageCropperView(image: Image("photo"),
imageSize: CGSize(width: 1920, height: 1080),
ratio: .r_16_9)
.onCropChanged { cropRect in
print(cropRect)
}@State private var shouldReset = false
@State private var shouldCrop = false
ImageCropperView(image: UIImage(named: "photo")!, ratio: .r_4_3)
.onCropChanged { cropRect in print(cropRect) }
// Visual
.backgroundColor(.black)
.overlayStyle(color: .black, opacity: 0.55)
.cornerStyle(color: .white, size: 20, weight: 2)
.showCornerGuides(true)
.cropShape(.circle)
.gridStyle(.goldenRatio)
.gridColor(.white, lineWidth: 0.5, opacity: 0.6)
.alwaysShowGrid(true)
// Behavior
.zoomRange(1.0...8.0)
.snapBackAnimation(.spring(response: 0.4, dampingFraction: 0.8))
.rubberBandEnabled(true)
.rubberBandFactor(0.35)
// Interactions
.doubleTapToZoom(enabled: true, targetScale: 2.0)
.hapticFeedback(true) // iOS only
// Triggers
.resetTrigger($shouldReset)
.cropTrigger($shouldCrop)
.onCropImage { croppedImage in
// croppedImage is UIImage (iOS) or NSImage (macOS)
}| Parameter | Type | Default | Required |
|---|---|---|---|
image |
UIImage / NSImage / Image |
— | Yes |
imageSize |
CGSize |
— | Only with Image init |
cropRect |
CGRect? |
nil |
No |
ratio |
CropperRatio |
— | Yes |
| Modifier | Description |
|---|---|
.onCropChanged { CGRect in } |
Called whenever the visible crop region changes |
.alwaysShowGrid(true) |
Show the grid permanently (default: only during interaction) |
| Modifier | Default | Description |
|---|---|---|
.backgroundColor(_:) |
.black |
Background color behind the image |
.overlayStyle(color:opacity:) |
black, 0.55 | Dimmed area outside the crop frame |
.cornerStyle(color:size:weight:) |
white, 20, 2 | Corner guide appearance |
.showCornerGuides(_:) |
true |
Show or hide corner guides (always hidden for .circle) |
.cropShape(_:) |
.rectangle |
.rectangle · .circle · .roundedRect(cornerRadius:) |
.gridStyle(_:) |
.ruleOfThirds |
.ruleOfThirds · .goldenRatio · .diagonal · .crosshair · .none |
.gridColor(_:lineWidth:opacity:) |
white, 0.5, 0.6 | Grid line appearance |
| Modifier | Default | Description |
|---|---|---|
.zoomRange(_:) |
1.0...5.0 |
Allowed zoom scale range |
.snapBackAnimation(_:) |
.easeInOut(0.3) |
Animation when the image settles after a gesture |
.rubberBandEnabled(_:) |
true |
Enable elastic drag beyond crop bounds |
.rubberBandFactor(_:) |
0.35 |
Dampening factor (0 = none, 1 = full stretch) |
| Modifier | Default | Description |
|---|---|---|
.doubleTapToZoom(enabled:targetScale:) |
off, 2.0 | Toggle zoom on double-tap/click |
.hapticFeedback(_:) |
false |
Haptic when dragging past bounds (iOS only) |
| Modifier | Description |
|---|---|
.resetTrigger($binding) |
Set to true to reset scale & offset (auto-resets to false) |
.cropTrigger($binding) |
Set to true to trigger crop export (auto-resets to false) |
.onCropImage { PlatformImage in } |
Receive the cropped UIImage/NSImage when crop fires |
| Preset | Value |
|---|---|
CropperRatio.r_1_1 |
1 : 1 |
CropperRatio.r_3_2 |
3 : 2 |
CropperRatio.r_4_3 |
4 : 3 |
CropperRatio.r_16_9 |
16 : 9 |
Custom ratios:
CropperRatio(width: 21, height: 9)// Cross-platform image type alias
#if iOS
public typealias PlatformImage = UIImage
#else
public typealias PlatformImage = NSImage
#endif
// Crop shape
public enum CropShape: Equatable, Sendable {
case rectangle
case circle
case roundedRect(cornerRadius: CGFloat)
}
// Grid style
public enum GridStyle: Sendable {
case ruleOfThirds
case goldenRatio
case diagonal
case crosshair
case none
}The repository includes two demo apps showcasing every modifier and option:
| App | Location | Description |
|---|---|---|
| Demo-macOS | Demo-macOS/ |
macOS app with HSplitView — cropper + sidebar controls |
| Demo-iOS | Demo-iOS/ |
iOS app with top cropper + scrollable controls below |
Open the workspace to build both:
open SwiftUI-ImageCropper.xcworkspaceSources/ImageCropper/
├── ImageCropperView.swift # Public API — SwiftUI View with all modifiers
├── CropperView.swift # Internal UI — ZStack, gesture handling, crop math
├── CropperConfiguration.swift# All configurable values in one struct
├── CropperRatio.swift # Aspect ratio value type with presets
├── CropShape.swift # Rectangle / circle / rounded rect enum
├── GridStyle.swift # Grid overlay pattern enum
├── GridOverlay.swift # Grid drawing view (5 styles)
├── CornerGuides.swift # L-shaped corner guide view
├── CropHoleShape.swift # Even-odd cutout shape
├── ChangeObserver.swift # onChange polyfill for iOS 13 / macOS 10.15
├── NativeGestureOverlay.swift # UIKit & AppKit gesture bridge
└── PlatformTypes.swift # PlatformImage typealias + crop helper
Data flow:
ImageCropperView → GeometryReader → CropperView → NativeGestureOverlay captures gestures → crop rect computed → .onCropChanged fires with normalized CGRect
| Requirement | Minimum |
|---|---|
| Xcode | 12+ |
| Swift | 5.3+ |
| iOS | 13.0+ |
| macOS | 10.15+ |
ImageCropper is available under the MIT license. See the LICENSE file for details.
