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.

main
v504 1 year ago
parent 03a145f7d9
commit 9bd7effd4c

@ -9,5 +9,9 @@
<string>en</string> <string>en</string>
<string>zh-Hans</string> <string>zh-Hans</string>
</array> </array>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要访问相册来选择自定义Logo图片</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>需要访问相册来保存生成的二维码图片</string>
</dict> </dict>
</plist> </plist>

@ -523,7 +523,8 @@ struct ImagePicker: UIViewControllerRepresentable {
let picker = UIImagePickerController() let picker = UIImagePickerController()
picker.delegate = context.coordinator picker.delegate = context.coordinator
picker.sourceType = .photoLibrary picker.sourceType = .photoLibrary
picker.allowsEditing = false picker.allowsEditing = true
picker.videoQuality = .typeHigh
return picker return picker
} }
@ -541,7 +542,9 @@ struct ImagePicker: UIViewControllerRepresentable {
} }
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { 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) parent.onImageSelected(image)
} }
picker.dismiss(animated: true) picker.dismiss(animated: true)

@ -1,6 +1,10 @@
import SwiftUI import SwiftUI
import QRCode import QRCode
import CoreData import CoreData
import Photos
#if canImport(PhotosUI)
import PhotosUI
#endif
// MARK: - 标签类型枚举 // MARK: - 标签类型枚举
enum TabType: String, CaseIterable { enum TabType: String, CaseIterable {
@ -46,6 +50,12 @@ struct QRCodeStyleView: View {
// Logo选择 // Logo选择
@State private var selectedLogo: QRCodeLogo? = nil @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? @State private var qrCodeImage: UIImage?
@ -81,10 +91,19 @@ struct QRCodeStyleView: View {
d.design.shape.eye = selectedEyeType.eyeShape d.design.shape.eye = selectedEyeType.eyeShape
// 如果有选择的Logo,设置Logo // 如果有选择的Logo,设置Logo
if let selectedLogo = selectedLogo, if let customLogoImage = customLogoImage,
let logoImage = selectedLogo.image { let cgImage = customLogoImage.cgImage {
// 设置Logo作为背景图片 // 使用自定义Logo
d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: logoImage.cgImage!) 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 return d
@ -109,6 +128,22 @@ struct QRCodeStyleView: View {
} }
} }
.onAppear { .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选项 // 无Logo选项
Button(action: { Button(action: {
selectedLogo = nil selectedLogo = nil
customLogoImage = nil
}) { }) {
VStack(spacing: 8) { VStack(spacing: 8) {
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
@ -349,18 +385,93 @@ struct QRCodeStyleView: View {
.padding(12) .padding(12)
.background( .background(
RoundedRectangle(cornerRadius: 16) 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( .overlay(
RoundedRectangle(cornerRadius: 16) 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选项 // Logo选项
ForEach(QRCodeLogo.allCases, id: \.self) { logo in ForEach(QRCodeLogo.allCases, id: \.self) { logo in
Button(action: { Button(action: {
selectedLogo = logo selectedLogo = logo
customLogoImage = nil // 清除自定义Logo
}) { }) {
VStack(spacing: 8) { VStack(spacing: 8) {
if let image = loadImage(named: logo.thumbnailName) { 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: - 辅助函数 // MARK: - 辅助函数
private func loadImage(named name: String) -> UIImage? { private func loadImage(named name: String) -> UIImage? {
// 方法1: 尝试从Bundle中直接加载 // 方法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: - 预览 // MARK: - 预览
#Preview { #Preview {
QRCodeStyleView(qrCodeContent: "https://www.example.com") QRCodeStyleView(qrCodeContent: "https://www.example.com")

Loading…
Cancel
Save