|
|
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 "colors".localized
|
|
|
case .dots: return "dot_types".localized
|
|
|
case .eyes: return "eyes".localized
|
|
|
case .logos: return "logo".localized
|
|
|
}
|
|
|
}
|
|
|
|
|
|
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
|
|
|
@EnvironmentObject var languageManager: LanguageManager
|
|
|
|
|
|
// 颜色选择
|
|
|
@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 showingSavedView = 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
|
|
|
|
|
|
// 如果有选择的Logo,设置Logo
|
|
|
if let customLogoImage = customLogoImage,
|
|
|
let cgImage = customLogoImage.cgImage {
|
|
|
// 使用自定义Logo
|
|
|
print("应用自定义Logo,CGImage大小: \(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("custom_style".localized)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
|
.toolbar {
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
|
Button("save".localized) {
|
|
|
saveQRCode()
|
|
|
}
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
.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)
|
|
|
)
|
|
|
}
|
|
|
.background(
|
|
|
NavigationLink(
|
|
|
destination: QRCodeSavedView(
|
|
|
qrCodeImage: generateQRCodeImage(),
|
|
|
qrCodeContent: qrCodeContent,
|
|
|
qrCodeType: qrCodeType,
|
|
|
styleData: createStyleData(),
|
|
|
historyItem: historyItem
|
|
|
),
|
|
|
isActive: $showingSavedView
|
|
|
) {
|
|
|
EmptyView()
|
|
|
}
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// 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: NSLocalizedString("foreground_color", comment: "Foreground color"),
|
|
|
colors: QRCodeColor.foregroundColors,
|
|
|
selectedColor: $selectedForegroundColor
|
|
|
)
|
|
|
|
|
|
// 背景色选择
|
|
|
colorSelectionSection(
|
|
|
title: NSLocalizedString("background_color", comment: "Background color"),
|
|
|
colors: QRCodeColor.backgroundColors,
|
|
|
selectedColor: $selectedBackgroundColor
|
|
|
)
|
|
|
}
|
|
|
.padding()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// MARK: - 点类型内容
|
|
|
private var dotsContent: some View {
|
|
|
ScrollView {
|
|
|
VStack(spacing: 16) {
|
|
|
Text("select_dot_type".localized)
|
|
|
.font(.title2)
|
|
|
.fontWeight(.bold)
|
|
|
.padding(.top)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
|
|
|
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("select_eye_type".localized)
|
|
|
.font(.title2)
|
|
|
.fontWeight(.bold)
|
|
|
.padding(.top)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
|
|
|
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("select_logo".localized)
|
|
|
.font(.title2)
|
|
|
.fontWeight(.bold)
|
|
|
.padding(.top)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
|
|
|
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("none".localized)
|
|
|
.font(.title2)
|
|
|
.foregroundColor(.secondary)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
)
|
|
|
|
|
|
Text("no_logo".localized)
|
|
|
.font(.caption)
|
|
|
.foregroundColor(.primary)
|
|
|
.multilineTextAlignment(.center)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
}
|
|
|
.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("custom".localized)
|
|
|
.font(.caption)
|
|
|
.foregroundColor(.primary)
|
|
|
.multilineTextAlignment(.center)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
}
|
|
|
.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("permission_required".localized)
|
|
|
.font(.caption)
|
|
|
.foregroundColor(.red)
|
|
|
.multilineTextAlignment(.center)
|
|
|
.id(languageManager.refreshTrigger)
|
|
|
}
|
|
|
.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() {
|
|
|
// 只保存到历史记录,不保存到相册
|
|
|
if historyItem != nil {
|
|
|
updateExistingHistory()
|
|
|
} else {
|
|
|
saveToHistory()
|
|
|
}
|
|
|
|
|
|
// 显示保存成功界面
|
|
|
showingSavedView = true
|
|
|
}
|
|
|
|
|
|
// 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 createStyleData() -> QRCodeStyleData {
|
|
|
var logoIdentifier: String? = nil
|
|
|
var hasCustomLogo = false
|
|
|
var customLogoFileName: String? = nil
|
|
|
|
|
|
if let customLogo = customLogoImage {
|
|
|
// 自定义Logo:保存到文件系统
|
|
|
let fileName = "custom_\(UUID().uuidString).png"
|
|
|
logoIdentifier = "custom_\(UUID().uuidString)"
|
|
|
hasCustomLogo = true
|
|
|
customLogoFileName = fileName
|
|
|
|
|
|
// 保存图片到文件系统
|
|
|
saveCustomLogoToFile(customLogo, fileName: fileName)
|
|
|
print("🖼️ 自定义Logo已保存到文件:\(fileName)")
|
|
|
} else if let selectedLogo = selectedLogo {
|
|
|
// 预设Logo
|
|
|
logoIdentifier = selectedLogo.rawValue
|
|
|
hasCustomLogo = false
|
|
|
print("🏷️ 使用预设Logo:\(selectedLogo.rawValue)")
|
|
|
}
|
|
|
|
|
|
return QRCodeStyleData(
|
|
|
foregroundColor: selectedForegroundColor.rawValue,
|
|
|
backgroundColor: selectedBackgroundColor.rawValue,
|
|
|
dotType: selectedDotType.rawValue,
|
|
|
eyeType: selectedEyeType.rawValue,
|
|
|
logo: logoIdentifier,
|
|
|
hasCustomLogo: hasCustomLogo,
|
|
|
customLogoFileName: customLogoFileName
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// 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)")
|
|
|
|
|
|
// 保存二维码样式数据
|
|
|
let styleData = self.createStyleData()
|
|
|
|
|
|
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
|
|
|
|
|
|
// 保存二维码样式数据
|
|
|
let styleData = self.createStyleData()
|
|
|
|
|
|
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: 尝试从Bundle的Resources目录加载
|
|
|
if Bundle.main.path(forResource: "Resources", ofType: nil) != 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
|
|
|
_ = 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)
|
|
|
.environmentObject(LanguageManager.shared)
|
|
|
}
|