Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MacOS support based on excellent work by Navan Chauhan #18

Closed
wants to merge 3 commits into from
Closed
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
6 changes: 6 additions & 0 deletions Demo/SwiftyCropDemo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@
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 Expand Up @@ -340,6 +343,9 @@
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
34 changes: 29 additions & 5 deletions Demo/SwiftyCropDemo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftyCrop

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 rectAspectRatio: PresetAspectRatios = .fourToThree
@State private var cropImageCircular: Bool
Expand Down Expand Up @@ -43,7 +43,7 @@ struct ContentView: View {

Group {
if let selectedImage = selectedImage {
Image(uiImage: selectedImage)
Image(platformImage: selectedImage)
.resizable()
.aspectRatio(contentMode: .fit)
.cornerRadius(8)
Expand Down Expand Up @@ -115,7 +115,7 @@ struct ContentView: View {
.frame(maxWidth: .infinity, alignment: .leading)

Button {
maskRadius = min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) / 2
maskRadius = defaultMaskRadius
} label: {
Image(systemName: "arrow.up.left.and.arrow.down.right")
.font(.footnote)
Expand Down Expand Up @@ -150,7 +150,31 @@ struct ContentView: View {
.onAppear {
loadImage()
}
#if os(iOS)
.fullScreenCover(isPresented: $showImageCropper) {
cropView
}
#else
.sheet(isPresented: $showImageCropper) {
cropView
}
#endif
}

var defaultMaskRadius: CGFloat {
#if canImport(UIKit)
return min(UIScreen.main.bounds.width, UIScreen.main.bounds.height) / 2
#elseif canImport(AppKit)
if let screen = NSScreen.main {
return min(screen.frame.width, screen.frame.height) / 2
} else {
return 130
}
#endif
}

private var cropView: some View {
Group {
if let selectedImage = selectedImage {
SwiftyCropView(
imageToCrop: selectedImage,
Expand Down Expand Up @@ -178,13 +202,13 @@ 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
Expand Down
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 os(iOS)
.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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ import SwiftyCrop

struct ExampleView: View {
@State private var showImageCropper: Bool = false
@State private var selectedImage: UIImage?
@State private var selectedImage: PlatformImage?

var body: some View {
VStack {
Expand Down Expand Up @@ -118,11 +118,11 @@ struct ExampleView: View {
}

// Example function for downloading an image
private func downloadExampleImage() async -> UIImage? {
private func downloadExampleImage() async -> PlatformImage? {
let urlString = "https://picsum.photos/1000/1200"
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
Expand Down
92 changes: 70 additions & 22 deletions Sources/SwiftyCrop/Models/CropViewModel.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import SwiftUI
import UIKit

class CropViewModel: ObservableObject {
private let maskRadius: CGFloat
Expand Down Expand Up @@ -78,50 +77,71 @@ class CropViewModel: ObservableObject {

/**
Crops the given image to a rectangle based on the current mask size and position.
- Parameter image: The UIImage to crop.
- Returns: A cropped UIImage, or nil if cropping fails.
- Parameter image: The PlatformImage to crop.
- Returns: A cropped PlatformImage, or nil if cropping fails.
*/
func cropToRectangle(_ image: UIImage) -> UIImage? {
func cropToRectangle(_ 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)
return PlatformImage(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
}

/**
Crops the given image to a square based on the current mask size and position.
- Parameter image: The UIImage to crop.
- Returns: A cropped UIImage, or nil if cropping fails.
- Parameter image: The PlatformImage to crop.
- Returns: A cropped PlatformImage, or nil if cropping fails.
*/
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
}

/**
Crops the given image to a circle based on the current mask size and position.
- Parameter image: The UIImage to crop.
- Returns: A cropped UIImage, or nil if cropping fails.
- Parameter image: The PlatformImage to crop.
- Returns: A cropped PlatformImage, or nil if cropping fails.
*/
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)
let imageRendererFormat = orientedImage.imageRendererFormat
imageRendererFormat.opaque = false

Expand All @@ -138,17 +158,37 @@ 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
}

/**
Rotates the given image by the specified angle.
- Parameter image: The UIImage to rotate.
- Parameter image: The PlatformImage to rotate.
- Parameter angle: The Angle to rotate the image by.
- Returns: A rotated UIImage, or nil if rotation fails.
- Returns: A rotated PlatformImage, or nil if rotation fails.
*/
func rotate(_ image: UIImage, _ angle: Angle) -> UIImage? {
guard let orientedImage = image.correctlyOriented,
let cgImage = orientedImage.cgImage else { return nil }
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)

Expand All @@ -158,15 +198,19 @@ class CropViewModel: ObservableObject {
let context = CIContext()
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 use for cropping the image based on the current mask size, scale, and offset.
- Parameter orientedImage: The correctly oriented UIImage to calculate the crop rect for.
- Parameter orientedImage: The correctly oriented PlatformImage to calculate the crop rect for.
- Returns: A CGRect representing the area to crop from the original image.
*/
private func calculateCropRect(_ orientedImage: UIImage) -> CGRect {
private func calculateCropRect(_ orientedImage: PlatformImage) -> CGRect {
let factor = min(
(orientedImage.size.width / imageSizeInView.width),
(orientedImage.size.height / imageSizeInView.height)
Expand Down Expand Up @@ -194,13 +238,14 @@ class CropViewModel: ObservableObject {
}
}

private extension UIImage {
private extension PlatformImage {
/**
A UIImage instance with corrected orientation.
A PlatformImage 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.
- Returns: An optional PlatformImage that represents the correctly oriented image.
*/
var correctlyOriented: UIImage? {
var correctlyOriented: PlatformImage? {
#if canImport(UIKit)
if imageOrientation == .up { return self }

UIGraphicsBeginImageContextWithOptions(size, false, scale)
Expand All @@ -209,6 +254,9 @@ private extension UIImage {
UIGraphicsEndImageContext()

return normalizedImage
#elseif canImport(AppKit)
return self
#endif
}
}

Expand Down
22 changes: 22 additions & 0 deletions Sources/SwiftyCrop/PlatformImage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif

#if canImport(UIKit)
public typealias PlatformImage = UIImage
#elseif canImport(AppKit)
public typealias PlatformImage = NSImage
#endif

extension Image {
@inlinable public init(platformImage: PlatformImage) {
#if canImport(UIKit)
self.init(uiImage: platformImage)
#elseif canImport(AppKit)
self.init(nsImage: platformImage)
#endif
}
}
10 changes: 5 additions & 5 deletions Sources/SwiftyCrop/SwiftyCrop.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import SwiftUI
/// - imageToCrop: The image to be cropped.
/// - maskShape: The shape of the mask used for cropping.
/// - configuration: The configuration for the cropping behavior. If nothing is specified, the default is used.
/// - onComplete: A closure that's called when the cropping is complete. This closure returns the cropped `UIImage?`.
/// - onComplete: A closure that's called when the cropping is complete. This closure returns the cropped `PlatformImage?`.
/// If an error occurs the return value is nil.
public struct SwiftyCropView: View {
private let imageToCrop: UIImage
private let imageToCrop: PlatformImage
private let maskShape: MaskShape
private let configuration: SwiftyCropConfiguration
private let onComplete: (UIImage?) -> Void
private let onComplete: (PlatformImage?) -> Void

public init(
imageToCrop: UIImage,
imageToCrop: PlatformImage,
maskShape: MaskShape,
configuration: SwiftyCropConfiguration = SwiftyCropConfiguration(),
onComplete: @escaping (UIImage?) -> Void
onComplete: @escaping (PlatformImage?) -> Void
) {
self.imageToCrop = imageToCrop
self.maskShape = maskShape
Expand Down
Loading
Loading