手机相册里存了很多随身码,核算码啥的图片(身在上海,经历过2020~2022年的都懂)。一直没有耐心好好去清理。但总惦记着这个事情,想写个工具app来清理相册里这种图片。

身为一名非典型性程序猿,之前只会用一些非主流的开发工具,想学iOS开发,却是举步维艰:一看object-c的代码就一个头两个大(那时还不知道swfit-UI)。曾经想过用XMarain for iOS做,也写了一个雏形,但是受限于时间和没有Mac电脑等等(都是借口而已),一拖又是几年过去了。
2025年,AI突然发力,甚至可以跟程序猿抢桃子了,我就动了用豆包帮我写这个iOS程序的念头。豆包真的很牛逼,我提了设想,它二话不说就把代码给我写完了,是个SwiftUI程序。代码很简洁,虽然也看不懂,但是好像比较偏向于高级语言。顺便多说两句灌个水,给非程序猿看官科普一下,高级语言这个“高级”不日常用语里高级感满满那个褒义词,是指编程语言更贴近自然语言,是相对于更贴近机器指令的低级语言(如C语言、汇编语言)而言。

手上没有可用的Mac设备,就折腾虚拟机吧。之前主要玩VirtualBox,因为免费。折腾了很久,装MacOS遇到各种问题。网上一看很多人推荐用VM-ware,又突然发现VM-ware workstation已经对个人免费了,于是果断弃暗投明。在VM-ware上装了一个MacOS虚拟机,再加上XCode,居然很顺利地在仿真iOS设备上把这个程序给调试通过了。步骤也很简单:在Xcode里新建一个iOS App工程,接口类型选了默认的SwiftUI,然后语言就只能选Swfit,把豆包给我写好的Swfit源码贴到主源码文件里就行了。

上真机测试的历程有点坎坷,一开始是因为我的Xcode版本太低(好像是XCode 14吧),我的手机偏偏又已经升级到了iOS 18.7,适配不了。一顿折腾,好容易升级到了MacOS 15.7.4 (Sequoia)和Xcode 16.4,终于能上真机调试了。程序跑一会儿就闪退。仿真设备相册里我只放进去40多张照片测试。真机里照片太多,估计是扫描过程中内存没及时释放,爆了,就闪退了。想调优代码,又看不太懂。反复测试,发现如果每次扫描的照片不超过70张,不会闪退。好在这个工具我主要是自己用,我自己能凑合,就改成分批次扫描,每次扫描64张。相册照片按时间从早到晚顺序排队接受检阅。每一批扫描之后,把本批次最后一张照片的日期时间记下来,下一轮扫描从这个日期时间开始的照片。扫描发现了二维码占主体的图片呢,就放到画廊里陈列出来,橱窗里显示缩略图,用户可以点开看大图,可以取消勾选。完成选择之后,工具就提示是否删除照片。说到这里,不得不表达对SwiftUI的敬佩,这些复杂的图形化UI交互,人家一个源码文件就全部搞定,代码还只有600多行。

除了这个SwfitUI的主源码文件(完全是豆包捉刀代笔),我还按照豆包的指导在XCode的工程理加了对相册权限的请求声明。(刚开始换MacOS和Xcode高版本把这一茬给忘了,程序一跑就报错,把我困惑了好几天,还以为是高版本系统不兼容代码,几乎放弃)。首先,在XCode的Project树形导航栏选择最上层的工程节点,然后在中间导航栏选择下面TARGETS区域第一个与工程同名的节点,最后在右侧的Custom iOS Target Properties列表中点+号按钮新增2项(Value对应界面提示文字,可以根据自己偏好调整):

Key

Type

Value

Privacy - Photo Library Usage Description

String

需要访问相册以扫描二维码照片

Privacy - Photo Library Additions Usage Description

String

需要访问相册以删除照片

最后,作为一个App,在桌面上得有一个Icon,随意用AI生成了一张图标,1024×1024的PNG图片。左侧工程树形导航栏里选Assets,中部导航栏选AppIcon,右侧左上角有个Any Apperance,把图片从Finder里拖进来就OK了。

以下附上主文件Swift源码,其他工程文件就不必要了。我自己没有买Apple的开发者订阅,毕竟一年要99美元,我又不是职业iOS开发程序猿,如果哪位土豪看官或者热心人想让我把这个App上架,可以赞助或者帮我众筹这笔经费。亲们如果有任何问题或者要求想跟我交流,欢迎跟我联系,eMail:zongchao@sina.com,微信号:zong_chao。

import SwiftUI
import Photos
import CoreImage
import CoreImage.CIFilterBuiltins

// MARK: - 配置Key
private let kLastScannedDateKey = "kLastScannedDateKey"
private let kQRThresholdKey = "kQRThresholdKey"
private let kTotalScanCountKey = "kTotalScanCount"

// MARK: - 全局设置
class AppSettings: ObservableObject {
    @Published var qrThreshold: Double {
        didSet {
            UserDefaults.standard.set(qrThreshold, forKey: kQRThresholdKey)
        }
    }
    
    init() {
        let saved = UserDefaults.standard.double(forKey: kQRThresholdKey)
        self.qrThreshold = saved == 0 ? 0.3 : saved
    }
}

// MARK: - 二维码检测模型
private let ciContext = CIContext(options: [.useSoftwareRenderer: false])

struct QRResult {
    let found: Bool
    let boundingBox: CGRect
    let imageSize: CGSize
    
    var widthRatio: Double {
        imageSize.width > 0 ? Double(boundingBox.width / imageSize.width) : 0.0
    }
    var heightRatio: Double {
        imageSize.height > 0 ? Double(boundingBox.height / imageSize.height) : 0.0
    }
    
    func isMainQRCode(threshold: Double) -> Bool {
        widthRatio >= threshold || heightRatio >= threshold
    }
}

extension UIImage {
    func detectQRCode(context: CIContext) -> QRResult {
        guard let detector = CIDetector(
            ofType: CIDetectorTypeQRCode,
            context: context,
            options: [CIDetectorAccuracy: CIDetectorAccuracyHigh]
        ) else {
            return QRResult(found: false, boundingBox: .zero, imageSize: size)
        }
        
        guard let ci = autoreleasepool(invoking: { CIImage(image: self) }),
              let features = detector.features(in: ci) as? [CIQRCodeFeature] else {
            return QRResult(found: false, boundingBox: .zero, imageSize: size)
        }
        
        guard let f = features.first else {
            return QRResult(found: false, boundingBox: .zero, imageSize: size)
        }
        
        let t = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -size.height)
        return QRResult(found: true, boundingBox: f.bounds.applying(t), imageSize: size)
    }
}

// MARK: - 扫描管理器
class PhotoScanner: ObservableObject {
    @Published var isScanning = false
    @Published var totalPhotos = 0
    @Published var currentIndex = 0
    @Published var foundCount = 0
    @Published var foundAssets: [PHAsset] = []
    @Published var showPermissionAlert = false
    @Published var lastScannedDate: Date?
    @Published var showNoMorePhotosAlert = false
    
    // 👇 只加了这一个:累计扫描总数
    @Published var totalScanCount = 0
    
    private var stopFlag = false
    private let ciContext = CIContext(options: [.useSoftwareRenderer: false])
    private let defaults = UserDefaults.standard
    private let batchSize = 64
    
    init() {
        lastScannedDate = defaults.object(forKey: kLastScannedDateKey) as? Date
        totalScanCount = defaults.integer(forKey: kTotalScanCountKey)
    }
    
    func stopScan() {
        stopFlag = true
    }
    
    private func saveBreakpoint(date: Date) {
        lastScannedDate = date
        defaults.set(date, forKey: kLastScannedDateKey)
    }
    
    func clearBreakpoint() {
        lastScannedDate = nil
        defaults.removeObject(forKey: kLastScannedDateKey)
        foundAssets.removeAll()
        foundCount = 0
        currentIndex = 0
        
        // 👇 清除记忆点时重置累计
        totalScanCount = 0
        defaults.set(0, forKey: kTotalScanCountKey)
    }
    
    func requestPermission(completion: @escaping (Bool) -> Void) {
        PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in
            DispatchQueue.main.async {
                completion(status == .authorized || status == .limited)
            }
        }
    }
    
    func startScan(threshold: Double) {
        guard !isScanning else { return }
        isScanning = true
        stopFlag = false
        currentIndex = 0
        totalPhotos = 0
        
        let opt = PHFetchOptions()
        opt.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
        opt.fetchLimit = batchSize
        
        if let lastDate = lastScannedDate {
            opt.predicate = NSPredicate(format: "creationDate > %@", lastDate as CVarArg)
        }
        
        let assets = PHAsset.fetchAssets(with: .image, options: opt)
        totalPhotos = assets.count
        
        // 👇 累加累计数
        totalScanCount += assets.count
        defaults.set(totalScanCount, forKey: kTotalScanCountKey)
        
        if assets.count == 0 {
            DispatchQueue.main.async {
                self.isScanning = false
                self.currentIndex = 1
                self.showNoMorePhotosAlert = true
            }
            return
        }
        
        func next(at i: Int) {
            if stopFlag || i >= assets.count {
                if i > 0, let date = assets.object(at: i-1).creationDate {
                    saveBreakpoint(date: date)
                }
                DispatchQueue.main.async {
                    self.isScanning = false
                    if self.currentIndex == 0 {
                        self.currentIndex = 1
                    }
                }
                return
            }
            
            DispatchQueue.main.async {
                self.currentIndex = i + 1
            }
            
            let asset = assets.object(at: i)
            loadImage(asset: asset) { img in
                guard let img = img else {
                    next(at: i + 1)
                    return
                }
                let res = img.detectQRCode(context: self.ciContext)
                if res.found && res.isMainQRCode(threshold: threshold) {
                    DispatchQueue.main.async {
                        self.foundCount += 1
                        self.foundAssets.append(asset)
                    }
                }
                next(at: i + 1)
            }
        }
        
        DispatchQueue.global(qos: .userInitiated).async {
            next(at: 0)
        }
    }
    
    private func loadImage(asset: PHAsset, completion: @escaping (UIImage?) -> Void) {
        let opt = PHImageRequestOptions()
        opt.isSynchronous = true
        opt.deliveryMode = .highQualityFormat
        opt.resizeMode = .fast
        let targetSize = CGSize(width: 1200, height: 1200)
        
        PHImageManager.default().requestImage(
            for: asset,
            targetSize: targetSize,
            contentMode: .aspectFit,
            options: opt
        ) { img, _ in
            autoreleasepool(invoking: {
                completion(img)
            })
        }
    }
}

// MARK: - 参数调节弹窗
struct QRThresholdSettingView: View {
    @ObservedObject var settings: AppSettings
    @Environment(\.dismiss) var dismiss
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 30) {
                Text("二维码占比阈值设置")
                    .font(.title2.bold())
                
                Text("当前值:\(Int(settings.qrThreshold * 100))%")
                    .font(.headline)
                
                Slider(value: $settings.qrThreshold, in: 0.05...0.95, step: 0.05)
                    .padding(.horizontal, 30)
                
                Text("调节范围 5% ~ 95%,步长 5%")
                    .font(.caption)
                    .foregroundColor(.gray)
                
                Spacer()
            }
            .padding(.top, 20)
            .toolbar {
                ToolbarItem(placement: .confirmationAction) {
                    Button("完成") {
                        dismiss()
                    }
                }
            }
        }
    }
}

// MARK: - 详情页
struct ResultDetailView: View {
    let assets: [PHAsset]
    @Binding var showDetail: Bool
    var onDeleteCompleted: () -> Void
    
    @State private var currentPage = 0
    @State private var selectedItems: Set<Int> = []
    @State private var showAlert = false
    @State private var alertMessage = ""
    @State private var isDeleteAlert = false
    @State private var selectedImage: UIImage?
    @State private var showFullScreen = false
    
    private let itemsPerPage = 9
    private let columns = Array(repeating: GridItem(.flexible()), count: 3)
    
    private var totalPages: Int {
        max(1, Int(ceil(Double(assets.count) / Double(itemsPerPage))))
    }
    
    private var pageItems: [PHAsset] {
        let start = currentPage * itemsPerPage
        let end = min(start + itemsPerPage, assets.count)
        return Array(assets[start..<end])
    }
    
    private var pageIndices: [Int] {
        let start = currentPage * itemsPerPage
        return (0..<pageItems.count).map { start + $0 }
    }
    
    init(assets: [PHAsset], showDetail: Binding<Bool>, onDeleteCompleted: @escaping () -> Void) {
        self.assets = assets
        self._showDetail = showDetail
        self.onDeleteCompleted = onDeleteCompleted
        self._selectedItems = State(initialValue: Set(0..<assets.count))
    }
    
    var body: some View {
        NavigationStack {
            VStack(spacing: 0) {
                Text("请选择要删除的照片:")
                    .font(.subheadline)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .padding(.horizontal, 12)
                
                Text("第 \(currentPage+1)/\(totalPages) 页")
                    .font(.caption)
                
                ScrollView(.vertical, showsIndicators: false) {
                    LazyVGrid(columns: columns, spacing: 8) {
                        ForEach(Array(zip(pageIndices, pageItems)), id: \.0) { idx, asset in
                            PhotoCheckItemView(
                                asset: asset,
                                isChecked: selectedItems.contains(idx),
                                onToggle: {
                                    if selectedItems.contains(idx) {
                                        selectedItems.remove(idx)
                                    } else {
                                        selectedItems.insert(idx)
                                    }
                                },
                                onTapImage: {
                                    loadFullImage(asset: asset)
                                }
                            )
                        }
                    }
                    .padding(.horizontal, 12)
                }
                
                HStack(spacing: 16) {
                    Button("上一页") {
                        if currentPage > 0 {
                            currentPage -= 1
                        }
                    }
                    .disabled(currentPage <= 0)
                    
                    Button("下一页") {
                        if currentPage < totalPages - 1 {
                            currentPage += 1
                        }
                    }
                    .disabled(currentPage >= totalPages - 1)
                    
                    Button("完成选择") {
                        checkSelectionAndShowAlert()
                    }
                    .foregroundColor(.blue)
                }
                .padding(.vertical, 4)
            }
            .navigationTitle("二维码主体照片")
            .navigationBarTitleDisplayMode(.inline)
            .padding(.top, 0)
            .alert("提示", isPresented: $showAlert) {
                if isDeleteAlert {
                    Button("是", role: .destructive) {
                        deleteSelected()
                    }
                    Button("否", role: .cancel) {
                        showDetail = false
                    }
                } else {
                    Button("确定") {
                        onDeleteCompleted()
                        showDetail = false
                    }
                }
            } message: {
                Text(alertMessage)
            }
            .overlay {
                if showFullScreen, let selectedImage {
                    FullScreenImageView(image: selectedImage) {
                        self.selectedImage = nil
                        self.showFullScreen = false
                    }
                }
            }
        }
    }
    
    private func loadFullImage(asset: PHAsset) {
        let options = PHImageRequestOptions()
        options.deliveryMode = .highQualityFormat
        options.isNetworkAccessAllowed = true
        
        PHImageManager.default().requestImage(
            for: asset,
            targetSize: PHImageManagerMaximumSize,
            contentMode: .aspectFit,
            options: options
        ) { image, _ in
            guard let image = image else { return }
            DispatchQueue.main.async {
                self.selectedImage = image
                self.showFullScreen = true
            }
        }
    }
    
    private func checkSelectionAndShowAlert() {
        if selectedItems.isEmpty {
            alertMessage = "共选中0张照片"
            isDeleteAlert = false
        } else {
            alertMessage = "共选中 \(selectedItems.count) 张照片,是否删除所选照片?"
            isDeleteAlert = true
        }
        showAlert = true
    }
    
    private func deleteSelected() {
        let toDelete = selectedItems.compactMap { assets.indices.contains($0) ? assets[$0] : nil }
        
        PHPhotoLibrary.shared().performChanges({
            PHAssetChangeRequest.deleteAssets(toDelete as NSArray)
        }, completionHandler: { success, error in
            DispatchQueue.main.async {
                if success {
                    onDeleteCompleted()
                    showDetail = false
                } else {
                    alertMessage = "删除失败:\(error?.localizedDescription ?? "未知错误")"
                    isDeleteAlert = false
                    showAlert = true
                }
            }
        })
    }
}

struct FullScreenImageView: View {
    let image: UIImage
    let onTap: () -> Void
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea()
            Image(uiImage: image)
                .resizable()
                .scaledToFit()
                .onTapGesture {
                    onTap()
                }
        }
    }
}

struct PhotoCheckItemView: View {
    let asset: PHAsset
    let isChecked: Bool
    let onToggle: () -> Void
    let onTapImage: () -> Void
    var body: some View {
        VStack(spacing: 4) {
            PhotoThumbnailView(asset: asset)
                .frame(width: 100, height: 100)
                .clipped()
                .onTapGesture {
                    onTapImage()
                }
            
            Toggle(isOn: Binding(get: { isChecked }, set: { _ in onToggle() })) {
                EmptyView()
            }
            .labelsHidden()
            .frame(height: 30)
        }
        .frame(width: 100)
    }
}

struct PhotoThumbnailView: View {
    let asset: PHAsset
    @State private var image: UIImage?
    var body: some View {
        ZStack {
            if let image = image {
                Image(uiImage: image)
                    .resizable()
                    .scaledToFit()
                    .frame(width: 100, height: 100)
            } else {
                Color.gray.opacity(0.4)
            }
        }
        .frame(width: 100, height: 100)
        .clipped()
        .onAppear {
            let opt = PHImageRequestOptions()
            opt.isNetworkAccessAllowed = true
            opt.deliveryMode = .highQualityFormat
            
            PHImageManager.default().requestImage(
                for: asset,
                targetSize: CGSize(width: 240, height: 240),
                contentMode: .aspectFit,
                options: opt
            ) { img, _ in
                DispatchQueue.main.async {
                    image = img
                }
            }
        }
    }
}

// MARK: - 主界面(已修复:所有文字强制黑色)
struct ContentView: View {
    @StateObject private var scanner = PhotoScanner()
    @StateObject private var settings = AppSettings()
    @State private var showDetail = false
    @State private var autoContinueWhenNotFound = false
    @State private var showClearSuccessAlert = false
    @State private var showSettingPage = false
    
    private var progress: Double {
        guard scanner.totalPhotos > 0 else { return 0 }
        return Double(scanner.currentIndex) / Double(scanner.totalPhotos)
    }
    
    private var lastScanDateText: String {
        guard let date = scanner.lastScannedDate else {
            return "已扫描照片最晚日期时间:无(将从最早照片开始)"
        }
        let fmt = DateFormatter()
        fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
        return "已扫描照片最晚日期时间:\n\(fmt.string(from: date))"
    }
    
    var body: some View {
        VStack(spacing: 8) {
            Spacer().frame(height: 60)
            Text("本应用可以从相册中查找二维码占主体的照片")
                .font(.title2)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 20)
            Text("(为防止占用过多系统资源导致应用闪退,每次仅扫描64张照片,随后可继续扫描)")
                .font(.subheadline)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
                .padding(.horizontal, 20)
            
            Button {
                scanner.requestPermission { granted in
                    if granted {
                        scanner.currentIndex = 1
                        scanner.startScan(threshold: settings.qrThreshold)
                    } else {
                        scanner.showPermissionAlert = true
                    }
                }
            } label: {
                Text("扫描二维码照片")
                    .frame(maxWidth: .infinity)
                    .padding()
            }
            .buttonStyle(.borderedProminent)
            .font(.title2.bold())
            .cornerRadius(12)
            .padding(.horizontal, 30)
            
            Spacer().frame(height: 24)
            Text(lastScanDateText)
                .font(.caption)
                .foregroundColor(.secondary)
                .multilineTextAlignment(.center)
            
            Button {
                scanner.clearBreakpoint()
                showClearSuccessAlert = true
            } label: {
                Text("清除记忆点(从头扫描)")
                    .frame(maxWidth: .infinity)
                    .padding()
            }
            .buttonStyle(.borderedProminent)
            .tint(.gray)
            .cornerRadius(12)
            .padding(.horizontal, 30)
            
            Spacer()
            
            HStack {
                Text("二维码占比阈值:\(Int(settings.qrThreshold * 100))%")
                    .font(.subheadline)
                Spacer()
                Button("调整参数") {
                    showSettingPage = true
                }
                .buttonStyle(.borderedProminent)
                .controlSize(.small)
            }
            .padding(.horizontal, 30)
            .padding(.bottom, 16)
        }
        .overlay {
            if scanner.isScanning || scanner.currentIndex > 0 {
                Color.white.ignoresSafeArea()
                ScrollView {
                    VStack(spacing: 16) {
                        if scanner.isScanning {
                            Text("正在扫描相册...")
                                .font(.title.bold())
                                .minimumScaleFactor(0.5)
                                .foregroundColor(.black) // 👈 修复
                            
                            ProgressView(value: progress)
                                .progressViewStyle(.linear)
                                .frame(height: 12)
                                .padding(.horizontal, 40)
                            
                            Text("\(Int(progress * 100))%")
                                .font(.headline)
                                .foregroundColor(.blue)
                                .minimumScaleFactor(0.5)
                            
                            Text("累计扫描:\(scanner.totalScanCount) 张")
                                .minimumScaleFactor(0.5)
                                .foregroundColor(.black) // 👈 修复
                            
                            Text("当前:第 \(scanner.currentIndex) 张")
                                .minimumScaleFactor(0.5)
                                .foregroundColor(.black) // 👈 修复
                            
                            Text("有 \(scanner.foundCount) 张二维码照片待处理")
                                .foregroundColor(.green)
                                .minimumScaleFactor(0.5)
                            
                            Button {
                                scanner.stopScan()
                                autoContinueWhenNotFound = false
                            } label: {
                                Text("🛑 停止扫描")
                                    .frame(maxWidth: 120)
                                    .frame(height: 40)
                            }
                            .buttonStyle(.borderedProminent)
                            .tint(.red)
                        } else {
                            Text("本批次扫描完成")
                                .font(.title.bold())
                                .minimumScaleFactor(0.5)
                                .lineLimit(1)
                                .foregroundColor(.black) // 👈 修复
                            
                            Text("本批次扫描:\(scanner.totalPhotos) 张")
                                .font(.title3)
                                .minimumScaleFactor(0.5)
                                .lineLimit(1)
                                .foregroundColor(.black) // 👈 修复
                            
                            Text("累计扫描:\(scanner.totalScanCount) 张")
                                .font(.title3)
                                .minimumScaleFactor(0.5)
                                .foregroundColor(.black) // 👈 修复
                            
                            Text("有 \(scanner.foundCount) 张二维码照片待处理")
                                .font(.title)
                                .foregroundColor(.green)
                                .minimumScaleFactor(0.5)
                                .lineLimit(1)
                            
                            Text(lastScanDateText)
                                .font(.caption)
                                .foregroundColor(.gray)
                                .multilineTextAlignment(.center)
                                .minimumScaleFactor(0.5)
                            
                            VStack(spacing: 14) {
                                Button {
                                    scanner.currentIndex = 0
                                } label: {
                                    Text("返回")
                                        .frame(maxWidth: .infinity)
                                        .frame(height: 40)
                                }
                                .buttonStyle(.borderedProminent)
                                .tint(.gray)
                                
                                if scanner.foundCount > 0 {
                                    Button {
                                        showDetail = true
                                    } label: {
                                        Text("查看详情")
                                            .frame(maxWidth: .infinity)
                                            .frame(height: 40)
                                    }
                                    .buttonStyle(.borderedProminent)
                                }
                                
                                Button {
                                    scanner.currentIndex = 1
                                    scanner.startScan(threshold: settings.qrThreshold)
                                } label: {
                                    Text("继续扫描")
                                        .frame(maxWidth: .infinity)
                                        .frame(height: 40)
                                }
                                .buttonStyle(.borderedProminent)
                            }
                            .frame(width: 200)
                            
                            Toggle(isOn: $autoContinueWhenNotFound) {
                                Text("累计不足9张自动续扫")
                                    .foregroundColor(.black)
                                    .minimumScaleFactor(0.5)
                            }
                            .padding(.horizontal, 30)
                        }
                    }
                    .padding(.top, 20)
                    .onAppear {
                        if !scanner.isScanning, scanner.totalPhotos == 0 {
                            DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
                                scanner.showNoMorePhotosAlert = true
                                autoContinueWhenNotFound = false
                            }
                        }
                    }
                }
            }
        }
        .sheet(isPresented: $showSettingPage) {
            QRThresholdSettingView(settings: settings)
        }
        .sheet(isPresented: $showDetail) {
            ResultDetailView(
                assets: scanner.foundAssets,
                showDetail: $showDetail
            ) {
                scanner.foundCount = 0
                scanner.foundAssets.removeAll()
            }
        }
        .onChange(of: scanner.isScanning) { oldValue, newValue in
            if !newValue, autoContinueWhenNotFound, scanner.foundCount < 9 {
                DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                    guard !scanner.isScanning else { return }
                    scanner.currentIndex = 1
                    scanner.startScan(threshold: settings.qrThreshold)
                }
            }
        }
        .alert("提示", isPresented: $scanner.showNoMorePhotosAlert) {
            Button("确定", role: .cancel) { }
        } message: {
            Text("没有更多照片可扫描了")
        }
        .onChange(of: scanner.showNoMorePhotosAlert) { oldValue, newValue in
            if newValue {
                autoContinueWhenNotFound = false
            }
        }
        .alert("需要相册权限", isPresented: $scanner.showPermissionAlert) {
            Button("去设置") {
                UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
            }
            Button("取消", role: .cancel) {}
        } message: {
            Text("请允许访问相册")
        }
        .alert("清除成功", isPresented: $showClearSuccessAlert) {
            Button("确定", role: .cancel) {}
        } message: {
            Text("已清除记忆点,下次从头扫描")
        }
    }
}

// MARK: - 预览(已修复)
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐