You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1063 lines
44 KiB

import SwiftUI
import QRCode
import CoreData
import Photos
import Combine
internal import SwiftImageReadWrite
#if canImport(PhotosUI)
import PhotosUI
#endif
// MARK: -
enum TabType: String, CaseIterable {
case colors = "colors"
case dots = "dots"
case eyes = "eyes"
case logos = "logos"
var displayName: String {
switch self {
case .colors: return "颜色"
case .dots: return "点类型"
case .eyes: return "眼睛"
case .logos: return "Logo"
}
}
var iconName: String {
switch self {
case .colors: return "paintpalette"
case .dots: return "circle.grid.3x3"
case .eyes: return "eye"
case .logos: return "photo"
}
}
}
// MARK: -
struct QRCodeStyleView: View {
let qrCodeContent: String
let qrCodeType: QRCodeType
let existingStyleData: QRCodeStyleData? //
let historyItem: HistoryItem? //
@Environment(\.dismiss) private var dismiss
@EnvironmentObject var coreDataManager: CoreDataManager
//
@State private var selectedForegroundColor: QRCodeColor = .black
@State private var selectedBackgroundColor: QRCodeColor = .white
//
@State private var selectedDotType: QRCodeDotType = .square
//
@State private var selectedEyeType: QRCodeEyeType = .square
// Logo
@State private var selectedLogo: QRCodeLogo? = nil
@State private var customLogoImage: UIImage? = nil
@State private var photoLibraryAccessGranted = false
@State private var showingImagePicker = false
//
@State private var isLoading = false
//
@State private var selectedTabType: TabType = .colors
// QRCode
private func createQRCodeDocument() -> QRCode.Document {
let d = try! QRCode.Document(engine: QRCodeEngineExternal())
// 使
d.utf8String = qrCodeContent
//
d.design.backgroundColor(selectedBackgroundColor.cgColor)
//
d.design.style.eye = QRCode.FillStyle.Solid(selectedForegroundColor.cgColor)
d.design.style.eyeBackground = selectedBackgroundColor.cgColor
//
d.design.shape.onPixels = selectedDotType.pixelShape
d.design.style.onPixels = QRCode.FillStyle.Solid(selectedForegroundColor.cgColor)
d.design.style.onPixelsBackground = selectedBackgroundColor.cgColor
d.design.shape.offPixels = selectedDotType.pixelShape
d.design.style.offPixels = QRCode.FillStyle.Solid(selectedBackgroundColor.cgColor)
d.design.style.offPixelsBackground = selectedBackgroundColor.cgColor
//
d.design.shape.eye = selectedEyeType.eyeShape
// LogoLogo
if let customLogoImage = customLogoImage,
let cgImage = customLogoImage.cgImage {
// 使Logo
print("应用自定义LogoCGImage大小: \(cgImage.width) x \(cgImage.height)")
d.logoTemplate = QRCode.LogoTemplate.CircleCenter(image: cgImage, inset: 0)
} 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
}
var body: some View {
VStack(spacing: 0) {
//
qrCodePreviewSection
//
styleSelectionSection
}
.navigationTitle("自定义样式")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("保存") {
saveQRCode()
}
.font(.system(size: 16, weight: .semibold))
}
}
.onAppear {
checkPhotoLibraryPermission()
initializeExistingStyle()
}
.sheet(isPresented: $showingImagePicker) {
ImagePicker(
onImageSelected: { image in
customLogoImage = image
selectedLogo = nil // Logo
},
shouldProcessImage: true,
targetSize: CGSize(width: 80, height: 80)
)
}
}
// MARK: -
private var qrCodePreviewSection: some View {
VStack(spacing: 16) {
QRCodeDocumentUIView(document: createQRCodeDocument())
.frame(width: 300, height: 300)
}
.padding()
.background(Color(.systemBackground))
}
// MARK: -
private var styleSelectionSection: some View {
VStack(spacing: 0) {
//
tabTypeSelection
//
contentArea
}
.background(Color(.systemGroupedBackground))
}
// MARK: -
private var tabTypeSelection: some View {
HStack(spacing: 0) {
ForEach(TabType.allCases, id: \.self) { tabType in
Button(action: {
selectedTabType = tabType
}) {
VStack(spacing: 4) {
Image(systemName: tabType.iconName)
.font(.system(size: 20))
.foregroundColor(selectedTabType == tabType ? .blue : .gray)
Text(tabType.displayName)
.font(.caption)
.foregroundColor(selectedTabType == tabType ? .blue : .gray)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.background(
Rectangle()
.fill(selectedTabType == tabType ? Color.blue.opacity(0.1) : Color.clear)
)
}
}
}
.background(Color(.systemBackground))
.overlay(
Rectangle()
.frame(height: 1)
.foregroundColor(Color(.separator)),
alignment: .bottom
)
}
// MARK: -
private var contentArea: some View {
Group {
switch selectedTabType {
case .colors:
colorsContent
case .dots:
dotsContent
case .eyes:
eyesContent
case .logos:
logosContent
}
}
.frame(maxHeight: 400)
}
// MARK: -
private var colorsContent: some View {
ScrollView {
VStack(spacing: 24) {
//
colorSelectionSection(
title: "前景色",
colors: QRCodeColor.foregroundColors,
selectedColor: $selectedForegroundColor
)
//
colorSelectionSection(
title: "背景色",
colors: QRCodeColor.backgroundColors,
selectedColor: $selectedBackgroundColor
)
}
.padding()
}
}
// MARK: -
private var dotsContent: some View {
ScrollView {
VStack(spacing: 16) {
Text("选择点类型")
.font(.title2)
.fontWeight(.bold)
.padding(.top)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
ForEach(QRCodeDotType.allCases, id: \.self) { dotType in
Button(action: {
selectedDotType = dotType
}) {
VStack(spacing: 8) {
if let image = loadImage(named: dotType.thumbnailName) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.background(Color.white)
.cornerRadius(12)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay(
Text("?")
.font(.title2)
.foregroundColor(.secondary)
)
}
Text(dotType.displayName)
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(selectedDotType == dotType ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(selectedDotType == dotType ? Color.blue : Color.clear, lineWidth: 3)
)
)
}
}
}
.padding(.horizontal)
}
}
}
// MARK: -
private var eyesContent: some View {
ScrollView {
VStack(spacing: 16) {
Text("选择眼睛类型")
.font(.title2)
.fontWeight(.bold)
.padding(.top)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
ForEach(QRCodeEyeType.allCases, id: \.self) { eyeType in
Button(action: {
selectedEyeType = eyeType
}) {
VStack(spacing: 8) {
if let image = loadImage(named: eyeType.thumbnailName) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.background(Color.white)
.cornerRadius(12)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay(
Text("?")
.font(.title2)
.foregroundColor(.secondary)
)
}
Text(eyeType.displayName)
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(selectedEyeType == eyeType ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(selectedEyeType == eyeType ? Color.blue : Color.clear, lineWidth: 3)
)
)
}
}
}
.padding(.horizontal)
}
}
}
// MARK: - Logo
private var logosContent: some View {
ScrollView {
VStack(spacing: 16) {
Text("选择Logo")
.font(.title2)
.fontWeight(.bold)
.padding(.top)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 3), spacing: 16) {
// Logo
Button(action: {
selectedLogo = nil
customLogoImage = nil
}) {
VStack(spacing: 8) {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay(
Text("")
.font(.title2)
.foregroundColor(.secondary)
)
Text("无Logo")
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(selectedLogo == nil && customLogoImage == nil ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.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) {
Image(uiImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 60, height: 60)
.background(Color.white)
.cornerRadius(12)
} else {
RoundedRectangle(cornerRadius: 12)
.fill(Color.gray.opacity(0.3))
.frame(width: 60, height: 60)
.overlay(
Text("?")
.font(.title2)
.foregroundColor(.secondary)
)
}
Text(logo.displayName)
.font(.caption)
.foregroundColor(.primary)
.multilineTextAlignment(.center)
}
.padding(12)
.background(
RoundedRectangle(cornerRadius: 16)
.fill(selectedLogo == logo ? Color.blue.opacity(0.1) : Color.clear)
.overlay(
RoundedRectangle(cornerRadius: 16)
.stroke(selectedLogo == logo ? Color.blue : Color.clear, lineWidth: 3)
)
)
}
}
}
.padding(.horizontal)
}
}
}
// MARK: -
private func colorSelectionSection(
title: String,
colors: [QRCodeColor],
selectedColor: Binding<QRCodeColor>
) -> some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
.foregroundColor(.primary)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: 12) {
ForEach(colors, id: \.self) { color in
Button(action: {
selectedColor.wrappedValue = color
}) {
RoundedRectangle(cornerRadius: 8)
.fill(color.color)
.frame(height: 40)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(selectedColor.wrappedValue == color ? Color.blue : Color.clear, lineWidth: 3)
)
}
}
}
}
}
// MARK: -
private func initializeExistingStyle() {
guard let existingStyle = existingStyleData else { return }
//
if let foregroundColor = QRCodeColor(rawValue: existingStyle.foregroundColor) {
selectedForegroundColor = foregroundColor
}
if let backgroundColor = QRCodeColor(rawValue: existingStyle.backgroundColor) {
selectedBackgroundColor = backgroundColor
}
if let dotType = QRCodeDotType(rawValue: existingStyle.dotType) {
selectedDotType = dotType
}
if let eyeType = QRCodeEyeType(rawValue: existingStyle.eyeType) {
selectedEyeType = eyeType
}
// Logo
if existingStyle.hasCustomLogo {
// Logo
customLogoImage = existingStyle.customLogoImage
} else if let logoString = existingStyle.logo,
let logo = QRCodeLogo(rawValue: logoString) {
selectedLogo = logo
}
}
// MARK: -
private func saveQRCode() {
//
let qrCodeImage = generateQRCodeImage()
//
UIImageWriteToSavedPhotosAlbum(qrCodeImage, nil, nil, nil)
//
if historyItem != nil {
updateExistingHistory()
} else {
saveToHistory()
}
dismiss()
}
// MARK: -
private func generateQRCodeImage() -> UIImage {
do {
let imageData = try createQRCodeDocument().pngData(dimension: 600)
return UIImage(data: imageData) ?? UIImage()
} catch {
print("生成二维码图片失败:\(error.localizedDescription)")
return UIImage()
}
}
// MARK: -
private func saveToHistory() {
// 线Core Data
DispatchQueue.main.async {
do {
let context = self.coreDataManager.container.viewContext
let historyItem = HistoryItem(context: context)
historyItem.id = UUID()
historyItem.dataType = DataType.qrcode.rawValue
historyItem.dataSource = DataSource.created.rawValue
historyItem.createdAt = Date()
historyItem.isFavorite = false
historyItem.qrCodeType = self.qrCodeType.rawValue
historyItem.content = self.qrCodeContent
print("📝 创建历史记录项:\(self.qrCodeContent)")
//
var logoIdentifier: String? = nil
var hasCustomLogo = false
var customLogoFileName: String? = nil
if let customLogo = self.customLogoImage {
// Logo
let fileName = "custom_\(UUID().uuidString).png"
logoIdentifier = "custom_\(UUID().uuidString)"
hasCustomLogo = true
customLogoFileName = fileName
//
self.saveCustomLogoToFile(customLogo, fileName: fileName)
print("🖼️ 自定义Logo已保存到文件\(fileName)")
} else if let selectedLogo = self.selectedLogo {
// Logo
logoIdentifier = selectedLogo.rawValue
hasCustomLogo = false
print("🏷️ 使用预设Logo\(selectedLogo.rawValue)")
}
let styleData = QRCodeStyleData(
foregroundColor: self.selectedForegroundColor.rawValue,
backgroundColor: self.selectedBackgroundColor.rawValue,
dotType: self.selectedDotType.rawValue,
eyeType: self.selectedEyeType.rawValue,
logo: logoIdentifier,
hasCustomLogo: hasCustomLogo,
customLogoFileName: customLogoFileName
)
print("🎨 样式数据创建成功:\(styleData.styleDescription)")
// JSON
do {
let jsonData = try JSONEncoder().encode(styleData)
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
historyItem.qrCodeStyleData = jsonString
print("✅ 样式数据已转换为JSON并设置到历史记录项")
print("📄 JSON字符串长度\(jsonString.count)")
} catch {
print("❌ 样式数据JSON编码失败\(error)")
}
//
if let savedJsonString = historyItem.qrCodeStyleData {
print("✅ 样式数据验证成功JSON字符串长度 \(savedJsonString.count)")
} else {
print("❌ 样式数据验证失败:数据未正确设置")
}
// Core Data
try context.save()
print("✅ 自定义二维码保存成功:\(self.qrCodeContent)")
//
self.coreDataManager.objectWillChange.send()
} catch {
print("❌ Core Data保存失败\(error.localizedDescription)")
print("❌ 错误详情:\(error)")
// NSError
if let nsError = error as NSError? {
print("❌ 错误域:\(nsError.domain)")
print("❌ 错误代码:\(nsError.code)")
print("❌ 用户信息:\(nsError.userInfo)")
}
}
}
}
// MARK: -
private func updateExistingHistory() {
guard let existingHistoryItem = historyItem else { return }
// 线Core Data
DispatchQueue.main.async {
do {
let context = self.coreDataManager.container.viewContext
//
var logoIdentifier: String? = nil
var hasCustomLogo = false
var customLogoFileName: String? = nil
if let customLogo = self.customLogoImage {
// Logo
let fileName = "custom_\(UUID().uuidString).png"
logoIdentifier = "custom_\(UUID().uuidString)"
hasCustomLogo = true
customLogoFileName = fileName
//
self.saveCustomLogoToFile(customLogo, fileName: fileName)
print("🖼️ 自定义Logo已保存到文件\(fileName)")
} else if let selectedLogo = self.selectedLogo {
// Logo
logoIdentifier = selectedLogo.rawValue
hasCustomLogo = false
print("🏷️ 使用预设Logo\(selectedLogo.rawValue)")
}
let styleData = QRCodeStyleData(
foregroundColor: self.selectedForegroundColor.rawValue,
backgroundColor: self.selectedBackgroundColor.rawValue,
dotType: self.selectedDotType.rawValue,
eyeType: self.selectedEyeType.rawValue,
logo: logoIdentifier,
hasCustomLogo: hasCustomLogo,
customLogoFileName: customLogoFileName
)
print("🎨 样式数据更新成功:\(styleData.styleDescription)")
// JSON
do {
let jsonData = try JSONEncoder().encode(styleData)
let jsonString = String(data: jsonData, encoding: .utf8) ?? ""
existingHistoryItem.qrCodeStyleData = jsonString
print("✅ 样式数据已更新到历史记录项")
} catch {
print("❌ 样式数据JSON编码失败\(error)")
}
// Core Data
try context.save()
print("✅ 二维码样式更新成功")
//
self.coreDataManager.objectWillChange.send()
} catch {
print("❌ Core Data更新失败\(error.localizedDescription)")
print("❌ 错误详情:\(error)")
// NSError
if let nsError = error as NSError? {
print("❌ 错误域:\(nsError.domain)")
print("❌ 错误代码:\(nsError.code)")
print("❌ 用户信息:\(nsError.userInfo)")
}
}
}
}
// 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: - Logo
private func saveCustomLogoToFile(_ image: UIImage, fileName: String) {
//
let maxSize: CGFloat = 200 //
let resizedImage = resizeImage(image, to: CGSize(width: maxSize, height: maxSize))
guard let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
print("❌ 无法获取文档目录")
return
}
let customLogosPath = documentsPath.appendingPathComponent("CustomLogos")
// CustomLogos
do {
try FileManager.default.createDirectory(at: customLogosPath, withIntermediateDirectories: true, attributes: nil)
} catch {
print("❌ 创建CustomLogos目录失败\(error)")
return
}
//
let imagePath = customLogosPath.appendingPathComponent(fileName)
if let imageData = resizedImage.pngData() {
do {
try imageData.write(to: imagePath)
print("✅ 自定义Logo保存成功\(fileName)")
} catch {
print("❌ 保存自定义Logo失败\(error)")
}
} else {
print("❌ 转换图片数据失败")
}
}
// MARK: -
private func resizeImage(_ image: UIImage, to size: CGSize) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { context in
image.draw(in: CGRect(origin: .zero, size: size))
}
}
// MARK: -
private func loadImage(named name: String) -> UIImage? {
// 1: Bundle
if let image = UIImage(named: name) {
return image
}
// 2: Resources
let subdirectories = ["dots", "eyes", "logos"]
for subdirectory in subdirectories {
if let path = Bundle.main.path(forResource: name, ofType: "png", inDirectory: "Resources/\(subdirectory)") {
return UIImage(contentsOfFile: path)
}
}
// 3: BundleResources
if let bundlePath = Bundle.main.path(forResource: "Resources", ofType: nil) {
for subdirectory in subdirectories {
if let imagePath = Bundle.main.path(forResource: name, ofType: "png", inDirectory: subdirectory) {
return UIImage(contentsOfFile: imagePath)
}
}
}
// 4: Assets.xcassets
if let image = UIImage(named: name, in: Bundle.main, with: nil) {
return image
}
// 5: Bundle
if let path = Bundle.main.path(forResource: name, ofType: "png") {
return UIImage(contentsOfFile: path)
}
return nil
}
}
// MARK: -
struct ImagePicker: UIViewControllerRepresentable {
let onImageSelected: (UIImage) -> Void
let shouldProcessImage: Bool
let targetSize: CGSize?
init(onImageSelected: @escaping (UIImage) -> Void, shouldProcessImage: Bool = false, targetSize: CGSize? = nil) {
self.onImageSelected = onImageSelected
self.shouldProcessImage = shouldProcessImage
self.targetSize = targetSize
}
func makeUIViewController(context: Context) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
picker.sourceType = .photoLibrary
picker.allowsEditing = false
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[.originalImage] as? UIImage {
let finalImage: UIImage
if parent.shouldProcessImage, let targetSize = parent.targetSize {
//
finalImage = processImageToSquare(image: image, targetSize: targetSize)
//
let memorySize = calculateImageMemorySize(image: finalImage)
print("📊 最终自定义图片内存大小: \(memorySize)")
} else {
// 使
finalImage = image
}
parent.onImageSelected(finalImage)
}
picker.dismiss(animated: true)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true)
}
// 5KB
private func processImageToSquare(image: UIImage, targetSize: CGSize) -> UIImage {
let originalSize = image.size
//
let squareSize = min(originalSize.width, originalSize.height)
//
let cropX = (originalSize.width - squareSize) / 2
let cropY = (originalSize.height - squareSize) / 2
let cropRect = CGRect(x: cropX, y: cropY, width: squareSize, height: squareSize)
//
guard let cgImage = image.cgImage?.cropping(to: cropRect) else {
return image
}
// UIImage
let croppedImage = UIImage(cgImage: cgImage)
//
let renderer = UIGraphicsImageRenderer(size: targetSize)
let scaledImage = renderer.image { context in
croppedImage.draw(in: CGRect(origin: .zero, size: targetSize))
}
// 5KB
return compressImageToTargetSize(scaledImage, targetSizeInKB: 5.0)
}
//
private func compressImageToTargetSize(_ image: UIImage, targetSizeInKB: Double) -> UIImage {
let targetSizeInBytes = Int64(targetSizeInKB * 1024)
//
let compressionQualities: [CGFloat] = [0.8, 0.6, 0.4, 0.2, 0.1, 0.05]
for quality in compressionQualities {
if let imageData = image.jpegData(compressionQuality: quality) {
let dataSize = Int64(imageData.count)
if dataSize <= targetSizeInBytes {
// UIImage
if let compressedImage = UIImage(data: imageData) {
print("✅ 图片压缩成功: \(dataSize) bytes (质量: \(quality))")
return compressedImage
}
}
}
}
// JPEG
print("⚠️ JPEG压缩后仍超过目标大小尝试缩小尺寸")
return compressImageByReducingSize(image, targetSizeInBytes: targetSizeInBytes)
}
//
private func compressImageByReducingSize(_ image: UIImage, targetSizeInBytes: Int64) -> UIImage {
let originalSize = image.size
let originalWidth = originalSize.width
let originalHeight = originalSize.height
//
let currentMemorySize = Int64(originalWidth * originalHeight * 4) // RGBA
//
let scaleFactor = sqrt(Double(targetSizeInBytes) / Double(currentMemorySize))
let newWidth = max(originalWidth * CGFloat(scaleFactor), 40) // 40
let newHeight = max(originalHeight * CGFloat(scaleFactor), 40)
let newSize = CGSize(width: newWidth, height: newHeight)
//
let renderer = UIGraphicsImageRenderer(size: newSize)
let resizedImage = renderer.image { context in
image.draw(in: CGRect(origin: .zero, size: newSize))
}
// JPEG
if let imageData = resizedImage.jpegData(compressionQuality: 0.3) {
let finalSize = Int64(imageData.count)
print("✅ 通过缩小尺寸压缩成功: \(finalSize) bytes (新尺寸: \(newWidth) x \(newHeight))")
if let finalImage = UIImage(data: imageData) {
return finalImage
}
}
//
print("⚠️ 无法压缩到目标大小,返回最小尺寸图片")
let minSize = CGSize(width: 40, height: 40)
let rendererMin = UIGraphicsImageRenderer(size: minSize)
return rendererMin.image { context in
image.draw(in: CGRect(origin: .zero, size: minSize))
}
}
//
private func calculateImageMemorySize(image: UIImage) -> String {
guard let cgImage = image.cgImage else {
return "无法计算"
}
let width = cgImage.width
let height = cgImage.height
let bitsPerComponent = cgImage.bitsPerComponent
let bytesPerRow = cgImage.bytesPerRow
let colorSpace = cgImage.colorSpace
//
let memorySizeInBytes = height * bytesPerRow
//
let formatter = ByteCountFormatter()
formatter.allowedUnits = [.useKB, .useMB]
formatter.countStyle = .memory
let memorySizeString = formatter.string(fromByteCount: Int64(memorySizeInBytes))
//
return "\(memorySizeString) (\(width) x \(height), \(bitsPerComponent) bits/component, \(bytesPerRow) bytes/row)"
}
}
}
// MARK: -
#Preview {
QRCodeStyleView(qrCodeContent: "https://www.example.com", qrCodeType: .url, existingStyleData: nil, historyItem: nil)
.environmentObject(CoreDataManager.shared)
}