From 63a60f802ae61c2b63aec56c612126eb810c4a18 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Wed, 16 Aug 2023 17:03:44 +0800 Subject: [PATCH 01/20] doc: Mention DocC doc in README. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 821c04f..c9f0fe7 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ Supports iOS 14.0 and above, or macOS Ventura 13.0 and above. - Very lightweight - (Optionally) bring your own crop UI +Full documentation is available on [Swift Package Index](https://swiftpackageindex.com/laosb/CropImage/main/documentation/cropimage). Be sure to choose the correct version. + Preview on macOS From e703e25200725ba8383f1fd5a98a0e2899b4ca12 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Fri, 1 Sep 2023 11:32:29 +0800 Subject: [PATCH 02/20] fix: Dragging after rotation(#1), adjust to fill after rotation, animation. --- Sources/CropImage/DefaultControlsView.swift | 2 +- Sources/CropImage/UnderlyingImageView.swift | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Sources/CropImage/DefaultControlsView.swift b/Sources/CropImage/DefaultControlsView.swift index ff35908..4b69e89 100644 --- a/Sources/CropImage/DefaultControlsView.swift +++ b/Sources/CropImage/DefaultControlsView.swift @@ -19,7 +19,7 @@ public struct DefaultControlsView: View { var rotateButton: some View { Button { let roundedAngle = Angle.degrees((rotation.degrees / 90).rounded() * 90) - withAnimation { + withAnimation(.interactiveSpring()) { rotation = roundedAngle + .degrees(90) } } label: { diff --git a/Sources/CropImage/UnderlyingImageView.swift b/Sources/CropImage/UnderlyingImageView.swift index 346c8eb..7166bf4 100644 --- a/Sources/CropImage/UnderlyingImageView.swift +++ b/Sources/CropImage/UnderlyingImageView.swift @@ -66,7 +66,7 @@ struct UnderlyingImageView: View { clampedOffset.height = clampedOffset.height.clamped(to: yOffsetBounds(at: clampedScale)) if clampedScale != scale || clampedOffset != offset { - withAnimation { + withAnimation(.interactiveSpring()) { scale = clampedScale offset = clampedOffset } @@ -104,7 +104,6 @@ struct UnderlyingImageView: View { .onEnded { value in offset = offset + tempOffset tempOffset = .zero - adjustToFulfillTargetFrame() } } @@ -116,7 +115,6 @@ struct UnderlyingImageView: View { .onEnded { value in scale = scale * tempScale tempScale = 1 - adjustToFulfillTargetFrame() } } @@ -133,14 +131,23 @@ struct UnderlyingImageView: View { var body: some View { imageView + .rotationEffect(rotation + tempRotation) .scaleEffect(scale * tempScale) .offset(offset + tempOffset) - .rotationEffect(rotation + tempRotation) .overlay(interactionView) + .clipped() .onChange(of: viewSize) { newValue in setInitialScale(basedOn: newValue) } - .clipped() + .onChange(of: scale) { _ in + adjustToFulfillTargetFrame() + } + .onChange(of: offset) { _ in + adjustToFulfillTargetFrame() + } + .onChange(of: rotation) { _ in + adjustToFulfillTargetFrame() + } } } From e4d096dafba5521d2daba1b1071166b12d86e999 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Thu, 7 Sep 2023 15:02:12 +0800 Subject: [PATCH 03/20] feat: Custom cut hole. --- Sources/CropImage/CropImageView.swift | 62 +++++++++++++++------ Sources/CropImage/DefaultCutHoleShape.swift | 20 +++++-- Sources/CropImage/DefaultCutHoleView.swift | 33 ++++++++--- 3 files changed, 86 insertions(+), 29 deletions(-) diff --git a/Sources/CropImage/CropImageView.swift b/Sources/CropImage/CropImageView.swift index 8a6376e..adc0777 100644 --- a/Sources/CropImage/CropImageView.swift +++ b/Sources/CropImage/CropImageView.swift @@ -11,7 +11,14 @@ import UIKit #endif /// A view that allows the user to crop an image. -public struct CropImageView: View { +public struct CropImageView: View { + /// Defines a custom view overlaid on the image cropper. + /// + /// - Parameters: + /// - offset: The offset binding of the image. + /// - 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 = ( _ offset: Binding, _ scale: Binding, @@ -19,6 +26,12 @@ public struct CropImageView: View { _ crop: @escaping () async -> () ) -> Controls + /// Defines custom view that indicates the cut hole to users. + /// + /// - Parameters: + /// - targetSize: The size of the cut hole. + public typealias CutHoleClosure = (_ targetSize: CGSize) -> CutHole + /// Errors that could happen during the cropping process. public enum CropError: Error { /// SwiftUI `ImageRenderer` returned nil when calling `nsImage` or `uiImage`. @@ -53,13 +66,26 @@ public struct CropImageView: View { /// /// The error should be a ``CropError``. public var onCrop: (Result) -> Void - /// A custom view overlaid on the image cropper. - /// - /// - Parameters: - /// - crop: An async function to trigger crop action. Result will be delivered via ``onCrop``. - public var controls: ControlClosure - - /// Create a ``CropImageView`` with a custom ``controls`` view. + var controls: ControlClosure + var cutHole: CutHoleClosure + /// Create a ``CropImageView`` with a custom controls view and a custom cut hole. + public init( + image: PlatformImage, + targetSize: CGSize, + targetScale: CGFloat = 1, + fulfillTargetFrame: Bool = true, + onCrop: @escaping (Result) -> Void, + @ViewBuilder controls: @escaping ControlClosure, + @ViewBuilder cutHole: @escaping CutHoleClosure + ) { + self.image = image + self.targetSize = targetSize + self.targetScale = targetScale + self.onCrop = onCrop + self.controls = controls + self.cutHole = cutHole + } + /// Create a ``CropImageView`` with a custom controls view and default cut hole. public init( image: PlatformImage, targetSize: CGSize, @@ -67,23 +93,24 @@ public struct CropImageView: View { fulfillTargetFrame: Bool = true, onCrop: @escaping (Result) -> Void, @ViewBuilder controls: @escaping ControlClosure - ) { + ) where CutHole == DefaultCutHoleView { self.image = image self.targetSize = targetSize self.targetScale = targetScale self.onCrop = onCrop self.controls = controls + self.cutHole = { targetSize in + DefaultCutHoleView(targetSize: targetSize) + } } - /// Create a ``CropImageView`` with the default ``controls`` view. - /// - /// The default ``controls`` view is a simple overlay with a checkmark icon on the bottom-trailing corner to trigger crop action. + /// Create a ``CropImageView`` with default UI elements. public init( image: PlatformImage, targetSize: CGSize, targetScale: CGFloat = 1, fulfillTargetFrame: Bool = true, onCrop: @escaping (Result) -> Void - ) where Controls == DefaultControlsView { + ) where Controls == DefaultControlsView, CutHole == DefaultCutHoleView { self.image = image self.targetSize = targetSize self.targetScale = targetScale @@ -91,6 +118,9 @@ public struct CropImageView: View { self.controls = { $offset, $scale, $rotation, crop in DefaultControlsView(offset: $offset, scale: $scale, rotation: $rotation, crop: crop) } + self.cutHole = { targetSize in + DefaultCutHoleView(targetSize: targetSize) + } } @State private var offset: CGSize = .zero @@ -164,10 +194,6 @@ public struct CropImageView: View { .clipped() } - var cutHole: some View { - DefaultCutHoleView(targetSize: targetSize) - } - var viewSizeReadingView: some View { GeometryReader { geo in Rectangle() @@ -192,7 +218,7 @@ public struct CropImageView: View { } public var body: some View { - cutHole + cutHole(targetSize) .background(underlyingImage) .background(viewSizeReadingView) .overlay(control) diff --git a/Sources/CropImage/DefaultCutHoleShape.swift b/Sources/CropImage/DefaultCutHoleShape.swift index 818324c..3a8c93e 100644 --- a/Sources/CropImage/DefaultCutHoleShape.swift +++ b/Sources/CropImage/DefaultCutHoleShape.swift @@ -9,6 +9,7 @@ import SwiftUI struct DefaultCutHoleShape: Shape { var size: CGSize + var isCircular = false var animatableData: AnimatablePair { get { .init(size.width, size.height) } @@ -30,10 +31,14 @@ struct DefaultCutHoleShape: Shape { ), size: size) path.move(to: newRect.origin) - path.addLine(to: .init(x: newRect.maxX, y: newRect.minY)) - path.addLine(to: .init(x: newRect.maxX, y: newRect.maxY)) - path.addLine(to: .init(x: newRect.minX, y: newRect.maxY)) - path.addLine(to: newRect.origin) + if isCircular { + path.addEllipse(in: newRect) + } else { + path.addLine(to: .init(x: newRect.maxX, y: newRect.minY)) + path.addLine(to: .init(x: newRect.maxX, y: newRect.maxY)) + path.addLine(to: .init(x: newRect.minX, y: newRect.maxY)) + path.addLine(to: newRect.origin) + } path.closeSubpath() return Path(path) } @@ -46,6 +51,13 @@ struct DefaultCutHoleShape_Previews: PreviewProvider { .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") } } diff --git a/Sources/CropImage/DefaultCutHoleView.swift b/Sources/CropImage/DefaultCutHoleView.swift index 86742a8..e7a756f 100644 --- a/Sources/CropImage/DefaultCutHoleView.swift +++ b/Sources/CropImage/DefaultCutHoleView.swift @@ -7,27 +7,46 @@ import SwiftUI -struct DefaultCutHoleView: View { +/// The default cut hole view. Stroke and mask color can be adjusted. +public struct DefaultCutHoleView: View { var targetSize: CGSize - var showStroke = true + var strokeWidth: CGFloat + var maskColor: Color + var isCircular: Bool + + /// Initialize a default rectangular or circular cut hole view with specified target size, stroke width and mask color. + public init( + targetSize: CGSize, + isCircular: Bool = false, + strokeWidth: CGFloat = 1, + maskColor: Color = .black.opacity(0.6) + ) { + self.targetSize = targetSize + self.strokeWidth = strokeWidth + self.maskColor = maskColor + self.isCircular = isCircular + } var background: some View { DefaultCutHoleShape(size: targetSize) .fill(style: FillStyle(eoFill: true)) - .foregroundColor(.black.opacity(0.6)) + .foregroundColor(maskColor) } var stroke: some View { Rectangle() - .strokeBorder(style: .init(lineWidth: 1)) - .frame(width: targetSize.width + 2, height: targetSize.height + 2) + .strokeBorder(style: .init(lineWidth: strokeWidth)) + .frame( + width: targetSize.width + strokeWidth * 2, + height: targetSize.height + strokeWidth * 2 + ) .foregroundColor(.white) } - var body: some View { + public var body: some View { background .allowsHitTesting(false) - .overlay(showStroke ? stroke : nil) + .overlay(strokeWidth > 0 ? stroke : nil) .animation(.default, value: targetSize) } } From 030ac8cbdeb99358d838d7ad49c6f0661e6dd924 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Thu, 7 Sep 2023 15:52:24 +0800 Subject: [PATCH 04/20] fix: Pass isCircular to DefaultCutHoleShape. --- Sources/CropImage/DefaultCutHoleView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CropImage/DefaultCutHoleView.swift b/Sources/CropImage/DefaultCutHoleView.swift index e7a756f..4ff9807 100644 --- a/Sources/CropImage/DefaultCutHoleView.swift +++ b/Sources/CropImage/DefaultCutHoleView.swift @@ -28,7 +28,7 @@ public struct DefaultCutHoleView: View { } var background: some View { - DefaultCutHoleShape(size: targetSize) + DefaultCutHoleShape(size: targetSize, isCircular: isCircular) .fill(style: FillStyle(eoFill: true)) .foregroundColor(maskColor) } From c296f3e6e5e832d8d7770b5593ddd9eb0f6ee9e5 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Thu, 7 Sep 2023 16:01:28 +0800 Subject: [PATCH 05/20] fix: Circular cut hole. --- Sources/CropImage/DefaultCutHoleView.swift | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Sources/CropImage/DefaultCutHoleView.swift b/Sources/CropImage/DefaultCutHoleView.swift index 4ff9807..9323e7d 100644 --- a/Sources/CropImage/DefaultCutHoleView.swift +++ b/Sources/CropImage/DefaultCutHoleView.swift @@ -33,9 +33,19 @@ public struct DefaultCutHoleView: View { .foregroundColor(maskColor) } + @ViewBuilder + var strokeShape: some View { + if isCircular { + Circle() + .strokeBorder(style: .init(lineWidth: strokeWidth)) + } else { + Rectangle() + .strokeBorder(style: .init(lineWidth: strokeWidth)) + } + } + var stroke: some View { - Rectangle() - .strokeBorder(style: .init(lineWidth: strokeWidth)) + strokeShape .frame( width: targetSize.width + strokeWidth * 2, height: targetSize.height + strokeWidth * 2 @@ -54,5 +64,8 @@ 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") } } From 1d66f10bff092fe574fb44b62c34688b0919a3a9 Mon Sep 17 00:00:00 2001 From: infinitepower18 <44692189+infinitepower18@users.noreply.github.com> Date: Tue, 19 Mar 2024 01:04:12 +0000 Subject: [PATCH 06/20] add visionos support --- Package.swift | 5 +++-- Sources/CropImage/CropImageView.swift | 10 +++++----- Sources/CropImage/PlatformImage.swift | 2 +- Sources/CropImage/UnderlyingImageView.swift | 2 +- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Package.swift b/Package.swift index 948f859..f88e851 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 5.10 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -7,7 +7,8 @@ let package = Package( name: "CropImage", platforms: [ .iOS(.v14), - .macOS(.v13) + .macOS(.v13), + .visionOS(.v1) ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. diff --git a/Sources/CropImage/CropImageView.swift b/Sources/CropImage/CropImageView.swift index adc0777..c58cdd8 100644 --- a/Sources/CropImage/CropImageView.swift +++ b/Sources/CropImage/CropImageView.swift @@ -6,7 +6,7 @@ // import SwiftUI -#if os(iOS) +#if !os(macOS) import UIKit #endif @@ -144,13 +144,13 @@ public struct CropImageView: View { if #available(iOS 16.0, macOS 13.0, *) { let renderer = ImageRenderer(content: snapshotView) renderer.scale = targetScale -#if os(iOS) +#if !os(macOS) if let image = renderer.uiImage { return image } else { throw CropError.imageRendererReturnedNil } -#elseif os(macOS) +#else if let image = renderer.nsImage { return image } else { @@ -160,7 +160,7 @@ public struct CropImageView: View { } else { #if os(macOS) fatalError("Cropping is not supported on macOS versions before Ventura 13.0.") -#elseif os(iOS) +#else let window = UIWindow(frame: CGRect(origin: .zero, size: targetSize)) let hosting = UIHostingController(rootView: snapshotView) hosting.view.frame = window.frame @@ -250,7 +250,7 @@ struct CropImageView_Previews: PreviewProvider { case let .success(croppedImage): #if os(macOS) Image(nsImage: croppedImage) -#elseif os(iOS) +#else Image(uiImage: croppedImage) #endif case let .failure(error): diff --git a/Sources/CropImage/PlatformImage.swift b/Sources/CropImage/PlatformImage.swift index 617ea5f..7f45b90 100644 --- a/Sources/CropImage/PlatformImage.swift +++ b/Sources/CropImage/PlatformImage.swift @@ -13,7 +13,7 @@ import AppKit /// /// On macOS, it's `NSImage` and on iOS it's `UIImage`. public typealias PlatformImage = NSImage -#elseif os(iOS) +#else import UIKit /// The image object type, aliased to each platform. /// diff --git a/Sources/CropImage/UnderlyingImageView.swift b/Sources/CropImage/UnderlyingImageView.swift index 7166bf4..e49de8a 100644 --- a/Sources/CropImage/UnderlyingImageView.swift +++ b/Sources/CropImage/UnderlyingImageView.swift @@ -84,7 +84,7 @@ struct UnderlyingImageView: View { var imageView: Image { #if os(macOS) Image(nsImage: image) -#elseif os(iOS) +#else Image(uiImage: image) #endif } From a74e54dd00f5168c07b86b71c85d5add21422a75 Mon Sep 17 00:00:00 2001 From: infinitepower18 <44692189+infinitepower18@users.noreply.github.com> Date: Tue, 19 Mar 2024 01:10:19 +0000 Subject: [PATCH 07/20] update docs --- README.md | 4 ++-- Sources/CropImage/Documentation.docc/Documentation.md | 4 ++-- Sources/CropImage/PlatformImage.swift | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index c9f0fe7..dd57d6c 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,9 @@ A simple SwiftUI view where user can move and resize an image to a pre-defined size. -Supports iOS 14.0 and above, or macOS Ventura 13.0 and above. +Supports iOS 14.0 and above, visionOS 1.0 and above or macOS Ventura 13.0 and above. -- Supports both iOS and macOS +- Supports iOS, visionOS and macOS - Use `ImageRenderer` to render the cropped image, when possible - Very lightweight - (Optionally) bring your own crop UI diff --git a/Sources/CropImage/Documentation.docc/Documentation.md b/Sources/CropImage/Documentation.docc/Documentation.md index 4197e07..f6a8eea 100644 --- a/Sources/CropImage/Documentation.docc/Documentation.md +++ b/Sources/CropImage/Documentation.docc/Documentation.md @@ -2,9 +2,9 @@ A simple SwiftUI view where user can move and resize an image to a pre-defined size. -Supports iOS 14.0 and above, or macOS Ventura 13.0 and above. +Supports iOS 14.0 and above, visionOS 1.0 and above or macOS Ventura 13.0 and above. -- Supports both iOS and macOS +- Supports iOS, visionOS and macOS - Use `ImageRenderer` to render the cropped image, when possible - Very lightweight - (Optionally) bring your own crop UI diff --git a/Sources/CropImage/PlatformImage.swift b/Sources/CropImage/PlatformImage.swift index 7f45b90..831e93b 100644 --- a/Sources/CropImage/PlatformImage.swift +++ b/Sources/CropImage/PlatformImage.swift @@ -11,12 +11,12 @@ import Foundation import AppKit /// The image object type, aliased to each platform. /// -/// On macOS, it's `NSImage` and on iOS it's `UIImage`. +/// On macOS, it's `NSImage` and on iOS/visionOS it's `UIImage`. public typealias PlatformImage = NSImage #else import UIKit /// The image object type, aliased to each platform. /// -/// On macOS, it's `NSImage` and on iOS it's `UIImage`. +/// On macOS, it's `NSImage` and on iOS/visionOS it's `UIImage`. public typealias PlatformImage = UIImage #endif From 57ff1a7b864c4ee4a84217deb0fb3fcf07b66b75 Mon Sep 17 00:00:00 2001 From: infinitepower18 <44692189+infinitepower18@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:22:33 +0000 Subject: [PATCH 08/20] Adjust button style for visionOS --- Sources/CropImage/DefaultControlsView.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Sources/CropImage/DefaultControlsView.swift b/Sources/CropImage/DefaultControlsView.swift index 4b69e89..67c0b8a 100644 --- a/Sources/CropImage/DefaultControlsView.swift +++ b/Sources/CropImage/DefaultControlsView.swift @@ -29,12 +29,16 @@ public struct DefaultControlsView: View { .labelStyle(.iconOnly) .padding(.horizontal, 6) .padding(.vertical, 3) + #if !os(visionOS) .background( RoundedRectangle(cornerRadius: 5, style: .continuous) .fill(.background) ) + #endif } + #if !os(visionOS) .buttonStyle(.plain) + #endif .padding() } @@ -57,11 +61,15 @@ public struct DefaultControlsView: View { .foregroundColor(.accentColor) .labelStyle(.iconOnly) .padding(1) + #if !os(visionOS) .background( Circle().fill(.background) ) + #endif } + #if !os(visionOS) .buttonStyle(.plain) + #endif .padding() } From 4e0f333e2f9de73b2439f83ad46e01663737b756 Mon Sep 17 00:00:00 2001 From: infinitepower18 <44692189+infinitepower18@users.noreply.github.com> Date: Tue, 19 Mar 2024 17:27:13 +0000 Subject: [PATCH 09/20] Don't use accent colour on visionOS --- Sources/CropImage/DefaultControlsView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Sources/CropImage/DefaultControlsView.swift b/Sources/CropImage/DefaultControlsView.swift index 67c0b8a..a49da93 100644 --- a/Sources/CropImage/DefaultControlsView.swift +++ b/Sources/CropImage/DefaultControlsView.swift @@ -25,7 +25,9 @@ public struct DefaultControlsView: View { } label: { Label("Rotate", systemImage: "rotate.right") .font(.title2) + #if !os(visionOS) .foregroundColor(.accentColor) + #endif .labelStyle(.iconOnly) .padding(.horizontal, 6) .padding(.vertical, 3) @@ -58,7 +60,9 @@ public struct DefaultControlsView: View { } } label: { Label("Crop", systemImage: "checkmark.circle.fill") .font(.title2) + #if !os(visionOS) .foregroundColor(.accentColor) + #endif .labelStyle(.iconOnly) .padding(1) #if !os(visionOS) From 517fb0c00352366bf6c518c3402afcbcd62557ca Mon Sep 17 00:00:00 2001 From: Ahnaf Mahmud <44692189+infinitepower18@users.noreply.github.com> Date: Fri, 22 Mar 2024 02:25:39 +0000 Subject: [PATCH 10/20] Update Sources/CropImage/CropImageView.swift Co-authored-by: Shibo Lyu --- Sources/CropImage/CropImageView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CropImage/CropImageView.swift b/Sources/CropImage/CropImageView.swift index c58cdd8..f47e08c 100644 --- a/Sources/CropImage/CropImageView.swift +++ b/Sources/CropImage/CropImageView.swift @@ -141,7 +141,7 @@ public struct CropImageView: View { fulfillTargetFrame: fulfillTargetFrame ) .frame(width: targetSize.width, height: targetSize.height) - if #available(iOS 16.0, macOS 13.0, *) { + if #available(iOS 16.0, macOS 13.0, visionOS 1.0, *) { let renderer = ImageRenderer(content: snapshotView) renderer.scale = targetScale #if !os(macOS) From 304a4c8e7e4505884fac863b762f8bf993710f2b Mon Sep 17 00:00:00 2001 From: infinitepower18 <44692189+infinitepower18@users.noreply.github.com> Date: Tue, 26 Mar 2024 17:11:06 +0000 Subject: [PATCH 11/20] Remove back compat code from visionOS --- Sources/CropImage/CropImageView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CropImage/CropImageView.swift b/Sources/CropImage/CropImageView.swift index f47e08c..2d6d558 100644 --- a/Sources/CropImage/CropImageView.swift +++ b/Sources/CropImage/CropImageView.swift @@ -160,7 +160,7 @@ public struct CropImageView: View { } else { #if os(macOS) fatalError("Cropping is not supported on macOS versions before Ventura 13.0.") -#else +#elseif os(iOS) let window = UIWindow(frame: CGRect(origin: .zero, size: targetSize)) let hosting = UIHostingController(rootView: snapshotView) hosting.view.frame = window.frame From ea5a5354fbd19fedca71826bc9dd2781426a16b5 Mon Sep 17 00:00:00 2001 From: Ahnaf Mahmud <44692189+infinitepower18@users.noreply.github.com> Date: Tue, 13 Aug 2024 20:30:21 +0100 Subject: [PATCH 12/20] Allow zoom using scroll wheel --- Sources/CropImage/UnderlyingImageView.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Sources/CropImage/UnderlyingImageView.swift b/Sources/CropImage/UnderlyingImageView.swift index e49de8a..382b290 100644 --- a/Sources/CropImage/UnderlyingImageView.swift +++ b/Sources/CropImage/UnderlyingImageView.swift @@ -81,6 +81,15 @@ struct UnderlyingImageView: View { scale = min(widthScale, heightScale) } + private func setupScrollMonitor() { + #if os(macOS) + NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) {event in + scale = scale + event.scrollingDeltaY/1000 + return event + } + #endif + } + var imageView: Image { #if os(macOS) Image(nsImage: image) @@ -94,6 +103,9 @@ struct UnderlyingImageView: View { .gesture(dragGesture) .gesture(magnificationgesture) .gesture(rotationGesture) + .onAppear { + setupScrollMonitor() + } } var dragGesture: some Gesture { From 672ec51d3ee1dc6c8a9c62ce318a9004da192dd9 Mon Sep 17 00:00:00 2001 From: infinitepower18 <44692189+infinitepower18@users.noreply.github.com> Date: Tue, 13 Aug 2024 21:02:07 +0100 Subject: [PATCH 13/20] only allow when hovered --- Sources/CropImage/UnderlyingImageView.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/CropImage/UnderlyingImageView.swift b/Sources/CropImage/UnderlyingImageView.swift index 382b290..2c87225 100644 --- a/Sources/CropImage/UnderlyingImageView.swift +++ b/Sources/CropImage/UnderlyingImageView.swift @@ -25,6 +25,9 @@ struct UnderlyingImageView: View { @State private var tempOffset: CGSize = .zero @State private var tempScale: CGFloat = 1 @State private var tempRotation: Angle = .zero + #if os(macOS) + @State private var isHovering: Bool = false + #endif // When rotated odd multiples of 90 degrees, we need to switch width and height of the image in calculations. var isRotatedOddMultiplesOf90Deg: Bool { @@ -84,7 +87,9 @@ struct UnderlyingImageView: View { private func setupScrollMonitor() { #if os(macOS) NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) {event in - scale = scale + event.scrollingDeltaY/1000 + if isHovering { + scale = scale + event.scrollingDeltaY/1000 + } return event } #endif @@ -160,6 +165,11 @@ struct UnderlyingImageView: View { .onChange(of: rotation) { _ in adjustToFulfillTargetFrame() } + #if os(macOS) + .onHover { hovering in + isHovering = hovering + } + #endif } } From 246b20d07998f7bbe60f8cf7f22944a57dc915bc Mon Sep 17 00:00:00 2001 From: infinitepower18 <44692189+infinitepower18@users.noreply.github.com> Date: Wed, 14 Aug 2024 22:24:08 +0100 Subject: [PATCH 14/20] Disable spring animation for scroll zoom due to performance issue --- Sources/CropImage/UnderlyingImageView.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/CropImage/UnderlyingImageView.swift b/Sources/CropImage/UnderlyingImageView.swift index 2c87225..6d7ab11 100644 --- a/Sources/CropImage/UnderlyingImageView.swift +++ b/Sources/CropImage/UnderlyingImageView.swift @@ -25,6 +25,7 @@ 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 isHovering: Bool = false #endif @@ -69,9 +70,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 + } } } } @@ -88,6 +95,7 @@ struct UnderlyingImageView: View { #if os(macOS) NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) {event in if isHovering { + scrolling = true scale = scale + event.scrollingDeltaY/1000 } return event From 6b33bcdbe4c1caf2cebc1cc2ff526e903a5f830b Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Wed, 18 Dec 2024 15:49:02 +0800 Subject: [PATCH 15/20] fix(macos): remove monitor after use --- Sources/CropImage/UnderlyingImageView.swift | 36 +++++++++++++-------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Sources/CropImage/UnderlyingImageView.swift b/Sources/CropImage/UnderlyingImageView.swift index 6d7ab11..29f8543 100644 --- a/Sources/CropImage/UnderlyingImageView.swift +++ b/Sources/CropImage/UnderlyingImageView.swift @@ -26,9 +26,10 @@ struct UnderlyingImageView: View { @State private var tempScale: CGFloat = 1 @State private var tempRotation: Angle = .zero @State private var scrolling: Bool = false - #if os(macOS) - @State private var isHovering: Bool = false - #endif +#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 { @@ -91,17 +92,23 @@ struct UnderlyingImageView: View { scale = min(widthScale, heightScale) } +#if os(macOS) private func setupScrollMonitor() { - #if os(macOS) - NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) {event in - if isHovering { + scrollMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { event in + if hovering { scrolling = true - scale = scale + event.scrollingDeltaY/1000 + scale = scale + event.scrollingDeltaY / 1000 } return event } - #endif } + + private func removeScrollMonitor() { + if let scrollMonitor { + NSEvent.removeMonitor(scrollMonitor) + } + } +#endif var imageView: Image { #if os(macOS) @@ -116,9 +123,14 @@ struct UnderlyingImageView: View { .gesture(dragGesture) .gesture(magnificationgesture) .gesture(rotationGesture) +#if os(macOS) .onAppear { setupScrollMonitor() } + .onDisappear { + removeScrollMonitor() + } +#endif } var dragGesture: some Gesture { @@ -173,11 +185,9 @@ struct UnderlyingImageView: View { .onChange(of: rotation) { _ in adjustToFulfillTargetFrame() } - #if os(macOS) - .onHover { hovering in - isHovering = hovering - } - #endif +#if os(macOS) + .onHover { hovering = $0 } +#endif } } From 54861801e6530cd9b82c6263ca5eeeb099979c44 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Wed, 18 Dec 2024 16:06:40 +0800 Subject: [PATCH 16/20] chore: convert to preview macro --- Sources/CropImage/CropImageView.swift | 24 +++++++++---------- Sources/CropImage/DefaultCutHoleShape.swift | 26 ++++++++++----------- Sources/CropImage/DefaultCutHoleView.swift | 13 +++++------ Sources/CropImage/UnderlyingImageView.swift | 12 ++++------ 4 files changed, 34 insertions(+), 41 deletions(-) diff --git a/Sources/CropImage/CropImageView.swift b/Sources/CropImage/CropImageView.swift index 2d6d558..88e523f 100644 --- a/Sources/CropImage/CropImageView.swift +++ b/Sources/CropImage/CropImageView.swift @@ -225,15 +225,15 @@ public struct CropImageView: View { } } -struct CropImageView_Previews: PreviewProvider { +#Preview { struct PreviewView: View { @State private var targetSize: CGSize = .init(width: 100, height: 100) @State private var result: Result? = nil - + var body: some View { VStack { CropImageView( - image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!, + image: .init(contentsOf: URL(string: "file:///System/Library/Desktop%20Pictures/Hello%20Metallic%20Blue.heic")!)!, 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 } diff --git a/Sources/CropImage/DefaultCutHoleShape.swift b/Sources/CropImage/DefaultCutHoleShape.swift index 3a8c93e..9b55369 100644 --- a/Sources/CropImage/DefaultCutHoleShape.swift +++ b/Sources/CropImage/DefaultCutHoleShape.swift @@ -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)) + } +} diff --git a/Sources/CropImage/DefaultCutHoleView.swift b/Sources/CropImage/DefaultCutHoleView.swift index 9323e7d..e9e8664 100644 --- a/Sources/CropImage/DefaultCutHoleView.swift +++ b/Sources/CropImage/DefaultCutHoleView.swift @@ -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) } diff --git a/Sources/CropImage/UnderlyingImageView.swift b/Sources/CropImage/UnderlyingImageView.swift index 29f8543..9d1ee3b 100644 --- a/Sources/CropImage/UnderlyingImageView.swift +++ b/Sources/CropImage/UnderlyingImageView.swift @@ -191,18 +191,18 @@ struct UnderlyingImageView: View { } } -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: .init(contentsOf: URL(string: "file:///System/Library/Desktop%20Pictures/Hello%20Metallic%20Blue.heic")!)!, viewSize: .init(width: 200, height: 100), targetSize: .init(width: 100, height: 100), fulfillTargetFrame: true @@ -210,8 +210,6 @@ struct MoveAndScalableImageView_Previews: PreviewProvider { .frame(width: 200, height: 100) } } - - static var previews: some View { - PreviewView() - } + + return PreviewView() } From a9672b8a33ed7b28993cae0e749b7b5964a0ee19 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Wed, 18 Dec 2024 16:11:02 +0800 Subject: [PATCH 17/20] feat: supports Swift 6 --- Package.swift | 3 ++- Sources/CropImage/CropImageView.swift | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Package.swift b/Package.swift index f88e851..5fa902a 100644 --- a/Package.swift +++ b/Package.swift @@ -26,5 +26,6 @@ let package = Package( .target( name: "CropImage", dependencies: []) - ] + ], + swiftLanguageVersions: [.version("6"), .v5] ) diff --git a/Sources/CropImage/CropImageView.swift b/Sources/CropImage/CropImageView.swift index 88e523f..4bed6f2 100644 --- a/Sources/CropImage/CropImageView.swift +++ b/Sources/CropImage/CropImageView.swift @@ -19,7 +19,7 @@ public struct CropImageView: 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 = ( + public typealias ControlClosure = ( _ offset: Binding, _ scale: Binding, _ rotation: Binding, @@ -30,7 +30,7 @@ public struct CropImageView: View { /// /// - Parameters: /// - targetSize: The size of the cut hole. - public typealias CutHoleClosure = (_ 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: View { /// /// The error should be a ``CropError``. public var onCrop: (Result) -> Void - var controls: ControlClosure - var cutHole: CutHoleClosure + 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: View { targetScale: CGFloat = 1, fulfillTargetFrame: Bool = true, onCrop: @escaping (Result) -> Void, - @ViewBuilder controls: @escaping ControlClosure, - @ViewBuilder cutHole: @escaping CutHoleClosure + @ViewBuilder controls: @escaping ControlClosure, + @ViewBuilder cutHole: @escaping CutHoleClosure ) { self.image = image self.targetSize = targetSize @@ -92,7 +92,7 @@ public struct CropImageView: View { targetScale: CGFloat = 1, fulfillTargetFrame: Bool = true, onCrop: @escaping (Result) -> Void, - @ViewBuilder controls: @escaping ControlClosure + @ViewBuilder controls: @escaping ControlClosure ) where CutHole == DefaultCutHoleView { self.image = image self.targetSize = targetSize From 7042102108a0c260d18c2869a3c8fa0d602c9a99 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Wed, 18 Dec 2024 16:49:24 +0800 Subject: [PATCH 18/20] fix: build on non-macOS. --- Sources/CropImage/CropImageView.swift | 2 +- Sources/CropImage/PlatformImage.swift | 7 +++++++ Sources/CropImage/UnderlyingImageView.swift | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Sources/CropImage/CropImageView.swift b/Sources/CropImage/CropImageView.swift index 4bed6f2..9cd2389 100644 --- a/Sources/CropImage/CropImageView.swift +++ b/Sources/CropImage/CropImageView.swift @@ -233,7 +233,7 @@ public struct CropImageView: View { var body: some View { VStack { CropImageView( - image: .init(contentsOf: URL(string: "file:///System/Library/Desktop%20Pictures/Hello%20Metallic%20Blue.heic")!)!, + image: .previewImage, targetSize: targetSize ) { result = $0 diff --git a/Sources/CropImage/PlatformImage.swift b/Sources/CropImage/PlatformImage.swift index 831e93b..7963809 100644 --- a/Sources/CropImage/PlatformImage.swift +++ b/Sources/CropImage/PlatformImage.swift @@ -13,10 +13,17 @@ import AppKit /// /// On macOS, it's `NSImage` and on iOS/visionOS it's `UIImage`. public typealias PlatformImage = NSImage +extension PlatformImage { + 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 diff --git a/Sources/CropImage/UnderlyingImageView.swift b/Sources/CropImage/UnderlyingImageView.swift index 9d1ee3b..882ade4 100644 --- a/Sources/CropImage/UnderlyingImageView.swift +++ b/Sources/CropImage/UnderlyingImageView.swift @@ -202,7 +202,7 @@ struct UnderlyingImageView: View { offset: $offset, scale: $scale, rotation: $rotation, - image: .init(contentsOf: URL(string: "file:///System/Library/Desktop%20Pictures/Hello%20Metallic%20Blue.heic")!)!, + image: .previewImage, viewSize: .init(width: 200, height: 100), targetSize: .init(width: 100, height: 100), fulfillTargetFrame: true From 481f59cf412f41641d4b737d8fbb51433031ce03 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Fri, 20 Dec 2024 14:54:32 +0800 Subject: [PATCH 19/20] fix: macos build fixes #4 --- Sources/CropImage/PlatformImage.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CropImage/PlatformImage.swift b/Sources/CropImage/PlatformImage.swift index 7963809..ab243b5 100644 --- a/Sources/CropImage/PlatformImage.swift +++ b/Sources/CropImage/PlatformImage.swift @@ -14,7 +14,7 @@ import AppKit /// On macOS, it's `NSImage` and on iOS/visionOS it's `UIImage`. public typealias PlatformImage = NSImage extension PlatformImage { - static let previewImage: PlatformImage = .init(contentsOf: URL(string: "file:///System/Library/Desktop%20Pictures/Hello%20Metallic%20Blue.heic")!)! + @MainActor static let previewImage: PlatformImage = .init(contentsOf: URL(string: "file:///System/Library/Desktop%20Pictures/Hello%20Metallic%20Blue.heic")!)! } #else import UIKit From 9a8b486f5529768600a885d3e53cde19867a7098 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Tue, 24 Dec 2024 14:18:08 +0800 Subject: [PATCH 20/20] Create FUNDING.yml --- .github/FUNDING.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..b1de7a2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [laosb] +buy_me_a_coffee: laosb