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")