diff options
author | Navan Chauhan <navanchauhan@gmail.com> | 2024-08-12 23:59:40 -0600 |
---|---|---|
committer | Navan Chauhan <navanchauhan@gmail.com> | 2024-08-13 00:00:24 -0600 |
commit | fb3e562581a8367e0c83bf5b4c1548a5216281cf (patch) | |
tree | fcc7ada95f409807863bf29b0ee2e435712d6fbb | |
parent | 24a67bf73595994ccf4b60cdad0021b37ca45c45 (diff) |
-rw-r--r-- | Demo/SwiftyCropDemo.xcodeproj/project.pbxproj | 8 | ||||
-rw-r--r-- | Demo/SwiftyCropDemo/ContentView.swift | 53 | ||||
-rw-r--r-- | Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift | 2 | ||||
-rw-r--r-- | Package.swift | 2 | ||||
-rw-r--r-- | Sources/SwiftyCrop/Models/CropViewModel.swift | 90 | ||||
-rw-r--r-- | Sources/SwiftyCrop/SwiftyCrop.swift | 21 | ||||
-rw-r--r-- | Sources/SwiftyCrop/View/CropView.swift | 38 |
7 files changed, 170 insertions, 44 deletions
diff --git a/Demo/SwiftyCropDemo.xcodeproj/project.pbxproj b/Demo/SwiftyCropDemo.xcodeproj/project.pbxproj index 5609787..dcf869d 100644 --- a/Demo/SwiftyCropDemo.xcodeproj/project.pbxproj +++ b/Demo/SwiftyCropDemo.xcodeproj/project.pbxproj @@ -309,9 +309,13 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "--PRODUCT-BUNDLE-IDENTIFIER-"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; @@ -337,9 +341,13 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 12.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "--PRODUCT-BUNDLE-IDENTIFIER-"; PRODUCT_NAME = "$(TARGET_NAME)"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; diff --git a/Demo/SwiftyCropDemo/ContentView.swift b/Demo/SwiftyCropDemo/ContentView.swift index 08b1060..d4a7d5d 100644 --- a/Demo/SwiftyCropDemo/ContentView.swift +++ b/Demo/SwiftyCropDemo/ContentView.swift @@ -1,9 +1,15 @@ import SwiftUI import SwiftyCrop +#if canImport(UIKit) +typealias PlatformImage = UIImage +#elseif canImport(AppKit) +typealias PlatformImage = NSImage +#endif + struct ContentView: View { @State private var showImageCropper: Bool = false - @State private var selectedImage: UIImage? + @State private var selectedImage: PlatformImage? @State private var selectedShape: MaskShape = .square @State private var cropImageCircular: Bool @State private var rotateImage: Bool @@ -27,8 +33,7 @@ struct ContentView: View { Group { if let selectedImage = selectedImage { - Image(uiImage: selectedImage) - .resizable() + PlatformImageView(image: selectedImage) .aspectRatio(contentMode: .fit) .cornerRadius(8) } else { @@ -84,7 +89,15 @@ struct ContentView: View { .frame(maxWidth: .infinity, alignment: .leading) Button { +#if canImport(UIKit) maskRadius = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) / 2 +#elseif canImport(AppKit) + if let screen = NSScreen.main { + maskRadius = min(screen.frame.width, screen.frame.height) / 2 + } else { + maskRadius = 200 // Default value if no screen is available + } +#endif } label: { Image(systemName: "arrow.up.left.and.arrow.down.right") .font(.footnote) @@ -119,7 +132,19 @@ struct ContentView: View { .onAppear { loadImage() } + #if os(macOS) + .sheet(isPresented: $showImageCropper) { + imageCropperView + } + #else .fullScreenCover(isPresented: $showImageCropper) { + imageCropperView + } + #endif + } + + private var imageCropperView: some View { + Group { if let selectedImage = selectedImage { SwiftyCropView( imageToCrop: selectedImage, @@ -137,6 +162,9 @@ struct ContentView: View { } } } + #if canImport(AppKit) + .frame(width: 600, height: 400) // Adjust size as needed for macOS + #endif } private func loadImage() { @@ -146,19 +174,34 @@ struct ContentView: View { } // Example function for downloading an image - private func downloadExampleImage() async -> UIImage? { + private func downloadExampleImage() async -> PlatformImage? { let portraitUrlString = "https://picsum.photos/1000/1200" let landscapeUrlString = "https://picsum.photos/2000/1000" let urlString = Int.random(in: 0...1) == 0 ? portraitUrlString : landscapeUrlString guard let url = URL(string: urlString), let (data, _) = try? await URLSession.shared.data(from: url), - let image = UIImage(data: data) + let image = PlatformImage(data: data) else { return nil } return image } } +struct PlatformImageView: View { + let image: PlatformImage + + var body: some View { + #if canImport(UIKit) + Image(uiImage: image) + .resizable() + #elseif canImport(AppKit) + Image(nsImage: image) + .resizable() + #endif + } +} + + #Preview { ContentView() } diff --git a/Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift b/Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift index f616413..9758f0f 100644 --- a/Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift +++ b/Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift @@ -15,6 +15,8 @@ struct DecimalTextField: View { TextField("maxMagnification", value: $value, formatter: decimalFormatter) .textFieldStyle(RoundedBorderTextFieldStyle()) .multilineTextAlignment(.trailing) + #if canImport(UIKit) .keyboardType(.decimalPad) + #endif } } diff --git a/Package.swift b/Package.swift index d233735..9d9a9b1 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "SwiftyCrop", defaultLocalization: "en", - platforms: [.iOS(.v16)], + platforms: [.iOS(.v16), .macOS(.v12)], products: [ // Products define the executables and libraries a package produces, making them visible to other packages. .library( diff --git a/Sources/SwiftyCrop/Models/CropViewModel.swift b/Sources/SwiftyCrop/Models/CropViewModel.swift index 869d4f8..1e9914b 100644 --- a/Sources/SwiftyCrop/Models/CropViewModel.swift +++ b/Sources/SwiftyCrop/Models/CropViewModel.swift @@ -1,5 +1,15 @@ import SwiftUI +#if canImport(UIKit) import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + +#if canImport(UIKit) +typealias PlatformImage = UIImage +#elseif canImport(AppKit) +typealias PlatformImage = NSImage +#endif class CropViewModel: ObservableObject { private let maxMagnificationScale: CGFloat @@ -53,19 +63,28 @@ class CropViewModel: ObservableObject { - image: The UIImage to crop - Returns: A cropped UIImage if the cropping operation is successful; otherwise nil. */ - func cropToSquare(_ image: UIImage) -> UIImage? { + func cropToSquare(_ image: PlatformImage) -> PlatformImage? { guard let orientedImage = image.correctlyOriented else { return nil } let cropRect = calculateCropRect(orientedImage) + #if canImport(UIKit) guard let cgImage = orientedImage.cgImage, let result = cgImage.cropping(to: cropRect) else { return nil } - return UIImage(cgImage: result) + #elseif canImport(AppKit) + guard let cgImage = orientedImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return nil + } + guard let croppedCGImage = cgImage.cropping(to: cropRect) else { + return nil + } + return NSImage(cgImage: croppedCGImage, size: cropRect.size) + #endif } /** @@ -74,13 +93,14 @@ class CropViewModel: ObservableObject { - image: The UIImage to crop - Returns: A cropped UIImage if the cropping operation is successful; otherwise nil. */ - func cropToCircle(_ image: UIImage) -> UIImage? { + func cropToCircle(_ image: PlatformImage) -> PlatformImage? { guard let orientedImage = image.correctlyOriented else { return nil } let cropRect = calculateCropRect(orientedImage) - + + #if canImport(UIKit) // A circular crop results in some transparency in the // cropped image, so set opaque to false to ensure the // cropped image does not include a background fill @@ -124,6 +144,21 @@ class CropViewModel: ObservableObject { } return circleCroppedImage + #elseif canImport(AppKit) + let circleCroppedImage = NSImage(size: cropRect.size) + circleCroppedImage.lockFocus() + let drawRect = NSRect(origin: .zero, size: cropRect.size) + NSBezierPath(ovalIn: drawRect).addClip() + let drawImageRect = NSRect( + origin: NSPoint(x: -cropRect.origin.x, y: -cropRect.origin.y), + size: orientedImage.size + ) + orientedImage.draw(in: drawImageRect) + circleCroppedImage.unlockFocus() + return circleCroppedImage + #endif + + } /** @@ -133,39 +168,42 @@ class CropViewModel: ObservableObject { - angle: The Angle to rotate to - Returns: A rotated UIImage if the rotating operation is successful; otherwise nil. */ - func rotate(_ image: UIImage, _ angle: Angle) -> UIImage? { + func rotate(_ image: PlatformImage, _ angle: Angle) -> PlatformImage? { guard let orientedImage = image.correctlyOriented else { return nil } + #if canImport(UIKit) guard let cgImage = orientedImage.cgImage else { return nil } + #elseif canImport(AppKit) + guard let cgImage = orientedImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else { + return nil + } + #endif let ciImage = CIImage(cgImage: cgImage) // Prepare filter - let filter = CIFilter.straightenFilter( - image: ciImage, - radians: angle.radians - ) - - // Get output image - guard let output = filter?.outputImage else { - return nil - } + guard let filter = CIFilter.straightenFilter(image: ciImage, radians: angle.radians), + // Get output image + let output = filter.outputImage else { + return nil + } // Create resulting image let context = CIContext() - guard let result = context.createCGImage( - output, - from: output.extent - ) else { + guard let result = context.createCGImage(output, from: output.extent) else { return nil } + #if canImport(UIKit) return UIImage(cgImage: result) - } + #elseif canImport(AppKit) + return NSImage(cgImage: result, size: NSSize(width: result.width, height: result.height)) + #endif + } /** Calculates the rectangle to crop. @@ -173,7 +211,7 @@ class CropViewModel: ObservableObject { - image: The UIImage to calculate the rectangle to crop for - Returns: A CGRect representing the rectangle to crop. */ - private func calculateCropRect(_ orientedImage: UIImage) -> CGRect { + private func calculateCropRect(_ orientedImage: PlatformImage) -> CGRect { // The relation factor of the originals image width/height // and the width/height of the image displayed in the view (initial) let factor = min( @@ -206,21 +244,25 @@ class CropViewModel: ObservableObject { } } -private extension UIImage { +extension PlatformImage { /** - A UIImage instance with corrected orientation. + For iOS, A UIImage instance with corrected orientation. If the instance's orientation is already `.up`, it simply returns the original. - Returns: An optional UIImage that represents the correctly oriented image. */ - var correctlyOriented: UIImage? { + var correctlyOriented: PlatformImage? { + #if canImport(UIKit) if imageOrientation == .up { return self } - + UIGraphicsBeginImageContextWithOptions(size, false, scale) draw(in: CGRect(origin: .zero, size: size)) let normalizedImage = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return normalizedImage + #elseif canImport(AppKit) + return self + #endif } } diff --git a/Sources/SwiftyCrop/SwiftyCrop.swift b/Sources/SwiftyCrop/SwiftyCrop.swift index 22268a9..bb6164c 100644 --- a/Sources/SwiftyCrop/SwiftyCrop.swift +++ b/Sources/SwiftyCrop/SwiftyCrop.swift @@ -11,11 +11,17 @@ import SwiftUI /// - onComplete: A closure that's called when the cropping is complete. This closure returns the cropped `UIImage?`. /// If an error occurs the return value is nil. public struct SwiftyCropView: View { - private let imageToCrop: UIImage private let maskShape: MaskShape private let configuration: SwiftyCropConfiguration + #if canImport(UIKit) + private let imageToCrop: UIImage private let onComplete: (UIImage?) -> Void + #elseif canImport(AppKit) + private let imageToCrop: NSImage + private let onComplete: (NSImage?) -> Void + #endif + #if canImport(UIKit) public init( imageToCrop: UIImage, maskShape: MaskShape, @@ -27,6 +33,19 @@ public struct SwiftyCropView: View { self.configuration = configuration self.onComplete = onComplete } + #elseif canImport(AppKit) + public init( + imageToCrop: NSImage, + maskShape: MaskShape, + configuration: SwiftyCropConfiguration = SwiftyCropConfiguration(), + onComplete: @escaping (NSImage?) -> Void + ) { + self.imageToCrop = imageToCrop + self.maskShape = maskShape + self.configuration = configuration + self.onComplete = onComplete + } + #endif public var body: some View { CropView( diff --git a/Sources/SwiftyCrop/View/CropView.swift b/Sources/SwiftyCrop/View/CropView.swift index b8ca961..a587281 100644 --- a/Sources/SwiftyCrop/View/CropView.swift +++ b/Sources/SwiftyCrop/View/CropView.swift @@ -4,17 +4,17 @@ struct CropView: View { @Environment(\.dismiss) private var dismiss @StateObject private var viewModel: CropViewModel - private let image: UIImage + private let image: PlatformImage private let maskShape: MaskShape private let configuration: SwiftyCropConfiguration - private let onComplete: (UIImage?) -> Void + private let onComplete: (PlatformImage?) -> Void private let localizableTableName: String init( - image: UIImage, + image: PlatformImage, maskShape: MaskShape, configuration: SwiftyCropConfiguration, - onComplete: @escaping (UIImage?) -> Void + onComplete: @escaping (PlatformImage?) -> Void ) { self.image = image self.maskShape = maskShape @@ -81,9 +81,7 @@ struct CropView: View { .zIndex(1) ZStack { - Image(uiImage: image) - .resizable() - .scaledToFit() + PlatformImageView(image: image) .rotationEffect(viewModel.angle) .scaleEffect(viewModel.scale) .offset(viewModel.offset) @@ -97,9 +95,7 @@ struct CropView: View { } ) - Image(uiImage: image) - .resizable() - .scaledToFit() + PlatformImageView(image: image) .rotationEffect(viewModel.angle) .scaleEffect(viewModel.scale) .offset(viewModel.offset) @@ -137,10 +133,10 @@ struct CropView: View { .background(.black) } - private func cropImage() -> UIImage? { - var editedImage: UIImage = image + private func cropImage() -> PlatformImage? { + var editedImage: PlatformImage = image if configuration.rotateImage { - if let rotatedImage: UIImage = viewModel.rotate( + if let rotatedImage: PlatformImage = viewModel.rotate( editedImage, viewModel.lastAngle ) { @@ -170,3 +166,19 @@ struct CropView: View { } } } + +struct PlatformImageView: View { + let image: PlatformImage + + var body: some View { + #if canImport(UIKit) + Image(uiImage: image) + .resizable() + .scaledToFit() + #elseif canImport(AppKit) + Image(nsImage: image) + .resizable() + .scaledToFit() + #endif + } +} |