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