diff --git a/MyQrCode/Utils/BarcodeGenerator.swift b/MyQrCode/Utils/BarcodeGenerator.swift new file mode 100644 index 0000000..e3b87a7 --- /dev/null +++ b/MyQrCode/Utils/BarcodeGenerator.swift @@ -0,0 +1,274 @@ +import Foundation +import UIKit +import CoreImage.CIFilterBuiltins + +// MARK: - 条形码生成器 +class BarcodeGenerator { + + // MARK: - 单例 + static let shared = BarcodeGenerator() + + private init() {} + + // MARK: - 生成条形码图片 + /// 根据内容和类型生成条形码图片 + /// - Parameters: + /// - content: 条形码内容 + /// - type: 条形码类型 + /// - size: 图片尺寸,默认 CGSize(width: 300, height: 120) + /// - Returns: 生成的条形码图片 + func generateBarcode(from content: String, type: String, size: CGSize = CGSize(width: 300, height: 120), showText: Bool = true) -> UIImage? { + guard !content.isEmpty else { return nil } + + // 根据条形码类型选择合适的生成方式 + switch type.lowercased() { + case let t where t.contains("ean13"): + return generateEAN13Barcode(from: content, size: size, showText: showText) + case let t where t.contains("ean8"): + return generateEAN8Barcode(from: content, size: size, showText: showText) + case let t where t.contains("upce"): + return generateUPCEBarcode(from: content, size: size, showText: showText) + case let t where t.contains("code39"): + return generateCode39Barcode(from: content, size: size, showText: showText) + case let t where t.contains("code128"): + return generateCode128Barcode(from: content, size: size, showText: showText) + case let t where t.contains("pdf417"): + return generatePDF417Barcode(from: content, size: size, showText: showText) + case let t where t.contains("itf14"): + return generateITF14Barcode(from: content, size: size, showText: showText) + default: + // 默认使用 Code 128 + return generateCode128Barcode(from: content, size: size, showText: showText) + } + } + + // MARK: - EAN-13 条形码生成 + private func generateEAN13Barcode(from content: String, size: CGSize, showText: Bool) -> UIImage? { + let context = CIContext() + let filter = CIFilter.code128BarcodeGenerator() + + // EAN-13 需要13位数字 + let paddedContent = content.padding(toLength: 13, withPad: "0", startingAt: 0) + filter.message = Data(paddedContent.utf8) + filter.quietSpace = 7.0 + + return processBarcodeFilter(filter, context: context, size: size, content: paddedContent, showText: showText) + } + + // MARK: - EAN-8 条形码生成 + private func generateEAN8Barcode(from content: String, size: CGSize, showText: Bool) -> UIImage? { + let context = CIContext() + let filter = CIFilter.code128BarcodeGenerator() + + // EAN-8 需要8位数字 + let paddedContent = content.padding(toLength: 8, withPad: "0", startingAt: 0) + filter.message = Data(paddedContent.utf8) + filter.quietSpace = 7.0 + + return processBarcodeFilter(filter, context: context, size: size, content: paddedContent, showText: showText) + } + + // MARK: - UPC-E 条形码生成 + private func generateUPCEBarcode(from content: String, size: CGSize, showText: Bool) -> UIImage? { + let context = CIContext() + let filter = CIFilter.code128BarcodeGenerator() + + // UPC-E 需要8位数字 + let paddedContent = content.padding(toLength: 8, withPad: "0", startingAt: 0) + filter.message = Data(paddedContent.utf8) + filter.quietSpace = 7.0 + + return processBarcodeFilter(filter, context: context, size: size, content: paddedContent, showText: showText) + } + + // MARK: - Code 39 条形码生成 + private func generateCode39Barcode(from content: String, size: CGSize, showText: Bool) -> UIImage? { + let context = CIContext() + let filter = CIFilter.code128BarcodeGenerator() + + // Code 39 支持字母数字 + filter.message = Data(content.utf8) + filter.quietSpace = 7.0 + + return processBarcodeFilter(filter, context: context, size: size, content: content, showText: showText) + } + + // MARK: - Code 128 条形码生成 + private func generateCode128Barcode(from content: String, size: CGSize, showText: Bool) -> UIImage? { + let context = CIContext() + let filter = CIFilter.code128BarcodeGenerator() + + filter.message = Data(content.utf8) + filter.quietSpace = 7.0 + + return processBarcodeFilter(filter, context: context, size: size, content: content, showText: showText) + } + + // MARK: - PDF417 条形码生成 + private func generatePDF417Barcode(from content: String, size: CGSize, showText: Bool) -> UIImage? { + let context = CIContext() + let filter = CIFilter.pdf417BarcodeGenerator() + + filter.message = Data(content.utf8) + filter.minWidth = 3 + filter.maxWidth = 3 + filter.minHeight = 3 + filter.maxHeight = 3 + + return processBarcodeFilter(filter, context: context, size: size, content: content, showText: showText) + } + + // MARK: - ITF-14 条形码生成 + private func generateITF14Barcode(from content: String, size: CGSize, showText: Bool) -> UIImage? { + let context = CIContext() + let filter = CIFilter.code128BarcodeGenerator() + + // ITF-14 需要14位数字 + let paddedContent = content.padding(toLength: 14, withPad: "0", startingAt: 0) + filter.message = Data(paddedContent.utf8) + filter.quietSpace = 7.0 + + return processBarcodeFilter(filter, context: context, size: size, content: paddedContent, showText: showText) + } + + // MARK: - 处理条形码滤镜 + private func processBarcodeFilter(_ filter: CIFilter, context: CIContext, size: CGSize, content: String, showText: Bool) -> UIImage? { + guard let outputImage = filter.outputImage else { return nil } + + // 计算缩放比例以适应目标尺寸 + let scaleX = size.width / outputImage.extent.width + let scaleY = size.height / outputImage.extent.height + + // 应用缩放变换 + let transform = CGAffineTransform(scaleX: scaleX, y: scaleY) + let scaledImage = outputImage.transformed(by: transform) + + // 转换为 UIImage + guard let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) else { return nil } + + let baseImage = UIImage(cgImage: cgImage) + + // 如果需要显示文本,在条形码下方添加文本标签 + if showText { + return addTextLabel(to: baseImage, content: content, size: size) + } + + return baseImage + } + + // MARK: - 添加文本标签 + private func addTextLabel(to image: UIImage, content: String, size: CGSize) -> UIImage? { + let renderer = UIGraphicsImageRenderer(size: size) + + return renderer.image { context in + // 绘制原始条形码图片 + image.draw(in: CGRect(origin: .zero, size: size)) + + // 设置文本属性 + let textAttributes: [NSAttributedString.Key: Any] = [ + .font: UIFont.systemFont(ofSize: 12, weight: .medium), + .foregroundColor: UIColor.black + ] + + // 计算每个字符的位置,让每个数字对应其条形码位置 + let charWidth = size.width / CGFloat(content.count) + let textY = size.height - 20 // 距离底部20像素 + + // 为每个字符绘制背景和文本 + for (index, char) in content.enumerated() { + let charString = String(char) + let charSize = charString.size(withAttributes: textAttributes) + + // 计算字符的X位置(居中对齐到对应的条形码区域) + let charX = CGFloat(index) * charWidth + (charWidth - charSize.width) / 2 + + // 绘制字符背景(白色背景,确保可读性) + let backgroundRect = CGRect( + x: charX - 2, + y: textY - 2, + width: charSize.width + 4, + height: charSize.height + 4 + ) + + UIColor.white.setFill() + context.fill(backgroundRect) + + // 绘制字符 + charString.draw(at: CGPoint(x: charX, y: textY), withAttributes: textAttributes) + } + } + } + + // MARK: - 获取条形码图标 + /// 根据条形码类型获取对应的图标名称 + /// - Parameter type: 条形码类型 + /// - Returns: 图标名称 + func getBarcodeIcon(for type: String) -> String { + let lowercasedType = type.lowercased() + + if lowercasedType.contains("ean") || lowercasedType.contains("upc") { + return "barcode" + } else if lowercasedType.contains("code39") || lowercasedType.contains("code128") { + return "barcode.viewfinder" + } else if lowercasedType.contains("pdf417") { + return "qrcode.viewfinder" + } else if lowercasedType.contains("itf14") { + return "barcode" + } else { + return "barcode" + } + } + + // MARK: - 验证条形码内容 + /// 验证条形码内容是否符合类型要求 + /// - Parameters: + /// - content: 条形码内容 + /// - type: 条形码类型 + /// - Returns: 是否有效 + func validateBarcodeContent(_ content: String, type: String) -> Bool { + guard !content.isEmpty else { return false } + + let lowercasedType = type.lowercased() + + switch lowercasedType { + case let t where t.contains("ean13"): + return content.count == 13 && content.allSatisfy { $0.isNumber } + case let t where t.contains("ean8"): + return content.count == 8 && content.allSatisfy { $0.isNumber } + case let t where t.contains("upce"): + return content.count == 8 && content.allSatisfy { $0.isNumber } + case let t where t.contains("itf14"): + return content.count == 14 && content.allSatisfy { $0.isNumber } + case let t where t.contains("code39"): + // Code 39 支持字母、数字和一些特殊字符 + return content.allSatisfy { $0.isLetter || $0.isNumber || " -.$/+%".contains($0) } + case let t where t.contains("code128"): + // Code 128 支持 ASCII 字符 + return content.allSatisfy { $0.asciiValue != nil } + case let t where t.contains("pdf417"): + // PDF417 支持二进制数据 + return true + default: + return true + } + } +} + +// MARK: - String 扩展 +extension String { + /// 将字符串填充到指定长度 + /// - Parameters: + /// - length: 目标长度 + /// - pad: 填充字符 + /// - startingAt: 开始位置 + /// - Returns: 填充后的字符串 + func padding(toLength length: Int, withPad pad: String, startingAt startIndex: Int) -> String { + if self.count >= length { + return String(self.prefix(length)) + } + + let paddingLength = length - self.count + let padding = String(repeating: pad, count: paddingLength) + return self + padding + } +} \ No newline at end of file diff --git a/MyQrCode/Views/BarcodeDetailView.swift b/MyQrCode/Views/BarcodeDetailView.swift new file mode 100644 index 0000000..9ea6739 --- /dev/null +++ b/MyQrCode/Views/BarcodeDetailView.swift @@ -0,0 +1,352 @@ +import SwiftUI +import CoreData + +struct BarcodeDetailView: View { + let historyItem: HistoryItem + @StateObject private var coreDataManager = CoreDataManager.shared + @State private var barcodeImage: UIImage? + @State private var showingShareSheet = false + @State private var showingAlert = false + @State private var alertMessage = "" + + var body: some View { + ScrollView { + VStack(spacing: 20) { + // 条形码图片 + barcodeImageView + + // 条形码类型信息 + barcodeTypeSection + + // 条形码内容信息 + barcodeContentSection + + // 原始内容 + originalContentSection + + // 操作按钮 + actionButtonsSection + } + .padding() + } + .navigationTitle("条形码详情") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + showingShareSheet = true + }) { + Image(systemName: "square.and.arrow.up") + } + } + } + .onAppear { + generateBarcodeImage() + } + .sheet(isPresented: $showingShareSheet) { + ShareSheet(activityItems: [historyItem.content ?? ""]) + } + .alert("提示", isPresented: $showingAlert) { + Button("确定") { } + } message: { + Text(alertMessage) + } + } + + // MARK: - 条形码图片视图 + private var barcodeImageView: some View { + VStack(spacing: 16) { + if let barcodeImage = barcodeImage { + Image(uiImage: barcodeImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 120) + .cornerRadius(12) + .shadow(radius: 8) + } else { + RoundedRectangle(cornerRadius: 12) + .fill(Color.gray.opacity(0.3)) + .frame(height: 120) + .overlay( + ProgressView() + .scaleEffect(1.5) + ) + } + + Text("扫描此条形码") + .font(.caption) + .foregroundColor(.secondary) + } + } + + // MARK: - 条形码类型信息 + private var barcodeTypeSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "barcode") + .font(.title2) + .foregroundColor(.green) + + Text("条形码类型") + .font(.headline) + + Spacer() + } + + if let barcodeTypeString = historyItem.barcodeType { + HStack { + Image(systemName: getBarcodeIcon(for: barcodeTypeString)) + .font(.title3) + .foregroundColor(.green) + + Text(barcodeTypeString) + .font(.title3) + .fontWeight(.medium) + + Spacer() + } + .padding() + .background(Color.green.opacity(0.1)) + .cornerRadius(8) + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + + // MARK: - 条形码内容信息 + private var barcodeContentSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "info.circle") + .font(.title2) + .foregroundColor(.blue) + + Text("条形码内容") + .font(.headline) + + Spacer() + } + + if let content = historyItem.content { + VStack(alignment: .leading, spacing: 8) { + HStack { + Image(systemName: "number") + .font(.title3) + .foregroundColor(.blue) + + Text("内容长度: \(content.count) 字符") + .font(.title3) + .fontWeight(.medium) + + Spacer() + } + + Text("数据内容") + .font(.subheadline) + .foregroundColor(.secondary) + .padding(.top, 4) + } + .padding() + .background(Color.blue.opacity(0.1)) + .cornerRadius(8) + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + + // MARK: - 原始内容 + private var originalContentSection: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "doc.text") + .font(.title2) + .foregroundColor(.purple) + + Text("原始内容") + .font(.headline) + + Spacer() + } + + if let content = historyItem.content { + ScrollView { + Text(content) + .font(.system(.body, design: .monospaced)) + .foregroundColor(.secondary) + .multilineTextAlignment(.leading) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(maxHeight: 200) + .background(Color.purple.opacity(0.1)) + .cornerRadius(8) + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + + // MARK: - 操作按钮 + private var actionButtonsSection: some View { + VStack(spacing: 12) { + // 收藏按钮 + Button(action: toggleFavorite) { + HStack { + Image(systemName: historyItem.isFavorite ? "heart.fill" : "heart") + .foregroundColor(historyItem.isFavorite ? .red : .gray) + + Text(historyItem.isFavorite ? "取消收藏" : "收藏") + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .padding() + .background(historyItem.isFavorite ? Color.red.opacity(0.1) : Color.gray.opacity(0.1)) + .foregroundColor(historyItem.isFavorite ? .red : .gray) + .cornerRadius(10) + } + + // 复制内容按钮 + Button(action: copyContent) { + HStack { + Image(systemName: "doc.on.doc") + .foregroundColor(.blue) + + Text("复制内容") + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.blue.opacity(0.1)) + .foregroundColor(.blue) + .cornerRadius(10) + } + + // 分享条形码图片按钮 + if barcodeImage != nil { + Button(action: { + showingShareSheet = true + }) { + HStack { + Image(systemName: "photo") + .foregroundColor(.green) + + Text("分享条形码图片") + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .padding() + .background(Color.green.opacity(0.1)) + .foregroundColor(.green) + .cornerRadius(10) + } + } + } + .padding() + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(radius: 2) + } + + // MARK: - 生成条形码图片 + private func generateBarcodeImage() { + guard let content = historyItem.content else { return } + + // 使用条形码生成器 + let barcodeType = historyItem.barcodeType ?? "" + let size = CGSize(width: 300, height: 120) + + if let image = BarcodeGenerator.shared.generateBarcode(from: content, type: barcodeType, size: size) { + self.barcodeImage = image + } + } + + // MARK: - 获取条形码图标 + private func getBarcodeIcon(for type: String) -> String { + return BarcodeGenerator.shared.getBarcodeIcon(for: type) + } + + // MARK: - 切换收藏状态 + private func toggleFavorite() { + historyItem.isFavorite.toggle() + coreDataManager.save() + + let message = historyItem.isFavorite ? "已添加到收藏" : "已取消收藏" + alertMessage = message + showingAlert = true + } + + // MARK: - 复制内容 + private func copyContent() { + if let content = historyItem.content { + UIPasteboard.general.string = content + alertMessage = "内容已复制到剪贴板" + showingAlert = true + } + } +} + +// MARK: - 分享表单 +struct BarcodeShareSheet: UIViewControllerRepresentable { + let activityItems: [Any] + + func makeUIViewController(context: Context) -> UIActivityViewController { + let controller = UIActivityViewController(activityItems: activityItems, applicationActivities: nil) + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} +} + +#Preview("EAN-13") { + let ctx = PreviewData.context + let item = PreviewData.ean13Sample(in: ctx) + return NavigationView { BarcodeDetailView(historyItem: item) } +} + +#Preview("Code 128") { + let ctx = PreviewData.context + let item = PreviewData.code128Sample(in: ctx) + return NavigationView { BarcodeDetailView(historyItem: item) } +} + +// MARK: - Preview Data +private enum PreviewData { + static let context: NSManagedObjectContext = { + let container = NSPersistentContainer(name: "MyQrCode") + let description = NSPersistentStoreDescription() + description.type = NSInMemoryStoreType + container.persistentStoreDescriptions = [description] + container.loadPersistentStores { _, _ in } + return container.viewContext + }() + + private static func makeBaseItem(in context: NSManagedObjectContext, content: String, barcodeType: BarcodeType, favorite: Bool = false) -> HistoryItem { + let item = HistoryItem(context: context) + item.id = UUID() + item.content = content + item.dataType = DataType.barcode.rawValue + item.dataSource = DataSource.created.rawValue + item.createdAt = Date() + item.isFavorite = favorite + item.barcodeType = barcodeType.rawValue + return item + } + + static func ean13Sample(in context: NSManagedObjectContext) -> HistoryItem { + let content = "1234567890128" + return makeBaseItem(in: context, content: content, barcodeType: .ean13, favorite: true) + } + + static func code128Sample(in context: NSManagedObjectContext) -> HistoryItem { + let content = "ABC123" + return makeBaseItem(in: context, content: content, barcodeType: .code128) + } +} \ No newline at end of file diff --git a/MyQrCode/Views/HistoryView.swift b/MyQrCode/Views/HistoryView.swift index a6292b9..88f3727 100644 --- a/MyQrCode/Views/HistoryView.swift +++ b/MyQrCode/Views/HistoryView.swift @@ -578,13 +578,18 @@ struct HistoryItemRow: View { } } .background( - // 为二维码类型添加导航链接 + // 根据数据类型添加导航链接 Group { if item.dataType == DataType.qrcode.rawValue { NavigationLink( destination: QRCodeDetailView(historyItem: item), label: { EmptyView() } ) + } else if item.dataType == DataType.barcode.rawValue { + NavigationLink( + destination: BarcodeDetailView(historyItem: item), + label: { EmptyView() } + ) } } )