From 645e4d6b2cc4a92c942329ad5ca482d0479cc717 Mon Sep 17 00:00:00 2001 From: Shibo Lyu Date: Fri, 21 Jul 2023 18:52:11 +0800 Subject: [PATCH] WIP. --- Package.swift | 9 ++- Sources/CropImage/CropImage.swift | 6 -- Sources/CropImage/CropImageView.swift | 57 +++++++++++++++ .../CropImage/MoveAndScalableImageView.swift | 73 +++++++++++++++++++ Sources/CropImage/PlatformImage.swift | 16 ++++ Sources/CropImage/RectHoleShape.swift | 45 ++++++++++++ Tests/CropImageTests/CropImageTests.swift | 11 --- 7 files changed, 196 insertions(+), 21 deletions(-) delete mode 100644 Sources/CropImage/CropImage.swift create mode 100644 Sources/CropImage/CropImageView.swift create mode 100644 Sources/CropImage/MoveAndScalableImageView.swift create mode 100644 Sources/CropImage/PlatformImage.swift create mode 100644 Sources/CropImage/RectHoleShape.swift delete mode 100644 Tests/CropImageTests/CropImageTests.swift diff --git a/Package.swift b/Package.swift index e073068..948f859 100644 --- a/Package.swift +++ b/Package.swift @@ -5,6 +5,10 @@ import PackageDescription let package = Package( name: "CropImage", + platforms: [ + .iOS(.v14), + .macOS(.v13) + ], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -20,9 +24,6 @@ let package = Package( // Targets can depend on other targets in this package, and on products in packages this package depends on. .target( name: "CropImage", - dependencies: []), - .testTarget( - name: "CropImageTests", - dependencies: ["CropImage"]), + dependencies: []) ] ) diff --git a/Sources/CropImage/CropImage.swift b/Sources/CropImage/CropImage.swift deleted file mode 100644 index 7e69e25..0000000 --- a/Sources/CropImage/CropImage.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct CropImage { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Sources/CropImage/CropImageView.swift b/Sources/CropImage/CropImageView.swift new file mode 100644 index 0000000..201bf96 --- /dev/null +++ b/Sources/CropImage/CropImageView.swift @@ -0,0 +1,57 @@ +// +// CropImageView.swift +// +// +// Created by Shibo Lyu on 2023/7/21. +// + +import SwiftUI + +public struct CropImageView: View { + var image: PlatformImage + var targetSize: CGSize + var onCrop: (PlatformImage) -> Void + + @State private var offset: CGSize = .zero + @State private var scale: CGFloat = 1 + + public var body: some View { + ZStack { + MoveAndScalableImageView(offset: $offset, scale: $scale, image: image) + RectHoleShape(size: targetSize) + .fill(style: FillStyle(eoFill: true)) + .foregroundColor(.black.opacity(0.6)) + .allowsHitTesting(false) + } + } +} + +struct CropImageView_Previews: PreviewProvider { + struct PreviewView: View { + @State private var croppedImage: PlatformImage? = nil + + var body: some View { + VStack { + CropImageView( + image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!, + targetSize: .init(width: 100, height: 100) + ) { _ in + + } + if let croppedImage { + #if os(macOS) + Image(nsImage: croppedImage) + #elseif os(iOS) + Image(uiImage: croppedImage) + #endif + } else { + Text("Press \(Image(systemName: "checkmark.circle.fill")) to crop.") + } + } + } + } + + static var previews: some View { + PreviewView() + } +} diff --git a/Sources/CropImage/MoveAndScalableImageView.swift b/Sources/CropImage/MoveAndScalableImageView.swift new file mode 100644 index 0000000..c622518 --- /dev/null +++ b/Sources/CropImage/MoveAndScalableImageView.swift @@ -0,0 +1,73 @@ +// +// MoveAndScalableImageView.swift +// +// +// Created by Shibo Lyu on 2023/7/21. +// + +import SwiftUI + +private extension CGSize { + static func + (lhs: CGSize, rhs: CGSize) -> CGSize { + .init(width: lhs.width + rhs.width, height: lhs.height + rhs.height) + } +} + +struct MoveAndScalableImageView: View { + @Binding var offset: CGSize + @Binding var scale: CGFloat + var image: PlatformImage + + @State private var tempOffset: CGSize = .zero + @State private var tempScale: CGFloat = 1 + + var body: some View { + ZStack { + #if os(macOS) + Image(nsImage: image) + .scaleEffect(scale * tempScale) + .offset(offset + tempOffset) + #elseif os(iOS) + Image(uiImage: image) + .scaleEffect(scale * tempScale) + .offset(offset + tempOffset) + #endif + Color.white.opacity(0.0001) + .gesture( + DragGesture() + .onChanged { value in + tempOffset = value.translation + } + .onEnded { value in + offset = offset + tempOffset + tempOffset = .zero + } + ) + .gesture( + MagnificationGesture() + .onChanged { value in + tempScale = value.magnitude + } + .onEnded { value in + scale = scale * tempScale + tempScale = 1 + } + ) + } + } +} + +struct MoveAndScalableImageView_Previews: PreviewProvider { + struct PreviewView: View { + @State private var offset: CGSize = .zero + @State private var scale: CGFloat = 1 + + var body: some View { + MoveAndScalableImageView(offset: $offset, scale: $scale, image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!) + } + } + + static var previews: some View { + PreviewView() + } +} diff --git a/Sources/CropImage/PlatformImage.swift b/Sources/CropImage/PlatformImage.swift new file mode 100644 index 0000000..d815963 --- /dev/null +++ b/Sources/CropImage/PlatformImage.swift @@ -0,0 +1,16 @@ +// +// PlatformImage.swift +// +// +// Created by Shibo Lyu on 2023/7/21. +// + +import Foundation + +#if os(macOS) +import AppKit +public typealias PlatformImage = NSImage +#elseif os(iOS) +import UIKit +public typealias PlatformImage = UIImage +#endif diff --git a/Sources/CropImage/RectHoleShape.swift b/Sources/CropImage/RectHoleShape.swift new file mode 100644 index 0000000..d77db99 --- /dev/null +++ b/Sources/CropImage/RectHoleShape.swift @@ -0,0 +1,45 @@ +// +// RectHoleShape.swift +// +// +// Created by Shibo Lyu on 2023/7/21. +// + +import SwiftUI + +struct RectHoleShape: Shape { + let size: CGSize + func path(in rect: CGRect) -> Path { + let path = CGMutablePath() + path.move(to: rect.origin) + path.addLine(to: .init(x: rect.maxX, y: rect.minY)) + path.addLine(to: .init(x: rect.maxX, y: rect.maxY)) + path.addLine(to: .init(x: rect.minX, y: rect.maxY)) + path.addLine(to: rect.origin) + path.closeSubpath() + + let newRect = CGRect(origin: .init( + x: rect.midX - size.width / 2.0, + y: rect.midY - size.height / 2.0 + ), 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) + path.closeSubpath() + return Path(path) + } +} + +struct RectHoleShape_Previews: PreviewProvider { + static var previews: some View { + VStack { + RectHoleShape(size: .init(width: 100, height: 100)) + .fill(style: FillStyle(eoFill: true)) + .foregroundColor(.black.opacity(0.6)) + } + } +} + diff --git a/Tests/CropImageTests/CropImageTests.swift b/Tests/CropImageTests/CropImageTests.swift deleted file mode 100644 index 8413054..0000000 --- a/Tests/CropImageTests/CropImageTests.swift +++ /dev/null @@ -1,11 +0,0 @@ -import XCTest -@testable import CropImage - -final class CropImageTests: XCTestCase { - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct - // results. - XCTAssertEqual(CropImage().text, "Hello, World!") - } -}