Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Demo/SwiftyCropDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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";
Expand Down
53 changes: 48 additions & 5 deletions Demo/SwiftyCropDemo/ContentView.swift
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -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() {
Expand All @@ -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()
}
2 changes: 2 additions & 0 deletions Demo/SwiftyCropDemo/UIElements/DecimalTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct DecimalTextField: View {
TextField("maxMagnification", value: $value, formatter: decimalFormatter)
.textFieldStyle(RoundedBorderTextFieldStyle())
.multilineTextAlignment(.trailing)
#if canImport(UIKit)
.keyboardType(.decimalPad)
#endif
}
}
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
90 changes: 66 additions & 24 deletions Sources/SwiftyCrop/Models/CropViewModel.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
}

/**
Expand All @@ -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
Expand Down Expand Up @@ -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


}

/**
Expand All @@ -133,47 +168,50 @@ 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.
- Parameters:
- 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(
Expand Down Expand Up @@ -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
}
}

Expand Down
21 changes: 20 additions & 1 deletion Sources/SwiftyCrop/SwiftyCrop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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(
Expand Down
Loading