Swift基于实际案例的异步任务调度队列
Swift基于实际案例的异步任务调度队列

Swift基于实际案例的异步任务调度队列

异步任务调度队列或逐步处理队列,其核心思想是将任务按顺序存储到一个队列中,逐一处理,并确保前一个任务完成后再启动下一个任务。

实际案例

在管理照片的应用中,通过PhotosPicker导入照片,并检测导入照片的行为后,立即要求用户命名照片。

PhotosPicker("新增照片", selection: $pickerItems, matching: .images)
    .onChange(of: pickerItems) { _, newItems in
        for item in newItems {
            Task {
                if let data = try? await item.loadTransferable(type: Data.self) {
                    newPhotoData = data // 暂存照片数据
                    showNamingAlert = true // 显示命名弹窗
                }
            }
        }
    }

当选取多张照片后,onChangehui 捕获到照片的数组,并将每一张照片进行遍历,每张照片都会被要求转换为Data类型,然后Data存储在newPhotoData变量中,并弹出命名弹窗。

if showNamingAlert {
    ZStack {
        Color.black.opacity(0.3)
            .frame(maxWidth: .infinity,maxHeight: .infinity)
            .edgesIgnoringSafeArea(.all)
        
        VStack(spacing: 20) {
            if let photoData = newPhotoData, let photo = UIImage(data: photoData) {
                Image(uiImage: photo)
                    .resizable()
                    .scaledToFill()
                    .frame(width: 250, height: 200)
                    .clipped()
                    .cornerRadius(10)
            }
            TextField("输入照片名称", text: $newPhotoName)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .padding()
                .frame(width: 200)
                .cornerRadius(10)
            Button(action: {
                // 保存逻辑
                if let photoData = newPhotoData {
                    let newPhoto = Photo(id: UUID(), photoName: newPhotoName, photo: photoData)
                    modelContext.insert(newPhoto)
                }
                resetState()
            }, label: {
                Text("保存")
                    .frame(width: 200,height: 40)
                    .foregroundColor(Color.white)
                    .background(Color.blue)
                    .cornerRadius(10)
            })
        }
        .frame(width: 300,height: 400)
        .background(Color.white)
        .cornerRadius(10)
    }
}

命名弹窗会将newPhotoData变量转换为Image照片显示出来,并提供一个文本输入框要求对照片进行命名。照片命名完成后,点击保存按钮,照片会生成Photoshi li 并插入到SwiftData中,然后调用resetState()方法,重置newPhotoName、photoData和showNamingAlert提示这三个变量,以便让后面的照片重新命名。

// 重置状态
func resetState() {
    newPhotoData = nil
    newPhotoName = ""
    showNamingAlert = false
}

案例流程图

但是实际上,每次导入多种照片时,只能命名最后一个照片的名称,保存后,也只能导入最后一张照片到SwiftData。

问题原因

因为在异步处理多个照片时,每次新的照片选中都会覆盖 newPhotoData 和 showNamingAlert 的状态,导致无法逐个弹出命名提示框。

具体原因为

在循环中,当多个照片被选中时,newPhotoData 和 showNamingAlert 的值被快速覆盖,只保留最后一个选中的照片信息。

每次修改 pickerItems 时,showNamingAlert 立即被设为 true,而没有等待当前命名完成。

解决方案

需要维护一个待命名照片的队列,让提示框逐个处理队列中的照片。

具体来讲,引入一个队列来存储需要命名的照片数据,弹出提示框时,从队列中取出第一张照片进行处理,用户完成命名后,继续处理队列中的下一张照片。

首先,定义一个照片队列的数组,内容是一个Photo数组。

@State private var namingQueue: [Photo] = [] // 待命名的照片队列

在PhotosPicker选择照片时,将每张照片新增到照片队列当中:

PhotosPicker("新增照片", selection: $pickerItems, matching: .images)
    .onChange(of: pickerItems) { _, newItems in
        for item in newItems {
            Task {
                if let data = try? await item.loadTransferable(type: Data.self) {
                    var photo = Photo(id: UUID(), photoName: "", photo: data)
                    namingQueue.append(photo)
                }
                resetState()
            }
        }
    }
}

从这里可以看到每张照片都会在转换为Data类型后,生成一个Photo实例 ,然后将该实例添加到照片队列中。之后调用resetState方法,激活照片命名页面:

func resetState() {
    showNamingAlert = !namingQueue.isEmpty
}

resetState方法会判断namingQueue照片队列是否为空,如果不为空,则showNamingAlert被赋值为true。

因为需要操作的是namingQueue照片队列的每一张照片,因此在照片命名页面中需要新增一个激活条件:

if showNamingAlert,let currentPhoto = namingQueue.first { }

那就是showNamingAlert为true时,获取照片队列中的首个Photo实例,然后给这个Photo实例命名。

其中,照片、文本输入框和按钮,都绑定这个Photo实例:

VStack(spacing: 20) {
    if let uiPhoto = currentPhoto.photo,let photo = UIImage(data: uiPhoto) {
        Image(uiImage: photo)
            .resizable()
            .scaledToFill()
            .frame(width: 250, height: 200)
            .clipped()
            .cornerRadius(10)
    }
    TextField("输入照片名称", text: Binding(
        get: { currentPhoto.photoName },
        set: { currentPhoto.photoName = $0 }))
    .textFieldStyle(RoundedBorderTextFieldStyle())
    .padding()
    .frame(width: 200)
    .cornerRadius(10)
    Button(action: {
        // 保存逻辑
        modelContext.insert(currentPhoto)
        namingQueue.removeFirst()
        resetState() // 检查是否需要显示命名弹窗
    }, label: {
        Text("保存")
            .frame(width: 200,height: 40)
            .foregroundColor(Color.white)
            .background(Color.blue)
            .cornerRadius(10)
    })
}
.frame(width: 300,height: 400)
.background(Color.white)
.cornerRadius(10)

当输入名称后,修改当前的实例名称。点击保存按钮时,将当前实例保存到Swift Data中,在namingQueue照片队列中,删除首个实例,然后重新判断是否需要命名弹窗。

实现效果:

以上就是异步任务调度队列的具体实现,通过数组和数组方法的管理,实现依次对每张图片的命名。

扩展知识

当检测命名时,不使用resetState()方法进行判断,仍然可以实现依次命名的效果。

只需要在PhotosPicker中将showNamingAlert改为true,即可实现检测的效果。

PhotosPicker("新增照片", selection: $pickerItems, matching: .images)
.onChange(of: pickerItems) { _, newItems in
    for item in newItems {
        Task {
            if let data = try? await item.loadTransferable(type: Data.self) {
                var photo = Photo(id: UUID(), photoName: "", photo: data)
                namingQueue.append(photo)
            }
            showNamingAlert = true
        }
    }
}

这是因为,命名照片弹窗的代码的if条件依赖于showNamingAlert 和 namingQueue.first:

if showNamingAlert, let currentPhoto = namingQueue.first {
    ZStack {
        // 弹窗视图
    }
}

每次插入新照片到 namingQueue 照片队列后,设置的 showNamingAlert = true会触发视图刷新。

当点击保存按钮时,通过 namingQueue.removeFirst() 移除了当前队列的第一个项目。

SwiftUI 的视图系统会重新检查条件 if showNamingAlert, let currentPhoto = namingQueue.first。

如果队列中还有元素,则显示下一张照片的命名弹窗。

如果队列为空,namingQueue.first 为 nil,条件自动不成立,弹窗消失。

因此,showNamingAlert = true 是显式触发弹窗的关键,而 namingQueue.removeFirst() 负责隐式关闭弹窗(当队列为空时)。

相关文章

SwiftUI 100 天:https://www.hackingwithswift.com/guide/ios-swiftui/6/3/challenge

完整代码

ContentView代码

import SwiftUI
import SwiftData
import PhotosUI

struct ContentView: View {
    @Environment(\.modelContext) var modelContext
    @Query var photos:[Photo]
    @State private var isDeleteAll = false
    @State private var pickerItems: [PhotosPickerItem] = []
    @State private var showNamingAlert = false
    @State private var namingQueue: [Photo] = [] // 待命名的照片队列
    let columns = [
        GridItem(.fixed(180)), // 自动根据屏幕宽度生成尽可能多的单元格,宽度最小为 80 点
        GridItem(.fixed(180))
    ]
    
    // 重置状态
    func resetState() {
        showNamingAlert = !namingQueue.isEmpty
    }
    
    var body: some View {
        NavigationStack {
            ZStack {
                // 照片视图列表
                VStack {
                    if !photos.isEmpty {
                        ScrollView {
                            LazyVGrid(columns: columns) {
                                ForEach(photos) { img in
                                    VStack {
                                        if let photoData = img.photo, let photo = UIImage(data: photoData) {
                                            Text(img.photoName)
                                            Image(uiImage: photo)
                                                .resizable()
                                                .scaledToFill()
                                                .frame(width: 170, height: 140)
                                                .clipped()
                                                .cornerRadius(8)
                                        }
                                    }
                                }
                            }
                        }
                    } else {
                        ContentUnavailableView("这里没有照片", systemImage: "photo.on.rectangle")
                    }
                    Spacer()
                    PhotosPicker("新增照片", selection: $pickerItems, matching: .images)
                        .onChange(of: pickerItems) { _, newItems in
                            for item in newItems {
                                Task {
                                    if let data = try? await item.loadTransferable(type: Data.self) {
                                        var photo = Photo(id: UUID(), photoName: "", photo: data)
                                        namingQueue.append(photo)
                                    }
                                    resetState()
                                }
                            }
                        }
                }
                // 照片名称填写页面
                if showNamingAlert,let currentPhoto = namingQueue.first {
                    ZStack {
                        Color.black.opacity(0.3)
                            .frame(maxWidth: .infinity,maxHeight: .infinity)
                            .edgesIgnoringSafeArea(.all)
                        
                        VStack(spacing: 20) {
                            if let uiPhoto = currentPhoto.photo,let photo = UIImage(data: uiPhoto) {
                                Image(uiImage: photo)
                                    .resizable()
                                    .scaledToFill()
                                    .frame(width: 250, height: 200)
                                    .clipped()
                                    .cornerRadius(10)
                            }
                            TextField("输入照片名称", text: Binding(
                                get: { currentPhoto.photoName },
                                set: { currentPhoto.photoName = $0 }))
                            .textFieldStyle(RoundedBorderTextFieldStyle())
                            .padding()
                            .frame(width: 200)
                            .cornerRadius(10)
                            Button(action: {
                                // 保存逻辑
                                modelContext.insert(currentPhoto)
                                namingQueue.removeFirst()
                                resetState() // 检查是否需要显示命名弹窗
                            }, label: {
                                Text("保存")
                                    .frame(width: 200,height: 40)
                                    .foregroundColor(Color.white)
                                    .background(Color.blue)
                                    .cornerRadius(10)
                            })
                        }
                        .frame(width: 300,height: 400)
                        .background(Color.white)
                        .cornerRadius(10)
                    }
                }
            }
            .navigationTitle("图片")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing, content: {
                    Button("删除全部") {
                        isDeleteAll = true
                    }
                })
            }
            .alert("删除全部图片",isPresented: $isDeleteAll){
                Button("确定", role: .destructive) {
                    try? modelContext.delete(model:Photo.self)
                }
                Button("取消",role: .cancel) {
                    isDeleteAll = false
                }
            }
            
        }
    }
    
}

#Preview {
    ContentView()
        .modelContainer(for: Photo.self)
}

Photos结构代码

import SwiftUI
import SwiftData
import Foundation
import PhotosUI

@Model
class Photo {
    var id: UUID
    var photoName = ""
    var photo: Data?
    
    init(id: UUID, photoName: String = "", photo: Data? = nil) {
        self.id = id
        self.photoName = photoName
        self.photo = photo
    }
}

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注