mirror of
https://github.com/laosb/CropImage.git
synced 2025-04-30 15:41:08 +00:00
feat: Rotation.
This commit is contained in:
parent
bbfe1e4636
commit
3f9e8fab8e
3 changed files with 130 additions and 42 deletions
|
@ -12,6 +12,13 @@ import UIKit
|
|||
|
||||
/// A view that allows the user to crop an image.
|
||||
public struct CropImageView<Controls: View>: View {
|
||||
public typealias ControlClosure<Controls> = (
|
||||
_ offset: Binding<CGSize>,
|
||||
_ scale: Binding<CGFloat>,
|
||||
_ rotation: Binding<Angle>,
|
||||
_ crop: @escaping () async -> ()
|
||||
) -> Controls
|
||||
|
||||
/// Errors that could happen during the cropping process.
|
||||
public enum CropError: Error {
|
||||
/// SwiftUI `ImageRenderer` returned nil when calling `nsImage` or `uiImage`.
|
||||
|
@ -28,29 +35,6 @@ public struct CropImageView<Controls: View>: View {
|
|||
case failedToGetImageFromCurrentUIGraphicsImageContext
|
||||
}
|
||||
|
||||
private static func defaultControlsView(crop: @escaping () async -> ()) -> AnyView { AnyView(
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Button { Task {
|
||||
await crop()
|
||||
} } label: {
|
||||
Label("Crop", systemImage: "checkmark.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.labelStyle(.iconOnly)
|
||||
.padding(1)
|
||||
.background(
|
||||
Circle().fill(.white)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
) }
|
||||
|
||||
/// The image to crop.
|
||||
public var image: PlatformImage
|
||||
/// The intended size of the cropped image, in points.
|
||||
|
@ -67,7 +51,7 @@ public struct CropImageView<Controls: View>: View {
|
|||
///
|
||||
/// - Parameters:
|
||||
/// - crop: An async function to trigger crop action. Result will be delivered via ``onCrop``.
|
||||
public var controls: (_ crop: @escaping () async -> ()) -> Controls
|
||||
public var controls: ControlClosure<Controls>
|
||||
|
||||
/// Create a ``CropImageView`` with a custom ``controls`` view.
|
||||
public init(
|
||||
|
@ -75,7 +59,7 @@ public struct CropImageView<Controls: View>: View {
|
|||
targetSize: CGSize,
|
||||
targetScale: CGFloat = 1,
|
||||
onCrop: @escaping (Result<PlatformImage, Error>) -> Void,
|
||||
@ViewBuilder controls: @escaping (_ crop: () async -> ()) -> Controls
|
||||
@ViewBuilder controls: @escaping ControlClosure<Controls>
|
||||
) {
|
||||
self.image = image
|
||||
self.targetSize = targetSize
|
||||
|
@ -91,21 +75,29 @@ public struct CropImageView<Controls: View>: View {
|
|||
targetSize: CGSize,
|
||||
targetScale: CGFloat = 1,
|
||||
onCrop: @escaping (Result<PlatformImage, Error>) -> Void
|
||||
) where Controls == AnyView {
|
||||
) where Controls == DefaultControlsView {
|
||||
self.image = image
|
||||
self.targetSize = targetSize
|
||||
self.targetScale = targetScale
|
||||
self.onCrop = onCrop
|
||||
self.controls = Self.defaultControlsView
|
||||
self.controls = { $offset, $scale, $rotation, crop in
|
||||
DefaultControlsView(offset: $offset, scale: $scale, rotation: $rotation, crop: crop)
|
||||
}
|
||||
}
|
||||
|
||||
@State private var offset: CGSize = .zero
|
||||
@State private var scale: CGFloat = 1
|
||||
@State private var rotation: Angle = .zero
|
||||
|
||||
@MainActor
|
||||
func crop() throws -> PlatformImage {
|
||||
let snapshotView = MoveAndScalableImageView(offset: $offset, scale: $scale, image: image)
|
||||
.frame(width: targetSize.width, height: targetSize.height)
|
||||
let snapshotView = UnderlyingImageView(
|
||||
offset: $offset,
|
||||
scale: $scale,
|
||||
rotation: $rotation,
|
||||
image: image
|
||||
)
|
||||
.frame(width: targetSize.width, height: targetSize.height)
|
||||
if #available(iOS 16.0, macOS 13.0, *) {
|
||||
let renderer = ImageRenderer(content: snapshotView)
|
||||
renderer.scale = targetScale
|
||||
|
@ -147,13 +139,18 @@ public struct CropImageView<Controls: View>: View {
|
|||
|
||||
public var body: some View {
|
||||
ZStack {
|
||||
MoveAndScalableImageView(offset: $offset, scale: $scale, image: image)
|
||||
UnderlyingImageView(
|
||||
offset: $offset,
|
||||
scale: $scale,
|
||||
rotation: $rotation,
|
||||
image: image
|
||||
)
|
||||
RectHoleShape(size: targetSize)
|
||||
.fill(style: FillStyle(eoFill: true))
|
||||
.foregroundColor(.black.opacity(0.6))
|
||||
.animation(.default, value: targetSize)
|
||||
.allowsHitTesting(false)
|
||||
controls {
|
||||
controls($offset, $scale, $rotation) {
|
||||
do {
|
||||
onCrop(.success(try crop()))
|
||||
} catch {
|
||||
|
|
70
Sources/CropImage/DefaultControlsView.swift
Normal file
70
Sources/CropImage/DefaultControlsView.swift
Normal file
|
@ -0,0 +1,70 @@
|
|||
//
|
||||
// DefaultControlsView.swift
|
||||
//
|
||||
//
|
||||
// Created by Shibo Lyu on 2023/8/10.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// The default controls view used when creating ``CropImageView`` using ``CropImageView/init(image:targetSize:targetScale:onCrop:)``.
|
||||
///
|
||||
/// It provides basic controls to crop, reset to default cropping & rotation, and rotate the image.
|
||||
public struct DefaultControlsView: View {
|
||||
@Binding var offset: CGSize
|
||||
@Binding var scale: CGFloat
|
||||
@Binding var rotation: Angle
|
||||
var crop: () async -> Void
|
||||
|
||||
public var body: some View {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Button {
|
||||
let roundedAngle = Angle.degrees((rotation.degrees / 90).rounded() * 90)
|
||||
withAnimation {
|
||||
rotation = roundedAngle + .degrees(90)
|
||||
}
|
||||
} label: {
|
||||
Label("Rotate", systemImage: "rotate.right")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.labelStyle(.iconOnly)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
||||
.fill(.white)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding()
|
||||
Spacer()
|
||||
Button("Reset") {
|
||||
withAnimation {
|
||||
offset = .zero
|
||||
scale = 1
|
||||
rotation = .zero
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.buttonBorderShape(.roundedRectangle)
|
||||
Spacer()
|
||||
Button { Task {
|
||||
await crop()
|
||||
} } label: {
|
||||
Label("Crop", systemImage: "checkmark.circle.fill")
|
||||
.font(.title2)
|
||||
.foregroundColor(.accentColor)
|
||||
.labelStyle(.iconOnly)
|
||||
.padding(1)
|
||||
.background(
|
||||
Circle().fill(.white)
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MoveAndScalableImageView.swift
|
||||
// UnderlyingImageView.swift
|
||||
//
|
||||
//
|
||||
// Created by Shibo Lyu on 2023/7/21.
|
||||
|
@ -13,25 +13,30 @@ private extension CGSize {
|
|||
}
|
||||
}
|
||||
|
||||
struct MoveAndScalableImageView: View {
|
||||
struct UnderlyingImageView: View {
|
||||
@Binding var offset: CGSize
|
||||
@Binding var scale: CGFloat
|
||||
@Binding var rotation: Angle
|
||||
var image: PlatformImage
|
||||
|
||||
@State private var tempOffset: CGSize = .zero
|
||||
@State private var tempScale: CGFloat = 1
|
||||
@State private var tempRotation: Angle = .zero
|
||||
|
||||
var imageView: Image {
|
||||
#if os(macOS)
|
||||
Image(nsImage: image)
|
||||
#elseif os(iOS)
|
||||
Image(uiImage: image)
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
#if os(macOS)
|
||||
Image(nsImage: image)
|
||||
imageView
|
||||
.scaleEffect(scale * tempScale)
|
||||
.offset(offset + tempOffset)
|
||||
#elseif os(iOS)
|
||||
Image(uiImage: image)
|
||||
.scaleEffect(scale * tempScale)
|
||||
.offset(offset + tempOffset)
|
||||
#endif
|
||||
.rotationEffect(rotation + tempRotation)
|
||||
Color.white.opacity(0.0001)
|
||||
.gesture(
|
||||
DragGesture()
|
||||
|
@ -46,13 +51,23 @@ struct MoveAndScalableImageView: View {
|
|||
.gesture(
|
||||
MagnificationGesture()
|
||||
.onChanged { value in
|
||||
tempScale = value.magnitude
|
||||
tempScale = value
|
||||
}
|
||||
.onEnded { value in
|
||||
scale = scale * tempScale
|
||||
tempScale = 1
|
||||
}
|
||||
)
|
||||
.gesture(
|
||||
RotationGesture()
|
||||
.onChanged { value in
|
||||
tempRotation = value
|
||||
}
|
||||
.onEnded { value in
|
||||
rotation = rotation + tempRotation
|
||||
tempRotation = .zero
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -61,9 +76,15 @@ struct MoveAndScalableImageView_Previews: PreviewProvider {
|
|||
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 {
|
||||
MoveAndScalableImageView(offset: $offset, scale: $scale, image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!)
|
||||
UnderlyingImageView(
|
||||
offset: $offset,
|
||||
scale: $scale,
|
||||
rotation: $rotation,
|
||||
image: .init(contentsOfFile: "/Users/laosb/Downloads/png.png")!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
Loading…
Add table
Reference in a new issue