我正在 SwiftUI 中开发 iOS 应用程序,我正在尝试实现一项功能,用户可以将签名添加到图像并保存结果。目前,我有一个 SignatureAddingView,它允许用户将签名叠加层添加到图像并对其进行操作(缩放、旋转和拖动)。但是,我正在努力弄清楚如何使用添加的签名覆盖来保存图像。
这是我的代码的相关部分:
// View with image and signature
VStack {
GeometryReader { geometry in
let magnificationGesture = MagnificationGesture()
.onChanged{ gesture in
scaleAnchor = .center
scale = lastScale * gesture
}
.onEnded { _ in
fixOffsetAndScale(geometry: geometry)
}
let dragGesture = DragGesture()
.onChanged { gesture in
var newOffset = lastOffset
newOffset.width += gesture.translation.width
newOffset.height += gesture.translation.height
offset = newOffset
}
.onEnded { _ in
fixOffsetAndScale(geometry: geometry)
}
Image(uiImage: scannedImage)
.resizable()
.scaledToFit()
.position(x: geometry.size.width / 2,
y: geometry.size.height / 2)
.scaleEffect(scale, anchor: scaleAnchor)
.offset(offset)
.gesture(dragGesture)
.gesture(magnificationGesture)
}
.overlay (
ZStack {
if let image = loadImageFromDocumentDirectory(filename: "signature.png") {
ZStack {
Rectangle()
.stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
.fill(.blue)
Image(uiImage: image)
.resizable()
.scaledToFit()
}
// .overlay(
// VStack {
// HStack {
//
// Spacer()
//
// Circle()
// .fill(Color.red) // Change the circle color as needed
// .frame(width: 20, height: 20) // Adjust the size of the circle as needed
// }
// .padding(.trailing, -18)
// .padding(.top, -13)
// Spacer()
// }
// )
.overlay(
VStack {
Spacer()
HStack {
Spacer()
Circle()
.fill(Color.green) // Change the circle color as needed
.frame(width: 20, height: 20) // Adjust the size of the circle as needed
}
.padding(.trailing, -18)
.padding(.bottom, -13)
}
)
.position(location)
.gesture(
simpleDrag.simultaneously(with: fingerDrag)
)
.gesture(RotationGesture().onChanged { angle in
// Rotate the image here based on the angle value
// You can apply the rotation transformation to the image
// using .rotationEffect(angle, anchor: .center) or simila
DispatchQueue.main.async {
withAnimation {
rotationAngle = Double(angle.degrees)
}
}
})
.gesture(
MagnificationGesture()
.onChanged { scaleValue in
DispatchQueue.main.async {
withAnimation {
scaling = scaleValue
}
}
}
)
}
}
.frame(width: 100, height: 100)
.rotationEffect(.degrees(Double(rotationAngle)), anchor: .center)
.scaleEffect(scaling)
)
}
// Function to save the modified image with the signature
func saveImageWithSignature() {
// Capture the current state of the view as an image
if let image = captureViewAsImage() {
// Save the image to the document directory
saveImageToDocumentDirectory(image: image, filename: "modified_image.png")
}
}
// Function to capture the current state of the view as an image
func captureViewAsImage() -> UIImage? {
let rect = UIScreen.main.bounds
let window = UIApplication.shared.windows.first { $0.isKeyWindow }
if let window = window {
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
window.drawHierarchy(in: rect, afterScreenUpdates: true)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return image
}
return nil
}
// Function to save an image to the document directory
func saveImageToDocumentDirectory(image: UIImage, filename: String) {
if let data = image.jpegData(compressionQuality: 1.0) {
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
let fileURL = documentsDirectory.appendingPathComponent(filename)
do {
try data.write(to: fileURL)
print("Image saved to document directory: \(fileURL)")
} catch {
print("Error saving image: \(error)")
}
}
}
总体思路是使用
GeometryReader
来传递要捕获的屏幕部分的框架。
这是一个使用屏幕捕获技术的简单示例,kontiki善意地提供了作为对另一篇文章的回答。从技术上讲,相机按钮也在
GeometryReader
的框架内,因为 GeometryProxy
需要位于点击回调的范围内。应用了 y 偏移,使其看起来按钮位于框架之外。或者,您可以将其保留在框架中并在快照期间将其隐藏。
struct ContentView: View {
@State private var snapshot: UIImage?
private var mainImage: some View {
Image(systemName: "fish.fill")
.resizable()
.scaledToFit()
.padding()
.foregroundColor(Color(red: 0.9, green: 0.45, blue: 0.0))
.background(Circle().foregroundColor(.white).frame(width: 40, height: 40).offset(x:80))
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background { Color(red: 0.47, green: 0.80, blue: 0.91) }
}
private var signature: some View {
Image(systemName: "signature")
.resizable()
.scaledToFit()
.foregroundColor(.black)
.frame(height: 40)
.padding()
.background(.white.opacity(0.3))
.shadow(color: .white, radius: 6)
.padding()
}
private var cameraButton: some View {
Image(systemName: "camera.fill")
.resizable()
.scaledToFit()
.foregroundColor(Color(UIColor.systemBackground))
.padding(10)
.frame(height: 50)
.background(Color(UIColor.secondaryLabel))
.cornerRadius(8)
.accessibilityAddTraits(.isButton)
}
private func captureSnapshot(rect: CGRect) -> UIImage? {
var result: UIImage?
if let scene = UIApplication.shared.connectedScenes.first(
where: { $0.activationState == .foregroundActive }
) as? UIWindowScene {
result = scene.windows[0].rootViewController?.view.asImage(rect: rect)
}
return result
}
@ViewBuilder
private var framedSnapshot: some View {
if let snapshot {
Image(uiImage: snapshot)
.resizable()
.scaledToFit()
.frame(width: 250)
.padding(10)
.border(.gray, width: 2)
.background(.white)
}
}
var body: some View {
VStack {
GeometryReader { proxy in
ZStack(alignment: .bottom) {
mainImage
.overlay(alignment: .bottomTrailing) {
signature
}
cameraButton
.offset(y: 70)
.onTapGesture {
snapshot = captureSnapshot(rect: proxy.frame(in: .global))
}
}
}
.frame(width: 350, height: 250)
.padding(.bottom, 120)
framedSnapshot
Spacer()
}
.padding(.top, 60)
}
}
// Credit to kontiki for the screenshot solution
// https://stackoverflow.com/a/57206207/20386264
extension UIView {
func asImage(rect: CGRect) -> UIImage {
let renderer = UIGraphicsImageRenderer(bounds: rect)
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
}
}