从零开始学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() } } }
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2564057.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!