Skip to content

Commit b3f84eb

Browse files
committed
feat: Custom crop control view.
1 parent bde2280 commit b3f84eb

File tree

1 file changed

+64
-23
lines changed

1 file changed

+64
-23
lines changed

Sources/CropImage/CropImageView.swift

Lines changed: 64 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import UIKit
1111
#endif
1212

1313
/// A view that allows the user to crop an image.
14-
public struct CropImageView: View {
14+
public struct CropImageView<Controls: View>: View {
1515
/// Errors that could happen during the cropping process.
1616
public enum CropError: Error {
1717
/// SwiftUI `ImageRenderer` returned nil when calling `nsImage` or `uiImage`.
@@ -28,6 +28,29 @@ public struct CropImageView: View {
2828
case failedToGetImageFromCurrentUIGraphicsImageContext
2929
}
3030

31+
private static func defaultControlsView(crop: @escaping () async -> ()) -> AnyView { AnyView(
32+
VStack {
33+
Spacer()
34+
HStack {
35+
Spacer()
36+
Button { Task {
37+
await crop()
38+
} } label: {
39+
Label("Crop", systemImage: "checkmark.circle.fill")
40+
.font(.title2)
41+
.foregroundColor(.accentColor)
42+
.labelStyle(.iconOnly)
43+
.padding(1)
44+
.background(
45+
Circle().fill(.white)
46+
)
47+
}
48+
.buttonStyle(.plain)
49+
.padding()
50+
}
51+
}
52+
) }
53+
3154
/// The image to crop.
3255
public var image: PlatformImage
3356
/// The intended size of the cropped image, in points.
@@ -40,6 +63,41 @@ public struct CropImageView: View {
4063
///
4164
/// The error should be a ``CropError``.
4265
public var onCrop: (Result<PlatformImage, Error>) -> Void
66+
/// A custom view overlaid on the image cropper.
67+
///
68+
/// - Parameters:
69+
/// - crop: An async function to trigger crop action. Result will be delivered via ``onCrop``.
70+
public var controls: (_ crop: @escaping () async -> ()) -> Controls
71+
72+
/// Create a ``CropImageView`` with a custom ``controls`` view.
73+
public init(
74+
image: PlatformImage,
75+
targetSize: CGSize,
76+
targetScale: CGFloat = 1,
77+
onCrop: @escaping (Result<PlatformImage, Error>) -> Void,
78+
@ViewBuilder controls: @escaping (_ crop: () async -> ()) -> Controls
79+
) {
80+
self.image = image
81+
self.targetSize = targetSize
82+
self.targetScale = targetScale
83+
self.onCrop = onCrop
84+
self.controls = controls
85+
}
86+
/// Create a ``CropImageView`` with the default ``controls`` view.
87+
///
88+
/// The default ``controls`` view is a simple overlay with a checkmark icon on the bottom-trailing corner to trigger crop action.
89+
public init(
90+
image: PlatformImage,
91+
targetSize: CGSize,
92+
targetScale: CGFloat = 1,
93+
onCrop: @escaping (Result<PlatformImage, Error>) -> Void
94+
) where Controls == AnyView {
95+
self.image = image
96+
self.targetSize = targetSize
97+
self.targetScale = targetScale
98+
self.onCrop = onCrop
99+
self.controls = Self.defaultControlsView
100+
}
43101

44102
@State private var offset: CGSize = .zero
45103
@State private var scale: CGFloat = 1
@@ -94,28 +152,11 @@ public struct CropImageView: View {
94152
.fill(style: FillStyle(eoFill: true))
95153
.foregroundColor(.black.opacity(0.6))
96154
.allowsHitTesting(false)
97-
VStack {
98-
Spacer()
99-
HStack {
100-
Spacer()
101-
Button { Task {
102-
do {
103-
onCrop(.success(try crop()))
104-
} catch {
105-
onCrop(.failure(error))
106-
}
107-
} } label: {
108-
Label("Crop", systemImage: "checkmark.circle.fill")
109-
.font(.title2)
110-
.foregroundColor(.accentColor)
111-
.labelStyle(.iconOnly)
112-
.padding(1)
113-
.background(
114-
Circle().fill(.white)
115-
)
116-
}
117-
.buttonStyle(.plain)
118-
.padding()
155+
controls {
156+
do {
157+
onCrop(.success(try crop()))
158+
} catch {
159+
onCrop(.failure(error))
119160
}
120161
}
121162
}

0 commit comments

Comments
 (0)