Home 寫程式SwiftUI – Drop & Drag List

SwiftUI – Drop & Drag List

by 艾普利
Adobe Firefly

忘記是那一年的WWDC公布了一個拖曳相關的功能,主要是讓你可以將iPhone 的資料像是圖片之類的丟到iPad 或Mac 上,因此有了 API 可以用在 Drop & Drag 動作上。

最近因為工作需要花了一些時間在研究Drop & Drag 應該要怎麼寫,需要實作的是一般的拖拉元件改變它的順序,並不需要讓它能拖到另一個裝置上,寫作上就會比較簡單一點。

參考以下文件與官方網站來練習

Swift Practice # 177 SwiftUI 拖曳 Drop & Drag 改變陣列Array位置

Drag and Drop List in SwiftUI

Apple Documentation – Drag and Drop


Start

一開始當然要先做一個要進行拖曳的List,建立一個顯示各種茶的List

建立List 的 Item View

struct TeaItemView: View {
    
    var title: String
    
    var body: some View {
        HStack {
            Text(title)
                .font(.system(size: 26))
        }
        .frame(maxWidth: .infinity)
        .padding(.vertical, 30)
        .background(.green)
        .clipShape(RoundedRectangle(cornerRadius: 20))
        
    }
}

再建立一個用 ForEach 做的 List

import SwiftUI

struct ContentView: View {
    
    @State private var draggedItem: String?
    @State private var fruitItems = ["🫖 Tea", " ☕️ Coffee", " 🧃 Juice", "🧋Pearl milk tea", "🥛 Milk", "🍹 Fruit tea", "🧉 Mate tea", "🍵 Matcha tea"]
    
    var body: some View {
        
        ScrollView(showsIndicators: false) {
            VStack(spacing: 10) {
                ForEach(fruitItems, id: \.self) { fruit in
                    TeaItemView(title: fruit)
                }
            }
            .padding(.horizontal, 20)
        }
        .background(.black)
    }
}

draggedItem 是負責記錄要被拖曳的元件,再來Run 一下程式,會出現下面畫面

DrogDelegate

如果要讓List 的元件可以被拖曳,必須實作 DropDelegate 這個Protocol,新增一個 DropDragDelegate 來實作它

import SwiftUI

struct DropDrapDelegate: DropDelegate {
    
    let destinationItem: String
    
    @Binding var fruits: [String]
    @Binding var draggedItem: String?
    
    func dropUpdated(info: DropInfo) -> DropProposal? {
        return DropProposal(operation: .move)
    }
    
    func performDrop(info: DropInfo) -> Bool {
        draggedItem = nil
        return true
    }
    
    func dropEntered(info: DropInfo) {
        if let draggedItem {
            let fromIndex = fruits.firstIndex(of: draggedItem)
            if let fromIndex {
                let toIndex = fruits.firstIndex(of: destinationItem)
                if let toIndex, fromIndex != toIndex {
                    withAnimation {
                        self.fruits.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: (toIndex > fromIndex ? (toIndex + 1) : toIndex))
                    }
                }
            }
        }
    }
}

在這個 Delegate 中有幾個需要實作的 function

func dropUpdated(info: DropInfo) -> DropProposal?

宣告這個拖曳是要什麼動作,以範例來說只是要做移動,所以是 .move,可以去官方文件查看 DropProposal 可以知道更多設定值,預設值會是 nil

func performDrop(info: DropInfo) -> Bool

拖曳完成的時候會呼叫到它,回傳 true 表示 Drop was successful

func dropEntered(info: DropInfo)

在這個拖曳要做些什麼? 這是我對這個function 的理解,這個範例主要是要交換順序,所以裡面寫的就是交換的方式

View

再回到 View ,將 OnDropOnDrag 加到剛剛寫好的 List 中

ScrollView(showsIndicators: false) {
    VStack(spacing: 10) {
        ForEach(fruitItems, id: \.self) { fruit in
            TeaItemView(title: fruit)
                .onDrag({
                    draggedItem = fruit
                    return NSItemProvider()
                })
                .onDrop(of: [.text], delegate: DropDrapDelegate(destinationItem: fruit, fruits: $fruitItems, draggedItem: $draggedItem))
            
        }
    }
    .padding(.horizontal, 20)
}
.background(.black)

onDrag

這個的重點在於 NSItemProviderNSItemProvider 它是為了在進行 Drag-and-drop 或是 Copy-and-paste 時傳送資料或檔案用的,設定方式我自己是不太理解,常見的設定是 URL,這個範例用不到,就直接建立一個空的ItemProvider,若有需要傳送資料到不同裝置上的話應該需要留意一下。

onDrop(of:delegate:)

設定Type與Delegate ,Delegate 就是先前建立的 DropDrapDelegate,直接代需要的參考就好。

Type的設定有二種方式,第一種是給它 The uniform type identifiers ,這是需要可以辨識要傳送的資料,查了一下,應該是可以自定這個 Identifier。第二種是設定 UTType ,在範例中的 [.text] 就是 UTType 裡的 Text,在官方文件中可以看到很多種類型,那UTType是什麼? 它其實就是指要進行load, send, or receive的資料類型。

加上 onDragonDrop 之後就可以進行拖曳了

可以在 Github 上下載這個範例程式

最後祝大家 Coding 愉快!!

You may also like