当前位置: 首页 > article >正文

从零开始学iOS开发(第三十二篇):SwiftUI 拖拽交互 —— 构建流畅的拖放体验

欢迎来到本系列教程的第三十二篇。在前三十一篇中你已经学习了从Swift基础到数据可视化的全方位iOS开发技能。现在你能够构建出功能完善、数据清晰的应用了。但是如何让用户与应用进行更自然的交互如何让用户通过拖拽来重新排序、移动内容或导入外部数据拖拽交互是现代移动应用的重要组成部分。无论是调整列表顺序、将图片拖入相册还是将文件拖入应用进行导入拖放都提供了直观、高效的操作方式。SwiftUI提供了强大的拖拽API让这一切变得简单。在这一篇中你将学到拖拽基础onDrag与onDrop修饰符NSItemProvider与数据类型拖拽预览自定义列表重排序拖拽重新排序跨列表拖拽自定义拖拽手柄图片拖拽图片拖出与拖入多图片拖拽拖拽缩略图自定义数据拖拽自定义数据类型拖拽动画拖拽反馈实战项目构建一个可拖拽的看板应用一、拖拽基础1.1 onDrag与onDropswiftimport SwiftUI import UniformTypeIdentifiers // MARK: - 基础拖拽示例 struct BasicDragDropView: View { State private var items [任务1, 任务2, 任务3, 任务4] State private var isTargeted false var body: some View { VStack(spacing: 20) { Text(拖拽源) .font(.headline) // 可拖拽的源视图 ForEach(items, id: \.self) { item in Text(item) .padding() .frame(maxWidth: .infinity) .background(Color.blue.opacity(0.2)) .cornerRadius(8) .onDrag { // 提供数据 NSItemProvider(object: item as NSString) } } Divider() Text(拖拽目标) .font(.headline) // 拖拽目标视图 RoundedRectangle(cornerRadius: 12) .fill(isTargeted ? Color.green.opacity(0.3) : Color.gray.opacity(0.2)) .frame(height: 200) .overlay( Text(拖拽到这里) .foregroundColor(isTargeted ? .green : .gray) ) .onDrop(of: [.text], isTargeted: $isTargeted) { providers in guard let provider providers.first else { return false } provider.loadObject(ofClass: NSString.self) { object, error in if let text object as? String { DispatchQueue.main.async { print(收到: \(text)) } } } return true } } .padding() } }1.2 数据类型与UTTypeswiftimport UniformTypeIdentifiers // MARK: - 支持多种数据类型 struct MultiTypeDragView: View { State private var droppedText: String State private var droppedImage: UIImage? State private var droppedURL: URL? var body: some View { VStack(spacing: 20) { // 可拖拽的文本 Text(拖拽我(文本)) .padding() .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) .onDrag { NSItemProvider(object: Hello from SwiftUI as NSString) } // 可拖拽的图片 Image(systemName: photo) .font(.system(size: 50)) .padding() .background(Color.green) .foregroundColor(.white) .cornerRadius(8) .onDrag { // 模拟图片数据 let image UIImage(systemName: photo)! guard let data image.pngData() else { return NSItemProvider() } let provider NSItemProvider(item: data as NSData, typeIdentifier: UTType.png.identifier) return provider } Divider() // 拖拽目标 VStack(spacing: 16) { if let image droppedImage { Image(uiImage: image) .resizable() .scaledToFit() .frame(height: 100) } if !droppedText.isEmpty { Text(文本: \(droppedText)) .foregroundColor(.blue) } if let url droppedURL { Text(URL: \(url.lastPathComponent)) .foregroundColor(.green) } } .frame(maxWidth: .infinity, minHeight: 150) .background(Color.gray.opacity(0.1)) .cornerRadius(12) .onDrop(of: [.text, .png, .url], isTargeted: nil) { providers in for provider in providers { // 检查文本 if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) { provider.loadObject(ofClass: NSString.self) { text, _ in DispatchQueue.main.async { droppedText text as? String ?? } } return true } // 检查图片 if provider.hasItemConformingToTypeIdentifier(UTType.png.identifier) { provider.loadObject(ofClass: UIImage.self) { image, _ in DispatchQueue.main.async { droppedImage image as? UIImage } } return true } // 检查URL if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) { provider.loadObject(ofClass: URL.self) { url, _ in DispatchQueue.main.async { droppedURL url } } return true } } return false } } .padding() } }1.3 自定义拖拽预览swift// MARK: - 自定义拖拽预览 struct CustomDragPreviewView: View { State private var items [苹果, 香蕉, 橙子, 葡萄] var body: some View { VStack(spacing: 16) { ForEach(items, id: \.self) { item in DragItemCard(title: item) .onDrag { let provider NSItemProvider(object: item as NSString) // 自定义拖拽预览 provider.previewImageHandler { (_, _, _) - Void in // 自定义预览逻辑 } return provider } } } .padding() } } struct DragItemCard: View { let title: String var body: some View { HStack { Image(systemName: line.3.horizontal) .foregroundColor(.gray) Text(title) Spacer() Image(systemName: chevron.right) .font(.caption) .foregroundColor(.gray) } .padding() .background(Color.white) .cornerRadius(8) .shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1) } } // MARK: - 自定义拖拽缩略图 struct CustomThumbnailDragView: View { State private var items [设计稿, 代码文件, 资源包, 文档] var body: some View { VStack(spacing: 16) { ForEach(items, id: \.self) { item in Text(item) .padding() .frame(maxWidth: .infinity) .background(Color.purple.opacity(0.2)) .cornerRadius(8) .onDrag { let provider NSItemProvider(object: item as NSString) // 设置预览视图 provider.previewProvider { let previewView UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) let label UILabel(frame: previewView.bounds) label.text item label.textAlignment .center label.backgroundColor UIColor.purple label.textColor .white label.layer.cornerRadius 8 label.clipsToBounds true previewView.addSubview(label) return previewView } return provider } } } .padding() } }二、列表重排序2.1 基本列表重排序swift// MARK: - 可拖拽重排序列表 struct DragReorderListView: View { State private var items [任务1, 任务2, 任务3, 任务4, 任务5] State private var draggingItem: String? var body: some View { VStack { Text(长按拖拽手柄重新排序) .font(.caption) .foregroundColor(.secondary) .padding(.top) List { ForEach(items, id: \.self) { item in HStack { Image(systemName: line.3.horizontal) .foregroundColor(.gray) .onDrag { self.draggingItem item return NSItemProvider(object: item as NSString) } Text(item) } .padding(.vertical, 8) } .onMove { source, destination in items.move(fromOffsets: source, toOffset: destination) } } .listStyle(.plain) } } } // MARK: - 使用onDrag/onDrop实现自定义重排序 struct CustomDragReorderView: View { State private var items [设计, 开发, 测试, 部署, 维护] State private var draggingIndex: Int? var body: some View { VStack(spacing: 12) { ForEach(Array(items.enumerated()), id: \.element) { index, item in HStack { Image(systemName: line.3.horizontal) .foregroundColor(.gray) .onDrag { self.draggingIndex index return NSItemProvider(object: item as NSString) } Text(item) Spacer() Text(\(index 1)) .font(.caption) .foregroundColor(.secondary) } .padding() .background(Color(.systemGray6)) .cornerRadius(8) .onDrop(of: [.text], delegate: DragReorderDelegate( item: item, currentIndex: index, draggingIndex: $draggingIndex, items: $items )) } } .padding() } } struct DragReorderDelegate: DropDelegate { let item: String let currentIndex: Int Binding var draggingIndex: Int? Binding var items: [String] func performDrop(info: DropInfo) - Bool { draggingIndex nil return true } func dropEntered(info: DropInfo) { guard let draggingIndex draggingIndex, draggingIndex ! currentIndex else { return } withAnimation(.spring()) { let movingItem items[draggingIndex] items.remove(at: draggingIndex) items.insert(movingItem, at: currentIndex) self.draggingIndex currentIndex } } }2.2 跨列表拖拽swift// MARK: - 两列看板(待办/已完成) struct TwoColumnDragView: View { State private var todoItems [整理文档, 回复邮件, 代码审查, 测试应用] State private var doneItems [参加会议, 提交报告] var body: some View { HStack(alignment: .top, spacing: 20) { // 待办列 DragColumn( title: 待办, items: $todoItems, backgroundColor: .orange.opacity(0.1), dropColor: .orange.opacity(0.3) ) // 已完成列 DragColumn( title: 已完成, items: $doneItems, backgroundColor: .green.opacity(0.1), dropColor: .green.opacity(0.3) ) } .padding() } } struct DragColumn: View { let title: String Binding var items: [String] let backgroundColor: Color let dropColor: Color State private var isTargeted false var body: some View { VStack(alignment: .leading, spacing: 12) { Text(title) .font(.headline) .padding(.leading) VStack(spacing: 8) { ForEach(items, id: \.self) { item in Text(item) .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.white) .cornerRadius(8) .shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1) .onDrag { NSItemProvider(object: item as NSString) } } } .frame(maxWidth: .infinity) .padding(8) .background(isTargeted ? dropColor : backgroundColor) .cornerRadius(12) .onDrop(of: [.text], isTargeted: $isTargeted) { providers in guard let provider providers.first else { return false } provider.loadObject(ofClass: NSString.self) { object, _ in if let text object as? String { DispatchQueue.main.async { items.append(text) } } } return true } } .frame(maxWidth: .infinity) } }三、图片拖拽3.1 图片拖入拖出swift// MARK: - 图片拖拽画廊 struct ImageDragGallery: View { State private var images: [UIImage] [] State private var isDropTargeted false let columns [ GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()) ] var body: some View { VStack { // 图片网格 ScrollView { LazyVGrid(columns: columns, spacing: 16) { ForEach(images.indices, id: \.self) { index in Image(uiImage: images[index]) .resizable() .scaledToFill() .frame(width: 100, height: 100) .clipped() .cornerRadius(8) .onDrag { // 提供图片数据 guard let data images[index].pngData() else { return NSItemProvider() } return NSItemProvider(item: data as NSData, typeIdentifier: UTType.png.identifier) } } } .padding() } // 拖拽目标区域 VStack { Image(systemName: photo.on.rectangle.angled) .font(.largeTitle) Text(拖拽图片到这里添加) .font(.caption) } .frame(maxWidth: .infinity) .frame(height: 120) .background(isDropTargeted ? Color.blue.opacity(0.2) : Color.gray.opacity(0.1)) .cornerRadius(12) .onDrop(of: [.image], isTargeted: $isDropTargeted) { providers in for provider in providers { provider.loadObject(ofClass: UIImage.self) { image, _ in if let uiImage image as? UIImage { DispatchQueue.main.async { images.append(uiImage) } } } } return true } } .padding() } }3.2 多图片拖拽swift// MARK: - 多图片拖拽 struct MultiImageDragView: View { State private var sourceImages: [UIImage] [ UIImage(systemName: photo)!, UIImage(systemName: camera)!, UIImage(systemName: video)! ] State private var targetImages: [UIImage] [] State private var draggedImages: [UIImage] [] var body: some View { HStack(spacing: 20) { // 源区域 VStack { Text(图片库) .font(.headline) LazyVGrid(columns: [GridItem(.flexible())], spacing: 12) { ForEach(sourceImages.indices, id: \.self) { index in Image(uiImage: sourceImages[index]) .resizable() .scaledToFit() .frame(height: 60) .cornerRadius(8) .onDrag { draggedImages [sourceImages[index]] let provider NSItemProvider(item: sourceImages[index].pngData() as NSData?, typeIdentifier: UTType.png.identifier) return provider } } } } .frame(maxWidth: .infinity) .padding() .background(Color.gray.opacity(0.1)) .cornerRadius(12) // 目标区域 VStack { Text(相册) .font(.headline) LazyVGrid(columns: [GridItem(.flexible())], spacing: 12) { ForEach(targetImages.indices, id: \.self) { index in Image(uiImage: targetImages[index]) .resizable() .scaledToFit() .frame(height: 60) .cornerRadius(8) } } } .frame(maxWidth: .infinity) .padding() .background(Color.gray.opacity(0.1)) .cornerRadius(12) .onDrop(of: [.image], isTargeted: nil) { providers in for provider in providers { provider.loadObject(ofClass: UIImage.self) { image, _ in if let uiImage image as? UIImage { DispatchQueue.main.async { targetImages.append(uiImage) if let index sourceImages.firstIndex(where: { $0.pngData() uiImage.pngData() }) { sourceImages.remove(at: index) } } } } } return true } } .padding() } }四、自定义数据拖拽4.1 自定义数据类型swift// MARK: - 自定义数据类型支持拖拽 // 自定义类型标识符 extension UTType { static let todoItem UTType(exportedAs: com.example.todo-item) } // 自定义数据模型 struct TodoItem: Codable, Identifiable, Equatable { let id UUID() let title: String let priority: Int let dueDate: Date } // 使TodoItem支持拖拽 extension TodoItem: Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .todoItem) } } struct CustomDataDragView: View { State private var todoItems: [TodoItem] [ TodoItem(title: 完成项目报告, priority: 1, dueDate: Date()), TodoItem(title: 团队会议, priority: 2, dueDate: Date().addingTimeInterval(86400)), TodoItem(title: 代码审查, priority: 1, dueDate: Date().addingTimeInterval(172800)) ] State private var isReordering false var body: some View { VStack { Text(自定义待办列表) .font(.headline) ForEach(todoItems) { item in CustomTodoCard(item: item) .padding(.horizontal) .onDrag { NSItemProvider(object: try! JSONEncoder().encode(item) as NSData) } .onDrop(of: [.data], delegate: TodoDropDelegate( item: item, items: $todoItems, isReordering: $isReordering )) } } .padding() } } struct CustomTodoCard: View { let item: TodoItem var body: some View { HStack { Image(systemName: line.3.horizontal) .foregroundColor(.gray) VStack(alignment: .leading) { Text(item.title) .font(.headline) Text(优先级: \(item.priority)) .font(.caption) Text(item.dueDate, format: .dateTime.month().day()) .font(.caption2) .foregroundColor(.secondary) } Spacer() } .padding() .background(Color(.systemGray6)) .cornerRadius(8) } } struct TodoDropDelegate: DropDelegate { let item: TodoItem Binding var items: [TodoItem] Binding var isReordering: Bool func performDrop(info: DropInfo) - Bool { isReordering false return true } func dropEntered(info: DropInfo) { guard let draggedData info.itemProviders(for: [.data]).first else { return } draggedData.loadObject(ofClass: Data.self) { data, _ in guard let data data, let draggedItem try? JSONDecoder().decode(TodoItem.self, from: data), let fromIndex items.firstIndex(of: draggedItem), let toIndex items.firstIndex(of: item), fromIndex ! toIndex else { return } DispatchQueue.main.async { withAnimation(.spring()) { items.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: toIndex fromIndex ? toIndex 1 : toIndex) isReordering true } } } } }4.2 拖拽动画与反馈swift// MARK: - 带动画的拖拽交互 struct AnimatedDragView: View { State private var items [Alpha, Beta, Gamma, Delta] State private var dragLocation: CGPoint .zero State private var isDragging false var body: some View { VStack(spacing: 20) { Text(拖拽项目到右侧垃圾箱删除) .font(.caption) HStack(alignment: .top, spacing: 40) { // 项目列表 VStack(spacing: 12) { ForEach(items, id: \.self) { item in Text(item) .padding() .frame(width: 150) .background(Color.blue) .foregroundColor(.white) .cornerRadius(8) .onDrag { let provider NSItemProvider(object: item as NSString) return provider } } } // 垃圾箱目标 Image(systemName: trash) .font(.system(size: 60)) .foregroundColor(isDragging ? .red : .gray) .scaleEffect(isDragging ? 1.2 : 1) .animation(.spring(), value: isDragging) .onDrop(of: [.text], isTargeted: $isDragging) { providers in guard let provider providers.first else { return false } provider.loadObject(ofClass: NSString.self) { object, _ in if let text object as? String { DispatchQueue.main.async { withAnimation(.spring()) { items.removeAll { $0 text } } // 提供触感反馈 let generator UINotificationFeedbackGenerator() generator.notificationOccurred(.success) } } } return true } } } .padding() } }五、实战看板应用现在让我们构建一个完整的可拖拽看板应用。swiftimport SwiftUI import UniformTypeIdentifiers // MARK: - 数据模型 struct TaskItem: Identifiable, Codable, Equatable { let id UUID() let title: String let description: String let priority: Priority let createdAt: Date enum Priority: String, Codable, CaseIterable { case low 低 case medium 中 case high 高 var color: Color { switch self { case .low: return .green case .medium: return .orange case .high: return .red } } } } extension TaskItem: Transferable { static var transferRepresentation: some TransferRepresentation { CodableRepresentation(contentType: .taskItem) } } extension UTType { static let taskItem UTType(exportedAs: com.example.task-item) } struct Column: Identifiable, Codable { let id UUID() let name: String var tasks: [TaskItem] } // MARK: - 示例数据 extension Column { static let sampleData: [Column] [ Column(name: 待办, tasks: [ TaskItem(title: 设计UI, description: 完成主界面设计, priority: .high, createdAt: Date()), TaskItem(title: 编写文档, description: 更新API文档, priority: .medium, createdAt: Date()) ]), Column(name: 进行中, tasks: [ TaskItem(title: 开发功能, description: 实现拖拽交互, priority: .high, createdAt: Date()) ]), Column(name: 已完成, tasks: [ TaskItem(title: 需求分析, description: 完成需求文档, priority: .low, createdAt: Date()) ]) ] } // MARK: - 视图模型 MainActor class KanbanViewModel: ObservableObject { Published var columns: [Column] Column.sampleData Published var draggingTask: TaskItem? func moveTask(task: TaskItem, from sourceColumn: Column, to destinationColumn: Column, at destinationIndex: Int? nil) { guard let sourceIndex columns.firstIndex(where: { $0.id sourceColumn.id }), let taskIndex columns[sourceIndex].tasks.firstIndex(where: { $0.id task.id }) else { return } let movingTask columns[sourceIndex].tasks.remove(at: taskIndex) if let destIndex columns.firstIndex(where: { $0.id destinationColumn.id }) { if let position destinationIndex { columns[destIndex].tasks.insert(movingTask, at: position) } else { columns[destIndex].tasks.append(movingTask) } } } func addTask(to column: Column, title: String, description: String, priority: TaskItem.Priority) { guard let index columns.firstIndex(where: { $0.id column.id }) else { return } let newTask TaskItem(title: title, description: description, priority: priority, createdAt: Date()) columns[index].tasks.append(newTask) } func deleteTask(_ task: TaskItem, from column: Column) { guard let columnIndex columns.firstIndex(where: { $0.id column.id }), let taskIndex columns[columnIndex].tasks.firstIndex(where: { $0.id task.id }) else { return } columns[columnIndex].tasks.remove(at: taskIndex) } } // MARK: - 主视图 struct KanbanBoardView: View { StateObject private var viewModel KanbanViewModel() State private var showingAddSheet false State private var selectedColumn: Column? var body: some View { NavigationView { ScrollView(.horizontal, showsIndicators: false) { HStack(alignment: .top, spacing: 16) { ForEach(viewModel.columns) { column in KanbanColumn( column: column, viewModel: viewModel ) } // 添加列按钮 AddColumnButton() } .padding() } .navigationTitle(看板) .toolbar { ToolbarItem(placement: .topBarTrailing) { Button { // 菜单 } label: { Image(systemName: ellipsis.circle) } } } .sheet(isPresented: $showingAddSheet) { if let column selectedColumn { AddTaskSheet(column: column, viewModel: viewModel) } } } } } // MARK: - 看板列 struct KanbanColumn: View { let column: Column ObservedObject var viewModel: KanbanViewModel State private var isTargeted false State private var showingAddSheet false var body: some View { VStack(alignment: .leading, spacing: 12) { // 列标题 HStack { Text(column.name) .font(.headline) Spacer() Text(\(column.tasks.count)) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 2) .background(Color.gray.opacity(0.2)) .cornerRadius(10) Button { showingAddSheet true } label: { Image(systemName: plus.circle) .font(.title3) } .buttonStyle(.plain) } .padding(.horizontal, 8) // 任务列表 ScrollView { VStack(spacing: 10) { ForEach(column.tasks) { task in KanbanTaskCard(task: task, column: column, viewModel: viewModel) .onDrag { viewModel.draggingTask task return NSItemProvider(object: try! JSONEncoder().encode(task) as NSData) } } } .padding(.vertical, 4) } .frame(minHeight: 300) .background( RoundedRectangle(cornerRadius: 12) .fill(isTargeted ? Color.blue.opacity(0.1) : Color.gray.opacity(0.05)) ) .onDrop(of: [.taskItem], isTargeted: $isTargeted) { providers in guard let provider providers.first, let draggingTask viewModel.draggingTask else { return false } provider.loadObject(ofClass: Data.self) { data, _ in if let data data, let task try? JSONDecoder().decode(TaskItem.self, from: data) { DispatchQueue.main.async { withAnimation { let sourceColumn viewModel.columns.first(where: { $0.tasks.contains(where: { $0.id task.id }) }) if let source sourceColumn { viewModel.moveTask(task: task, from: source, to: column) } viewModel.draggingTask nil } // 触感反馈 let generator UIImpactFeedbackGenerator(style: .medium) generator.impactOccurred() } } } return true } } .frame(width: 320) .sheet(isPresented: $showingAddSheet) { AddTaskSheet(column: column, viewModel: viewModel) } } } // MARK: - 任务卡片 struct KanbanTaskCard: View { let task: TaskItem let column: Column ObservedObject var viewModel: KanbanViewModel State private var showingDeleteConfirmation false var body: some View { VStack(alignment: .leading, spacing: 8) { HStack { Text(task.title) .font(.headline) .lineLimit(1) Spacer() Menu { Button(role: .destructive) { showingDeleteConfirmation true } label: { Label(删除, systemImage: trash) } } label: { Image(systemName: ellipsis) .font(.caption) .foregroundColor(.gray) } } Text(task.description) .font(.caption) .foregroundColor(.secondary) .lineLimit(2) HStack { Circle() .fill(task.priority.color) .frame(width: 8, height: 8) Text(task.priority.rawValue) .font(.caption2) .foregroundColor(task.priority.color) Spacer() Text(task.createdAt, format: .dateTime.month().day()) .font(.caption2) .foregroundColor(.secondary) } } .padding(12) .background(Color(.systemBackground)) .cornerRadius(10) .shadow(color: .black.opacity(0.05), radius: 2, x: 0, y: 1) .overlay( RoundedRectangle(cornerRadius: 10) .stroke(Color.gray.opacity(0.2), lineWidth: 1) ) .confirmationDialog(删除任务, isPresented: $showingDeleteConfirmation) { Button(删除, role: .destructive) { withAnimation { viewModel.deleteTask(task, from: column) } } Button(取消, role: .cancel) { } } message: { Text(确定要删除\\(task.title)\吗) } } } // MARK: - 添加列按钮 struct AddColumnButton: View { State private var showingAlert false State private var columnName var body: some View { Button { showingAlert true } label: { VStack { Image(systemName: plus) .font(.title) Text(添加列表) .font(.caption) } .frame(width: 120, height: 100) .background(Color.gray.opacity(0.1)) .cornerRadius(12) } .alert(新建列表, isPresented: $showingAlert) { TextField(列表名称, text: $columnName) Button(取消, role: .cancel) { } Button(创建) { // 添加列逻辑 } } } } // MARK: - 添加任务表单 struct AddTaskSheet: View { let column: Column ObservedObject var viewModel: KanbanViewModel Environment(\.dismiss) var dismiss State private var title State private var description State private var priority: TaskItem.Priority .medium var body: some View { NavigationView { Form { Section { TextField(任务标题, text: $title) TextField(任务描述, text: $description, axis: .vertical) .frame(minHeight: 80) } Section { Picker(优先级, selection: $priority) { ForEach(TaskItem.Priority.allCases, id: \.self) { p in HStack { Circle() .fill(p.color) .frame(width: 8, height: 8) Text(p.rawValue) } .tag(p) } } .pickerStyle(.segmented) } } .navigationTitle(新建任务) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button(取消) { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button(添加) { guard !title.isEmpty else { return } viewModel.addTask(to: column, title: title, description: description, priority: priority) dismiss() } .disabled(title.isEmpty) } } } } } // MARK: - 应用入口 main struct KanbanApp: App { var body: some Scene { WindowGroup { KanbanBoardView() } } }

相关文章:

从零开始学iOS开发(第三十二篇):SwiftUI 拖拽交互 —— 构建流畅的拖放体验

欢迎来到本系列教程的第三十二篇。在前三十一篇中,你已经学习了从Swift基础到数据可视化的全方位iOS开发技能。现在,你能够构建出功能完善、数据清晰的应用了。但是,如何让用户与应用进行更自然的交互?如何让用户通过拖拽来重新排…...

软考高项-案例万金油(进度成本纠偏)

进度纠偏措施:赶工。投入更多的资源或增加工作时间,以缩短关键活动的工期。快速跟进。并行施工,以缩短关键路径的长度。高效替换低效。使用高素质的资源或经验更丰富的人员。采用新技术。改进方法或技术,以提高生产效率。缩小范围…...

上市公司会计审计报告5种意见的含义,看完秒懂

上市公司会计审计报告5种意见的含义,看完秒懂 关键词:审计报告类型、无保留意见、保留意见、否定意见、无法表示意见、财务审计科普表1-1 会计师出具意见与其真实意思对照会计师出具意见会计师真实意思标准无保留意见的审计报告造假迹象未被本人发现附带…...

终极指南:3步掌握LSPatch免Root模块注入框架

终极指南:3步掌握LSPatch免Root模块注入框架 【免费下载链接】LSPatch LSPatch: A non-root Xposed framework extending from LSPosed 项目地址: https://gitcode.com/gh_mirrors/ls/LSPatch 在Android生态系统中,LSPatch作为一款创新的免Root X…...

CSS怎样调整弹性项目排列顺序_使用order属性轻松控制DOM显示顺序

order属性未生效最常见的原因是父容器未设置display: flex或inline-flex;它仅作用于弹性项目,且按数值升序排列,不影响DOM顺序及可访问性。order属性为什么没生效最常见的原因是父容器没设 display: flex 或 display: inline-flex。Flex布局里…...

Dev Containers + Kubernetes本地沙箱联动失效?2026年3大厂商联合认证的5步跨集群同步协议(含YAML原子模板)

更多请点击: https://intelliparadigm.com 第一章:Dev Containers与Kubernetes本地沙箱协同失效的根本归因分析 当开发者在 VS Code 中启用 Dev Container 并同时运行 Kind 或 Minikube 作为本地 Kubernetes 沙箱时,网络隔离、权限模型与容器…...

从开发到部署:用Docker Compose封装你的MySQL+phpMyAdmin本地开发环境(附完整yml文件)

从开发到部署:用Docker Compose封装你的MySQLphpMyAdmin本地开发环境(附完整yml文件) 在当今快节奏的开发环境中,能够快速搭建、复制和销毁开发环境已成为现代开发者的核心竞争力之一。想象一下这样的场景:你刚刚加入一…...

达梦DM8 JDBC连接串配置避坑指南:从单机到集群,这些参数你配对了吗?

达梦DM8 JDBC连接串深度优化实战:高并发场景下的参数配置艺术 当Java应用与达梦DM8数据库相遇时,连接串配置这个看似简单的环节往往成为系统稳定性的"阿喀琉斯之踵"。我曾亲眼目睹一个日活百万的金融系统因switchInterval参数误配导致集群切换…...

别再手动排UV了!3dMax 2024搭配这5款插件,效率直接翻倍(附保姆级安装教程)

3dMax 2024 UV工作流革命:5款插件组合拳实战指南 UV展开是3D建模流程中最耗时的环节之一。传统手动操作不仅效率低下,还容易导致纹理拉伸、接缝错位等问题。对于游戏美术和建筑可视化从业者来说,批量处理复杂模型的UV需求尤为迫切。本文将深入…...

别再手动填地址了!LabVIEW 2020 Modbus TCP批量读取与数据解析技巧分享

LabVIEW 2020 Modbus TCP高效开发:批量读取与智能解析实战 在工业自动化领域,Modbus TCP协议因其简单可靠的特点,成为设备通信的主流选择。但面对需要同时监控数十甚至上百个数据点的复杂系统,传统的手动配置方式不仅效率低下&…...

Boss-Key终极指南:Windows窗口智能隐藏与隐私保护完整教程

Boss-Key终极指南:Windows窗口智能隐藏与隐私保护完整教程 【免费下载链接】Boss-Key 老板来了?快用Boss-Key老板键一键隐藏静音当前窗口!上班摸鱼必备神器 项目地址: https://gitcode.com/gh_mirrors/bo/Boss-Key Boss-Key是一款专为…...

基于多维数据分析的PID参数智能优化系统:工业级控制性能提升框架

基于多维数据分析的PID参数智能优化系统:工业级控制性能提升框架 【免费下载链接】PIDtoolbox PIDtoolbox is a set of graphical tools for analyzing blackbox log data 项目地址: https://gitcode.com/gh_mirrors/pi/PIDtoolbox PIDtoolbox是一款面向工业…...

敏捷教练的必备工具箱:让团队真正“敏捷”起来

在敏捷转型的浪潮中,软件测试从业者扮演着至关重要的角色。我们既是质量关隘的守卫者,也是流程效率的体验者与反馈者。然而,许多团队的“敏捷”实践常常流于形式,站会、看板、迭代回顾一应俱全,却未能触及敏捷的核心—…...

用LVGL v8.3设计一个简洁的状态栏:从布局对齐到响应式适配的完整实践

用LVGL v8.3设计一个简洁的状态栏:从布局对齐到响应式适配的完整实践 在嵌入式UI开发中,状态栏作为用户界面的"信息中枢",既要保证关键信息的清晰展示,又要适应不同屏幕尺寸的变化。LVGL v8.3作为轻量级图形库的佼佼者&…...

3步完成:如何在Chrome浏览器中快速转换网页图片格式

3步完成:如何在Chrome浏览器中快速转换网页图片格式 【免费下载链接】Save-Image-as-Type Save Image as Type is an chrome extension which add Save as PNG / JPG / WebP to the context menu of image. 项目地址: https://gitcode.com/gh_mirrors/sa/Save-Ima…...

物联网设备OTA升级避坑指南:从Bootloader设计到固件回滚策略

物联网设备OTA升级避坑指南:从Bootloader设计到固件回滚策略 当数千台设备已部署在偏远地区时,凌晨三点收到现场升级失败的报警邮件——这种场景对物联网开发者而言绝不陌生。OTA升级看似只是简单的文件传输,实则暗藏从网络抖动到存储损坏等二…...

告别信号衰减!PCIe 5.0硬件设计实战:从板材选择到玻纤效应的完整避坑指南

PCIe 5.0硬件设计实战:从板材选择到玻纤效应的完整避坑指南 当32GT/s的高速信号在PCB走线上疾驰时,每一个设计细节都可能成为性能的绊脚石。作为经历过三代PCIe标准迭代的硬件工程师,我至今记得第一次看到PCIe 5.0眼图崩溃时的震撼——那些理…...

嵌入式内存管理避坑指南:从GD32F470的TCMSRAM设计,聊聊多块非连续SRAM的实战分配策略

嵌入式系统多块非连续SRAM的高效管理策略与实践 在嵌入式系统开发中,内存管理一直是工程师面临的核心挑战之一。当我们使用像GD32F470这类将SRAM物理分割成多块的MCU时,如何合理规划这些非连续的内存区域,直接关系到项目的稳定性和性能表现。…...

开源网盘直链下载解决方案:LinkSwift 技术架构与实战指南

开源网盘直链下载解决方案:LinkSwift 技术架构与实战指南 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天…...

告别‘纸面协议’:用Python模拟UE的LTE附着与PDN连接建立全过程(含PGW选择逻辑)

用Python构建LTE附着流程模拟器:从APN解析到PGW选择的实战指南 当我们在手机上看到4G信号满格时,背后正上演着一场精密的网络协奏曲。作为开发者,理解LTE核心网流程不仅有助于排查网络问题,更能为5G核心网开发打下基础。本文将用P…...

C# OnnxRuntime 部署 DINOv3 密集特征可视化

说明官网地址:https://github.com/facebookresearch/dinov3效果模型信息Model Properties ------------------------- ---------------------------------------------------------------Inputs ------------------------- name:input tensor&#xff1a…...

AM32电调PID调参实战:手把手配置电流环、速度环与抗堵转PID

AM32电调PID调参实战:从电流环到抗堵转的精细控制 在FPV竞速和航拍领域,电机响应速度的毫秒级差异往往决定着比赛胜负或镜头稳定性。AM32固件作为开源电调方案的集大成者,其多环PID控制系统提供了近乎工业级的调节维度。本文将带您深入电流环…...

日记 3.0:我用 Hermes+Obsidian,把流水账日记变成洞察与成长的飞轮,基于 Karpathy 日记法演进

日记 3.0:我用 Obsidian Hermes,把流水账变成洞察与成长的飞轮(周洞察版)这是我《Hermes Agent 养成指南》系列的第 12 篇文章。如果你也期望更系统的学习和应用 Hermes,不妨点个关注,一起学习交流。如果你…...

Mac Mouse Fix终极指南:3步让你的普通鼠标变身Mac生产力神器

Mac Mouse Fix终极指南:3步让你的普通鼠标变身Mac生产力神器 【免费下载链接】mac-mouse-fix Mac Mouse Fix - Make Your $10 Mouse Better Than an Apple Trackpad! 项目地址: https://gitcode.com/GitHub_Trending/ma/mac-mouse-fix 你是否曾为macOS上第三…...

Go语言for循环如何写_Go语言for循环语法教程【经典】.txt

MailKit批量发送邮件卡在SendAsync因缺乏并发控制,需用SemaphoreSlim限流、复用SmtpClient、单建MimeMessage、用BodyBuilder构建HTML正文并内联样式,逐封捕获异常定位问题。MailKit 发送批量邮件时为什么总卡在 SmtpClient.SendAsync?因为默…...

如何卸载并重装Oracle Grid_Deinstall脚本与ASM磁盘清理

...

杰理之外部使用多算法授权或者使用到了CRC校验概率会导致80S时间点上otp_api_verify死机【篇】

u16 chip_crc16(void *ptr, u32 len) { return CRC16(ptr,len); }...

一声唤醒 万物响应|AtomGit 首款开源鸿蒙 AI 硬件「小鸿」发布(附网页地址)

2026 年 4 月 28 日,AtomGit 在深圳正式发布首款开源鸿蒙 AI 硬件 XiaoHong「小鸿」。本次发布会以「一声唤醒,万物响应」为主题,推出基于 OpenHarmony 原生打造的开放式智能中枢,标志着 AI 硬件从“设备”迈向“入口”的重要一步…...

DataRoom大屏设计器快速上手指南:5步打造专业数据可视化

DataRoom大屏设计器快速上手指南:5步打造专业数据可视化 【免费下载链接】DataRoom 🔥基于SpringBoot、MyBatisPlus、ElementUI、G2Plot、Echarts等技术栈的大屏设计器,具备目录管理、DashBoard设计、预览能力,支持MySQL、Oracle、…...

终极解决方案:30秒快速重置JetBrains IDE试用期,免费延长开发工具使用时间

终极解决方案:30秒快速重置JetBrains IDE试用期,免费延长开发工具使用时间 【免费下载链接】ide-eval-resetter 项目地址: https://gitcode.com/gh_mirrors/id/ide-eval-resetter 你是否曾经因为JetBrains IDE试用期到期而中断开发工作&#xff…...