@ -1,6 +1,10 @@
import SwiftUI
import SwiftUI
import QRCode
import QRCode
import CoreData
import CoreData
import Photos
#if canImport ( PhotosUI )
import PhotosUI
#endif
// MARK: - 标 签 类 型 枚 举
// MARK: - 标 签 类 型 枚 举
enum TabType : String , CaseIterable {
enum TabType : String , CaseIterable {
@ -46,6 +50,12 @@ struct QRCodeStyleView: View {
// L o g o 选 择
// L o g o 选 择
@ State private var selectedLogo : QRCodeLogo ? = nil
@ 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 qrCodeImage : UIImage ?
@ -81,10 +91,19 @@ struct QRCodeStyleView: View {
d . design . shape . eye = selectedEyeType . eyeShape
d . design . shape . eye = selectedEyeType . eyeShape
// 如 果 有 选 择 的 L o g o , 设 置 L o g o
// 如 果 有 选 择 的 L o g o , 设 置 L o g o
if let selectedLogo = selectedLogo ,
if let customLogoImage = customLogoImage ,
let logoImage = selectedLogo . image {
let cgImage = customLogoImage . cgImage {
// 设 置 L o g o 作 为 背 景 图 片
// 使 用 自 定 义 L o g o
d . logoTemplate = QRCode . LogoTemplate . CircleCenter ( image : logoImage . cgImage ! )
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 {
// 使 用 预 设 L o g o
print ( " 应用预设Logo: \( selectedLogo . displayName ) " )
d . logoTemplate = QRCode . LogoTemplate . CircleCenter ( image : cgImage )
} else {
print ( " 没有设置任何Logo " )
}
}
return d
return d
@ -109,6 +128,22 @@ struct QRCodeStyleView: View {
}
}
}
}
. onAppear {
. 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 // 清 除 预 设 L o g o 选 择
self . imageToCrop = nil
}
}
}
}
}
}
@ -330,6 +365,7 @@ struct QRCodeStyleView: View {
// 无 L o g o 选 项
// 无 L o g o 选 项
Button ( action : {
Button ( action : {
selectedLogo = nil
selectedLogo = nil
customLogoImage = nil
} ) {
} ) {
VStack ( spacing : 8 ) {
VStack ( spacing : 8 ) {
RoundedRectangle ( cornerRadius : 12 )
RoundedRectangle ( cornerRadius : 12 )
@ -349,18 +385,93 @@ struct QRCodeStyleView: View {
. padding ( 12 )
. padding ( 12 )
. background (
. background (
RoundedRectangle ( cornerRadius : 16 )
RoundedRectangle ( cornerRadius : 16 )
. fill ( selectedLogo = = nil ? Color . blue . opacity ( 0.1 ) : Color . clear )
. fill ( selectedLogo = = nil && customLogoImage = = nil ? Color . blue . opacity ( 0.1 ) : Color . clear )
. overlay (
. overlay (
RoundedRectangle ( cornerRadius : 16 )
RoundedRectangle ( cornerRadius : 16 )
. stroke ( selectedLogo = = nil ? Color . blue : Color . clear , lineWidth : 3 )
. stroke ( selectedLogo = = nil && customLogoImage = = nil ? Color . blue : Color . clear , lineWidth : 3 )
)
)
)
)
}
}
// 自 定 义 L o g o 选 项
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 )
)
)
}
}
// L o g o 选 项
// L o g o 选 项
ForEach ( QRCodeLogo . allCases , id : \ . self ) { logo in
ForEach ( QRCodeLogo . allCases , id : \ . self ) { logo in
Button ( action : {
Button ( action : {
selectedLogo = logo
selectedLogo = logo
customLogoImage = nil // 清 除 自 定 义 L o g o
} ) {
} ) {
VStack ( spacing : 8 ) {
VStack ( spacing : 8 ) {
if let image = loadImage ( named : logo . thumbnailName ) {
if let image = loadImage ( named : logo . thumbnailName ) {
@ -466,6 +577,31 @@ struct QRCodeStyleView: View {
}
}
}
}
// 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: - 辅 助 函 数
// MARK: - 辅 助 函 数
private func loadImage ( named name : String ) -> UIImage ? {
private func loadImage ( named name : String ) -> UIImage ? {
// 方 法 1 : 尝 试 从 B u n d l e 中 直 接 加 载
// 方 法 1 : 尝 试 从 B u n d l e 中 直 接 加 载
@ -504,6 +640,197 @@ struct QRCodeStyleView: View {
}
}
}
}
// 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: - 预 览
// MARK: - 预 览
# Preview {
# Preview {
QRCodeStyleView ( qrCodeContent : " https://www.example.com " )
QRCodeStyleView ( qrCodeContent : " https://www.example.com " )