|
|
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<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 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")
|
|
|
}
|