From 9bd7effd4c47bd63f8e02056bead8e25a1bac27c Mon Sep 17 00:00:00 2001 From: v504 Date: Mon, 25 Aug 2025 19:40:25 +0800 Subject: [PATCH] Enhance QRCodeStyleView to support custom logo selection from the photo library, including image cropping functionality. Added permissions handling for photo library access and updated logo selection logic to accommodate both custom and preset logos, improving user experience and flexibility in logo customization. --- MyQrCode/Info.plist | 4 + MyQrCode/ScannerView/ScannerView.swift | 7 +- MyQrCode/Views/QRCodeStyleView.swift | 339 ++++++++++++++++++++++++- 3 files changed, 342 insertions(+), 8 deletions(-) diff --git a/MyQrCode/Info.plist b/MyQrCode/Info.plist index 010cb47..4057890 100644 --- a/MyQrCode/Info.plist +++ b/MyQrCode/Info.plist @@ -9,5 +9,9 @@ en zh-Hans + NSPhotoLibraryUsageDescription + 需要访问相册来选择自定义Logo图片 + NSPhotoLibraryAddUsageDescription + 需要访问相册来保存生成的二维码图片 diff --git a/MyQrCode/ScannerView/ScannerView.swift b/MyQrCode/ScannerView/ScannerView.swift index d1ceff3..f06a0c1 100644 --- a/MyQrCode/ScannerView/ScannerView.swift +++ b/MyQrCode/ScannerView/ScannerView.swift @@ -523,7 +523,8 @@ struct ImagePicker: UIViewControllerRepresentable { let picker = UIImagePickerController() picker.delegate = context.coordinator picker.sourceType = .photoLibrary - picker.allowsEditing = false + picker.allowsEditing = true + picker.videoQuality = .typeHigh return picker } @@ -541,7 +542,9 @@ struct ImagePicker: UIViewControllerRepresentable { } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - if let image = info[.originalImage] as? UIImage { + if let editedImage = info[.editedImage] as? UIImage { + parent.onImageSelected(editedImage) + } else if let image = info[.originalImage] as? UIImage { parent.onImageSelected(image) } picker.dismiss(animated: true) diff --git a/MyQrCode/Views/QRCodeStyleView.swift b/MyQrCode/Views/QRCodeStyleView.swift index b1104dd..52303c3 100644 --- a/MyQrCode/Views/QRCodeStyleView.swift +++ b/MyQrCode/Views/QRCodeStyleView.swift @@ -1,6 +1,10 @@ import SwiftUI import QRCode import CoreData +import Photos +#if canImport(PhotosUI) +import PhotosUI +#endif // MARK: - 标签类型枚举 enum TabType: String, CaseIterable { @@ -46,6 +50,12 @@ struct QRCodeStyleView: View { // Logo选择 @State private var selectedLogo: QRCodeLogo? = nil + @State private var customLogoImage: UIImage? = nil + @State private var photoPickerItem: Any? = nil + @State private var photoLibraryAccessGranted = false + @State private var showingImagePicker = false + @State private var showingImageCropper = false + @State private var imageToCrop: UIImage? = nil // 生成的二维码图片 @State private var qrCodeImage: UIImage? @@ -81,10 +91,19 @@ struct QRCodeStyleView: View { d.design.shape.eye = selectedEyeType.eyeShape // 如果有选择的Logo,设置Logo - if let selectedLogo = selectedLogo, - let logoImage = selectedLogo.image { - // 设置Logo作为背景图片 - d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: logoImage.cgImage!) + if let customLogoImage = customLogoImage, + let cgImage = customLogoImage.cgImage { + // 使用自定义Logo + print("应用自定义Logo,CGImage大小: \(cgImage.width) x \(cgImage.height)") + d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: cgImage) + } else if let selectedLogo = selectedLogo, + let logoImage = selectedLogo.image, + let cgImage = logoImage.cgImage { + // 使用预设Logo + print("应用预设Logo: \(selectedLogo.displayName)") + d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: cgImage) + } else { + print("没有设置任何Logo") } return d @@ -109,6 +128,22 @@ struct QRCodeStyleView: View { } } .onAppear { + checkPhotoLibraryPermission() + } + .sheet(isPresented: $showingImagePicker) { + ImagePicker { image in + imageToCrop = image + showingImageCropper = true + } + } + .sheet(isPresented: $showingImageCropper) { + if let imageToCrop = imageToCrop { + ImageCropperView(image: imageToCrop) { croppedImage in + customLogoImage = croppedImage + selectedLogo = nil // 清除预设Logo选择 + self.imageToCrop = nil + } + } } } @@ -330,6 +365,7 @@ struct QRCodeStyleView: View { // 无Logo选项 Button(action: { selectedLogo = nil + customLogoImage = nil }) { VStack(spacing: 8) { RoundedRectangle(cornerRadius: 12) @@ -349,18 +385,93 @@ struct QRCodeStyleView: View { .padding(12) .background( RoundedRectangle(cornerRadius: 16) - .fill(selectedLogo == nil ? Color.blue.opacity(0.1) : Color.clear) + .fill(selectedLogo == nil && customLogoImage == nil ? Color.blue.opacity(0.1) : Color.clear) .overlay( RoundedRectangle(cornerRadius: 16) - .stroke(selectedLogo == nil ? Color.blue : Color.clear, lineWidth: 3) + .stroke(selectedLogo == nil && customLogoImage == nil ? Color.blue : Color.clear, lineWidth: 3) ) ) } + // 自定义Logo选项 + if photoLibraryAccessGranted { + Button(action: { + showingImagePicker = true + }) { + VStack(spacing: 8) { + if let customLogoImage = customLogoImage { + Image(uiImage: customLogoImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 60, height: 60) + .background(Color.white) + .cornerRadius(12) + } else { + RoundedRectangle(cornerRadius: 12) + .fill(Color.blue.opacity(0.2)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: "photo.badge.plus") + .font(.title2) + .foregroundColor(.blue) + ) + } + + Text("自定义") + .font(.caption) + .foregroundColor(.primary) + .multilineTextAlignment(.center) + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(customLogoImage != nil ? Color.blue.opacity(0.1) : Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(customLogoImage != nil ? Color.blue : Color.clear, lineWidth: 3) + ) + ) + } + } else { + // 权限被拒绝时的处理 + Button(action: { + // 引导用户到设置页面 + if let settingsUrl = URL(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(settingsUrl) + } + }) { + VStack(spacing: 8) { + RoundedRectangle(cornerRadius: 12) + .fill(Color.red.opacity(0.2)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: "exclamationmark.triangle") + .font(.title2) + .foregroundColor(.red) + ) + + Text("需要权限") + .font(.caption) + .foregroundColor(.red) + .multilineTextAlignment(.center) + } + .padding(12) + .background( + RoundedRectangle(cornerRadius: 16) + .fill(Color.clear) + .overlay( + RoundedRectangle(cornerRadius: 16) + .stroke(Color.red.opacity(0.3), lineWidth: 1) + ) + ) + } + } + // Logo选项 ForEach(QRCodeLogo.allCases, id: \.self) { logo in Button(action: { selectedLogo = logo + customLogoImage = nil // 清除自定义Logo }) { VStack(spacing: 8) { if let image = loadImage(named: logo.thumbnailName) { @@ -466,6 +577,31 @@ struct QRCodeStyleView: View { } } + // MARK: - 权限检查 + private func checkPhotoLibraryPermission() { + let status = PHPhotoLibrary.authorizationStatus() + print("相册权限状态: \(status.rawValue)") + switch status { + case .authorized, .limited: + photoLibraryAccessGranted = true + print("相册权限已授权") + case .denied, .restricted: + photoLibraryAccessGranted = false + print("相册权限被拒绝") + case .notDetermined: + print("相册权限未确定,正在请求...") + PHPhotoLibrary.requestAuthorization { newStatus in + DispatchQueue.main.async { + self.photoLibraryAccessGranted = (newStatus == .authorized || newStatus == .limited) + print("权限请求结果: \(newStatus.rawValue), 授权状态: \(self.photoLibraryAccessGranted)") + } + } + @unknown default: + photoLibraryAccessGranted = false + print("相册权限未知状态") + } + } + // MARK: - 辅助函数 private func loadImage(named name: String) -> UIImage? { // 方法1: 尝试从Bundle中直接加载 @@ -504,6 +640,197 @@ struct QRCodeStyleView: View { } } +// MARK: - 图片裁剪视图 +struct ImageCropperView: View { + let image: UIImage + let onCropComplete: (UIImage) -> Void + + @Environment(\.dismiss) private var dismiss + @State private var scale: CGFloat = 1.0 + @State private var lastScale: CGFloat = 1.0 + @State private var offset: CGSize = .zero + @State private var lastOffset: CGSize = .zero + + var body: some View { + NavigationView { + GeometryReader { geometry in + ZStack { + Color.black + .ignoresSafeArea() + + VStack { + // 裁剪区域 + ZStack { + // 背景图片 + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .scaleEffect(scale) + .offset(offset) + .gesture( + SimultaneousGesture( + MagnificationGesture() + .onChanged { value in + let delta = value / lastScale + lastScale = value + scale = min(max(scale * delta, 0.5), 3.0) + } + .onEnded { _ in + lastScale = 1.0 + }, + DragGesture() + .onChanged { value in + let delta = CGSize( + width: value.translation.width - lastOffset.width, + height: value.translation.height - lastOffset.height + ) + lastOffset = value.translation + offset = CGSize( + width: offset.width + delta.width, + height: offset.height + delta.height + ) + } + .onEnded { _ in + lastOffset = .zero + } + ) + ) + + // 裁剪框 + CropOverlay() + } + .frame(height: geometry.size.width) // 保持正方形 + .clipped() + + Spacer() + + // 提示文字 + Text("拖动和缩放来选择圆形Logo区域") + .foregroundColor(.white) + .font(.caption) + .padding(.bottom, 20) + } + } + } + .navigationTitle("裁剪圆形Logo") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button("取消") { + dismiss() + } + .foregroundColor(.white) + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button("完成") { + let croppedImage = cropImage() + onCropComplete(croppedImage) + dismiss() + } + .foregroundColor(.white) + .font(.system(size: 16, weight: .semibold)) + } + } + } + } + + private func cropImage() -> UIImage { + let renderer = UIGraphicsImageRenderer(size: CGSize(width: 80, height: 80)) + + return renderer.image { context in + // 计算裁剪区域 + let imageSize = image.size + let viewSize = CGSize(width: 80, height: 80) + + // 计算图片在视图中的实际大小和位置 + let imageAspectRatio = imageSize.width / imageSize.height + let viewAspectRatio = viewSize.width / viewSize.height + + let scaledImageSize: CGSize + let scaledImageOffset: CGPoint + + if imageAspectRatio > viewAspectRatio { + // 图片更宽,以高度为准 + scaledImageSize = CGSize(width: viewSize.height * imageAspectRatio, height: viewSize.height) + scaledImageOffset = CGPoint(x: (viewSize.width - scaledImageSize.width) / 2, y: 0) + } else { + // 图片更高,以宽度为准 + scaledImageSize = CGSize(width: viewSize.width, height: viewSize.width / imageAspectRatio) + scaledImageOffset = CGPoint(x: 0, y: (viewSize.height - scaledImageSize.height) / 2) + } + + // 应用缩放和偏移 + let finalImageSize = CGSize( + width: scaledImageSize.width * scale, + height: scaledImageSize.height * scale + ) + + let finalImageOffset = CGPoint( + x: scaledImageOffset.x + offset.width, + y: scaledImageOffset.y + offset.height + ) + + // 计算裁剪区域 + let cropRect = CGRect( + x: -finalImageOffset.x, + y: -finalImageOffset.y, + width: finalImageSize.width, + height: finalImageSize.height + ) + + // 创建圆形裁剪路径 - 半径和正方形宽度一样 + let circlePath = UIBezierPath(ovalIn: CGRect(x: 0, y: 0, width: 80, height: 80)) + circlePath.addClip() + + // 绘制裁剪后的图片 + image.draw(in: cropRect) + } + } +} + +// MARK: - 裁剪覆盖层 +struct CropOverlay: View { + var body: some View { + GeometryReader { geometry in + ZStack { + // 半透明遮罩 + Color.black.opacity(0.5) + .mask( + Rectangle() + .overlay( + Circle() + .frame( + width: min(geometry.size.width, geometry.size.height) * 0.8, + height: min(geometry.size.width, geometry.size.height) * 0.8 + ) + .blendMode(.destinationOut) + ) + ) + + // 圆形裁剪框边框 + Circle() + .stroke(Color.white, lineWidth: 2) + .frame( + width: min(geometry.size.width, geometry.size.height) * 0.8, + height: min(geometry.size.width, geometry.size.height) * 0.8 + ) + + // 圆形裁剪框四角标记 + ForEach(0..<4) { corner in + Circle() + .fill(Color.white) + .frame(width: 6, height: 6) + .offset( + x: corner % 2 == 0 ? -min(geometry.size.width, geometry.size.height) * 0.4 : min(geometry.size.width, geometry.size.height) * 0.4, + y: corner < 2 ? -min(geometry.size.width, geometry.size.height) * 0.4 : min(geometry.size.width, geometry.size.height) * 0.4 + ) + } + } + } + } +} + // MARK: - 预览 #Preview { QRCodeStyleView(qrCodeContent: "https://www.example.com")