import SwiftUI import QRCode import CoreData import Photos #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 @Environment(\.dismiss) private var dismiss @StateObject private var coreDataManager = CoreDataManager.shared // 颜色选择 @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 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 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 // 如果有选择的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) } 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() } .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 } } } } // 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 ) -> 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 saveQRCode() { guard let qrCodeImage = qrCodeImage else { return } // 保存到相册 UIImageWriteToSavedPhotosAlbum(qrCodeImage, nil, nil, nil) // 保存到历史记录 saveToHistory() dismiss() } // MARK: - 保存到历史记录 private func saveToHistory() { let context = 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 = "custom" historyItem.content = qrCodeContent do { try context.save() } catch { print("保存到历史记录失败:\(error.localizedDescription)") } } // 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中直接加载 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 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 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") }