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 2 months ago
parent 03a145f7d9
commit 9bd7effd4c

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

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

@ -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
// LogoLogo
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("应用自定义LogoCGImage大小: \(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")

Loading…
Cancel
Save