4.1 在 SwiftUI 中采用拖放

原文链接: https://developer.apple.com/documentation/swiftui/adopting-drag-and-drop-using-swiftui

4.1 在 SwiftUI 中采用拖放

在列表、表格和自定义视图里启用拖放交互。

概述

这个示例代码项目演示了 SwiftUI 视图如何充当拖拽源或放置目标。

要启用拖拽交互,请为一个视图添加 draggable(_:) 修饰符,从而在应用内、你自己的多个应用之间,或者在你的应用与支持导入或导出某种数据格式的其他应用之间发送或接收 Transferable 项目。要处理被放置的内容,请使用 dropDestination(for:action:isTargeted:) 修饰符来接收预期的被放置项目。

在这个示例应用中,大家可以从联系人列表中拖出一个联系人,并把它放到另一个应用里,例如“通讯录”“备忘录”或“信息”。此外,大家还可以从“通讯录”或“备忘录”等其他应用把新的联系人拖放到这个示例应用中。

在 iPad 上,大家可以在分屏视图中把这个示例与“通讯录”或“备忘录”这样的第二个应用一起使用。人们可以把该应用中的某一行拖到“通讯录”,也可以把“通讯录”中的某个联系人拖到这个应用中。

启用拖拽交互

要启用拖拽,请为一个视图添加 draggable(_:) 修饰符,用来发送遵循 Transferable 协议的项目。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
List {
    ForEach(dataModel.contacts) { contact in
        NavigationLink {
            ContactDetailView(contact: contact)
        } label: {
            CompactContactView(contact: contact)
                .draggable(contact) {
                    ThumbnailView(contact: contact)
                }
        }
    }
}

当有人从联系人列表中拖出一个联系人时,应用会使用 draggable(_:preview:) 修饰符为被拖拽的项目定义一个自定义预览。

要了解更多关于采用 draggable API 为拖拽操作提供项目的信息,请参阅 把视图变成拖拽源。

启用放置交互

使用 dropDestination(for:action:isTargeted:) 修饰符来接收被拖拽的项目,并定义处理被放置内容的目标。

1
2
3
.dropDestination(for: Contact.self) { droppedContacts, index in
    dataModel.handleDroppedContacts(droppedContacts: droppedContacts, index: index)
}

该修饰符期望一个遵循 Transferable 协议的 Contact 类型。dropDestination(for:action:isTargeted:) 修饰符的实现会使用 transferRepresentation 来接收表示被放置联系人信息的拖拽项目。

应用按优先级顺序定义各种传输表示。应用会使用最合适的表示来创建并初始化一个 Contact 对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
static var transferRepresentation: some TransferRepresentation {
    // 允许用自定义内容类型传输 Contact。
    CodableRepresentation(contentType: .exampleContact)
    // 允许以 vCard 格式导入和导出 Contact 数据。
    DataRepresentation(contentType: .vCard) { contact in
        try contact.toVCardData()
    } importing: { data in
        try await parseVCardData(data)
    }
    .suggestedFileName { $0.fullName }
    // 允许把 `phoneNumber` 字符串作为整个 `Contact` 的代理导出。
    ProxyRepresentation { contact in
        contact.phoneNumber
    } importing: { value in
        Contact(id: UUID().uuidString, givenName: value, familyName: "", phoneNumber: "")
    }
}

如果应用收到一个带有自定义 统一类型标识符(例如 com.example.contact)的项目,它会使用 CodableRepresentation 来表示 Contact 数据结构。

示例应用声明的 com.example.contact 遵循 public.contact。为了确保操作系统知道如何处理该内容类型,它应遵循 UTType.data、UTType.package,或者从这两者之一继承的某个类型。

ProxyRepresentation 充当一种替代表示,让大家可以把联系人拖放到任何不支持 com.example.contact 或 vCard 内容、但支持文本格式的文本编辑器中。在这种情况下,应用会把 phoneNumber 导出为字符串。当有人把字符串放到 List 或 Table 上时,示例应用会用 ProxyRepresentation 把这个字符串转换为 Contact 对象。

最后,DataRepresentation 会创建 Contact 对象的二进制表示,并为支持 vCard 内容类型的接收方构造相应的值。

当有人把联系人放到 Table 或 List 上时,完成处理器会把被放置的联系人插入到放置位置处的联系人集合中。如果这次放置没有指定索引,完成处理器会把被放置的联系人添加到集合末尾。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func handleDroppedContacts(droppedContacts: [Contact], index: Int? = nil) {
    guard let firstContact = droppedContacts.first else {
        return
    }
    // 如果第一个联系人的 ID 已经存在于联系人列表中,
    // 就把该联系人从当前位置移动到新的索引处。
    // 如果没有指定索引,就把该联系人插入到列表末尾。
    if let existingIndex = contacts.firstIndex(where: { $0.id == firstContact.id }) {
        let indexSet = IndexSet(integer: existingIndex)
        contacts.move(fromOffsets: indexSet, toOffset: index ?? contacts.endIndex)
    } else {
        contacts.insert(firstContact, at: index ?? contacts.endIndex)
    }
}

最后,dropDestination(for:action:isTargeted:) 修饰符可以接收起始于任何其他视图或应用的拖拽交互。

关于采用拖放的设计指导,请参阅 Human Interface Guidelines > 拖放。