mirror of
https://github.com/laosb/CropImage.git
synced 2025-04-30 23:51:08 +00:00
Compare commits
9 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9a8b486f55 | ||
![]() |
481f59cf41 | ||
![]() |
7042102108 | ||
![]() |
a9672b8a33 | ||
![]() |
54861801e6 | ||
![]() |
6b33bcdbe4 | ||
![]() |
246b20d079 | ||
![]() |
672ec51d3e | ||
![]() |
ea5a5354fb |
7 changed files with 93 additions and 50 deletions
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
github: [laosb]
|
||||
buy_me_a_coffee: laosb
|
|
@ -26,5 +26,6 @@ let package = Package(
|
|||
.target(
|
||||
name: "CropImage",
|
||||
dependencies: [])
|
||||
]
|
||||
],
|
||||
swiftLanguageVersions: [.version("6"), .v5]
|
||||
)
|
||||
|
|
|
@ -19,7 +19,7 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
|
|||
/// - scale: The scale binding of the image.
|
||||
/// - rotation: The rotation binding of the image.
|
||||
/// - crop: An async function to trigger crop action. Result will be delivered via ``onCrop``.
|
||||
public typealias ControlClosure<Controls> = (
|
||||
public typealias ControlClosure = (
|
||||
_ offset: Binding<CGSize>,
|
||||
_ scale: Binding<CGFloat>,
|
||||
_ rotation: Binding<Angle>,
|
||||
|
@ -30,7 +30,7 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
|
|||
///
|
||||
/// - Parameters:
|
||||
/// - targetSize: The size of the cut hole.
|
||||
public typealias CutHoleClosure<CutHole> = (_ targetSize: CGSize) -> CutHole
|
||||
public typealias CutHoleClosure = (_ targetSize: CGSize) -> CutHole
|
||||
|
||||
/// Errors that could happen during the cropping process.
|
||||
public enum CropError: Error {
|
||||
|
@ -66,8 +66,8 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
|
|||
///
|
||||
/// The error should be a ``CropError``.
|
||||
public var onCrop: (Result<PlatformImage, Error>) -> Void
|
||||
var controls: ControlClosure<Controls>
|
||||
var cutHole: CutHoleClosure<CutHole>
|
||||
var controls: ControlClosure
|
||||
var cutHole: CutHoleClosure
|
||||
/// Create a ``CropImageView`` with a custom controls view and a custom cut hole.
|
||||
public init(
|
||||
image: PlatformImage,
|
||||
|
@ -75,8 +75,8 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
|
|||
targetScale: CGFloat = 1,
|
||||
fulfillTargetFrame: Bool = true,
|
||||
onCrop: @escaping (Result<PlatformImage, Error>) -> Void,
|
||||
@ViewBuilder controls: @escaping ControlClosure<Controls>,
|
||||
@ViewBuilder cutHole: @escaping CutHoleClosure<CutHole>
|
||||
@ViewBuilder controls: @escaping ControlClosure,
|
||||
@ViewBuilder cutHole: @escaping CutHoleClosure
|
||||
) {
|
||||
self.image = image
|
||||
self.targetSize = targetSize
|
||||
|
@ -92,7 +92,7 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
|
|||
targetScale: CGFloat = 1,
|
||||
fulfillTargetFrame: Bool = true,
|
||||
onCrop: @escaping (Result<PlatformImage, Error>) -> Void,
|
||||
@ViewBuilder controls: @escaping ControlClosure<Controls>
|
||||
@ViewBuilder controls: @escaping ControlClosure
|
||||
) where CutHole == DefaultCutHoleView {
|
||||
self.image = image
|
||||
self.targetSize = targetSize
|
||||
|
@ -225,15 +225,15 @@ public struct CropImageView<Controls: View, CutHole: View>: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct CropImageView_Previews: PreviewProvider {
|
||||
#Preview {
|
||||
struct PreviewView: View {
|
||||
@State private var targetSize: CGSize = .init(width: 100, height: 100)
|
||||
@State private var result: Result<PlatformImage, Error>? = nil
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
CropImageView(
|
||||
image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!,
|
||||
image: .previewImage,
|
||||
targetSize: targetSize
|
||||
) {
|
||||
result = $0
|
||||
|
@ -262,18 +262,16 @@ struct CropImageView_Previews: PreviewProvider {
|
|||
}
|
||||
} header: { Text("Result") }
|
||||
}
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
.formStyle(.grouped)
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
PreviewView()
|
||||
#if os(macOS)
|
||||
.frame(width: 500)
|
||||
.frame(minHeight: 600)
|
||||
#endif
|
||||
}
|
||||
|
||||
return PreviewView()
|
||||
#if os(macOS)
|
||||
.frame(width: 500)
|
||||
.frame(minHeight: 600)
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -44,20 +44,18 @@ struct DefaultCutHoleShape: Shape {
|
|||
}
|
||||
}
|
||||
|
||||
struct DefaultCutHoleShape_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
VStack {
|
||||
DefaultCutHoleShape(size: .init(width: 100, height: 100))
|
||||
.fill(style: FillStyle(eoFill: true))
|
||||
.foregroundColor(.black.opacity(0.6))
|
||||
}
|
||||
.previewDisplayName("Default")
|
||||
VStack {
|
||||
DefaultCutHoleShape(size: .init(width: 100, height: 100), isCircular: true)
|
||||
.fill(style: FillStyle(eoFill: true))
|
||||
.foregroundColor(.black.opacity(0.6))
|
||||
}
|
||||
.previewDisplayName("Circular")
|
||||
#Preview("Default") {
|
||||
VStack {
|
||||
DefaultCutHoleShape(size: .init(width: 100, height: 100))
|
||||
.fill(style: FillStyle(eoFill: true))
|
||||
.foregroundColor(.black.opacity(0.6))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Circular") {
|
||||
VStack {
|
||||
DefaultCutHoleShape(size: .init(width: 100, height: 100), isCircular: true)
|
||||
.fill(style: FillStyle(eoFill: true))
|
||||
.foregroundColor(.black.opacity(0.6))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,11 +61,10 @@ public struct DefaultCutHoleView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct DefaultCutHoleView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DefaultCutHoleView(targetSize: .init(width: 100, height: 100))
|
||||
.previewDisplayName("Default")
|
||||
DefaultCutHoleView(targetSize: .init(width: 100, height: 100), isCircular: true)
|
||||
.previewDisplayName("Circular")
|
||||
}
|
||||
#Preview("Default") {
|
||||
DefaultCutHoleView(targetSize: .init(width: 100, height: 100))
|
||||
}
|
||||
|
||||
#Preview("Circular") {
|
||||
DefaultCutHoleView(targetSize: .init(width: 100, height: 100), isCircular: true)
|
||||
}
|
||||
|
|
|
@ -13,10 +13,17 @@ import AppKit
|
|||
///
|
||||
/// On macOS, it's `NSImage` and on iOS/visionOS it's `UIImage`.
|
||||
public typealias PlatformImage = NSImage
|
||||
extension PlatformImage {
|
||||
@MainActor static let previewImage: PlatformImage = .init(contentsOf: URL(string: "file:///System/Library/Desktop%20Pictures/Hello%20Metallic%20Blue.heic")!)!
|
||||
}
|
||||
#else
|
||||
import UIKit
|
||||
/// The image object type, aliased to each platform.
|
||||
///
|
||||
/// On macOS, it's `NSImage` and on iOS/visionOS it's `UIImage`.
|
||||
public typealias PlatformImage = UIImage
|
||||
extension PlatformImage {
|
||||
// This doesn't really work, but at least passes build.
|
||||
static let previewImage: PlatformImage = .init(contentsOfFile: "/System/Library/Desktop Pictures/Hello Metallic Blue.heic")!
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -25,6 +25,11 @@ struct UnderlyingImageView: View {
|
|||
@State private var tempOffset: CGSize = .zero
|
||||
@State private var tempScale: CGFloat = 1
|
||||
@State private var tempRotation: Angle = .zero
|
||||
@State private var scrolling: Bool = false
|
||||
#if os(macOS)
|
||||
@State private var hovering: Bool = false
|
||||
@State private var scrollMonitor: Any?
|
||||
#endif
|
||||
|
||||
// When rotated odd multiples of 90 degrees, we need to switch width and height of the image in calculations.
|
||||
var isRotatedOddMultiplesOf90Deg: Bool {
|
||||
|
@ -66,9 +71,15 @@ struct UnderlyingImageView: View {
|
|||
clampedOffset.height = clampedOffset.height.clamped(to: yOffsetBounds(at: clampedScale))
|
||||
|
||||
if clampedScale != scale || clampedOffset != offset {
|
||||
withAnimation(.interactiveSpring()) {
|
||||
if scrolling {
|
||||
scale = clampedScale
|
||||
offset = clampedOffset
|
||||
scrolling = false
|
||||
} else {
|
||||
withAnimation(.interactiveSpring()) {
|
||||
scale = clampedScale
|
||||
offset = clampedOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +92,24 @@ struct UnderlyingImageView: View {
|
|||
scale = min(widthScale, heightScale)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private func setupScrollMonitor() {
|
||||
scrollMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { event in
|
||||
if hovering {
|
||||
scrolling = true
|
||||
scale = scale + event.scrollingDeltaY / 1000
|
||||
}
|
||||
return event
|
||||
}
|
||||
}
|
||||
|
||||
private func removeScrollMonitor() {
|
||||
if let scrollMonitor {
|
||||
NSEvent.removeMonitor(scrollMonitor)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
var imageView: Image {
|
||||
#if os(macOS)
|
||||
Image(nsImage: image)
|
||||
|
@ -94,6 +123,14 @@ struct UnderlyingImageView: View {
|
|||
.gesture(dragGesture)
|
||||
.gesture(magnificationgesture)
|
||||
.gesture(rotationGesture)
|
||||
#if os(macOS)
|
||||
.onAppear {
|
||||
setupScrollMonitor()
|
||||
}
|
||||
.onDisappear {
|
||||
removeScrollMonitor()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var dragGesture: some Gesture {
|
||||
|
@ -148,21 +185,24 @@ struct UnderlyingImageView: View {
|
|||
.onChange(of: rotation) { _ in
|
||||
adjustToFulfillTargetFrame()
|
||||
}
|
||||
#if os(macOS)
|
||||
.onHover { hovering = $0 }
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
struct MoveAndScalableImageView_Previews: PreviewProvider {
|
||||
#Preview {
|
||||
struct PreviewView: View {
|
||||
@State private var offset: CGSize = .zero
|
||||
@State private var scale: CGFloat = 1
|
||||
@State private var rotation: Angle = .zero
|
||||
|
||||
|
||||
var body: some View {
|
||||
UnderlyingImageView(
|
||||
offset: $offset,
|
||||
scale: $scale,
|
||||
rotation: $rotation,
|
||||
image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!,
|
||||
image: .previewImage,
|
||||
viewSize: .init(width: 200, height: 100),
|
||||
targetSize: .init(width: 100, height: 100),
|
||||
fulfillTargetFrame: true
|
||||
|
@ -170,8 +210,6 @@ struct MoveAndScalableImageView_Previews: PreviewProvider {
|
|||
.frame(width: 200, height: 100)
|
||||
}
|
||||
}
|
||||
|
||||
static var previews: some View {
|
||||
PreviewView()
|
||||
}
|
||||
|
||||
return PreviewView()
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue