Dev-iOS/UI

[UI] Drag & Drop - (SwiftUI 2.0 - iOS 14 이상)

lafortune 2023. 7. 12. 11:22
반응형

SwiftUI에서는 .onDrag와 .onDrop을 사용하여 뷰에 Drag & Drop 기능을 추가할 수 있습니다.

 

SwiftUI에서는 Drag & Drop을 구현하려면 .onDrag / .onDrop Modifier와 DropDelegate가 필요합니다.

 

1. 필수 항목

1.) .onDrag Modifier


.onDrag 는 사용자가 뷰를 드래그할 때 발생하는 액션을 정의합니다.

 

이 Modifier는 드래그 액션이 시작되는 동안 실행되는 클로저를 파라미터로 받습니다. 이 클로저는 NSItemProvider를 반환해야 합니다.

NSItemProvider에 대한 간단한 설명은 이곳에서 ->  https://lafortune.tistory.com/17

NSItemProvider는 드래그하는 동안 전달되는 데이터를 나타냅니다.

 

 

Assets에 있는 이미지를 이용한다고 하면 이미지의 URL을 NSItemProvider에 전달합니다.

(item은 NSSecureCoding 프로토콜을 받기 때문에 NSSecureCoding으로 형 변환합니다, typeIdentifier는 전달되는 데이터에 대한 식별자를 작성합니다. URL의 타입이기 때문에 kUTTypeURL을 작성합니다.)

kUTType에 대한 간단한 설명은 이곳에서 -> https://lafortune.tistory.com/18
.onDrag { // 테스트를 위해 강제 언래핑
    NSItemProvider(item: .some(URL(string: "이미지이름")! as NSSecureCoding), typeIdentifier: String(kUTTypeURL))
}

 

2). onDrop Modifier

 

.onDrop는 사용자가 뷰에 항목을 드롭할 때 발생하는 액션을 정의합니다. 

 

이 Modifier는 드롭을 처리하는 데 사용되는 DropDelegate를 파라미터로 받습니다.

DropDelegate는 드롭 작업을 정교하게 제어할 수 있게 해주는 여러 메서드를 제공합니다. 

.onDrop(of: [String(kUTTypeURL)], delegate: viewmodel)

 

3). DropDelegate 

 

DropDelegate에서는 performDrop(info: DropInfo) 함수를 통해 드롭을 처리합니다. 

이 함수는 드롭이 발생하면 호출되며, 드롭 정보를 파라미터로 받습니다.

드롭이 성공한다면 true를 반환하도록 합니다.

 

func performDrop(info: DropInfo) -> Bool {
    return true
}

 

 

2. 구현

위 필수 항목을 이용해서 Drag & Drop 을 구현해보았습니다. 

1) 구현 UI

 

2) 구현 코드

struct DragAndDrop: View {
    @StateObject var viewmodel = DragAndDropViewModel()

    var body: some View {
        VStack {
            ScrollView(.horizontal) {
                HStack(spacing: 20) {
                    ForEach(viewmodel.images) { image in
                        Image(image.image)
                            .resizable()
                            .frame(height: 150)
                            .cornerRadius(15)
                            .onDrag {
                                NSItemProvider(item: .some(URL(string: image.image)! as NSSecureCoding), typeIdentifier: String(kUTTypeURL))
                            }
                    }
                }
            }
            .frame(maxHeight: .infinity)
            .background(Color.gray.opacity(0.4))

            Divider()

            ScrollView(.horizontal) {
                HStack {
                    ForEach(viewmodel.droppedImages) { image in
                        if image.image != "" {
                            Image(image.image)
                                .resizable()
                                .frame(height: 150)
                                .cornerRadius(15)
                        }
                    }
                }
                .frame(maxHeight: .infinity)
            }
            .background(Color.blue.opacity(0.4))
            .onDrop(of: [String(kUTTypeURL)], delegate: viewmodel)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

class DragAndDropViewModel: ObservableObject, DropDelegate {
    @Published var images: [DragAndDropImageData] = [
        DragAndDropImageData(image: "img1"),
        DragAndDropImageData(image: "img2"),
        DragAndDropImageData(image: "img3"),
        DragAndDropImageData(image: "img4"),
        DragAndDropImageData(image: "img5"),
        DragAndDropImageData(image: "img6")
    ]

    @Published var droppedImages: [DragAndDropImageData] = []

    func performDrop(info: DropInfo) -> Bool {
        for provider in info.itemProviders(for: [String(kUTTypeURL)]) {
            if provider.canLoadObject(ofClass: URL.self) {
                let _ = provider.loadObject(ofClass: URL.self) { url, error in
                    guard let url = url else { return }
                    let imageUrl = "\(url)"

                    DispatchQueue.main.async {
                        withAnimation(.easeIn) {
                            if !self.droppedImages.contains(where: { $0.image == imageUrl }) {
                                self.droppedImages.append(DragAndDropImageData(image: imageUrl))
                                self.images.removeAll(where: { $0.image == imageUrl })
                            }
                        }
                    }
                }
            }
        }
        return true
    }
}

struct DragAndDropImageData: Identifiable {
    var id: String = UUID().uuidString
    var image: String
}

 

3. 구현 개선

Drag & Drop을 단일 방향이 아닌 양방향으로 할 수 있도록 코드를 수정해보았습니다.

 

1) 구현 UI

 

2) 구현 코드

 

추가된 코드는 "// 추가" 주석 추가
struct DragAndDrop: View {
    @StateObject var viewmodel = DragAndDropViewModel()

    var body: some View {
        VStack {
            ScrollView(.horizontal) {
                HStack(spacing: 20) {
                    ForEach(viewmodel.images) { image in
                        if image.image != "" {
                            Image(image.image)
                                .resizable()
                                .frame(height: 150)
                                .cornerRadius(15)
                                .onDrag {
                                    NSItemProvider(item: .some(URL(string: image.image)! as NSSecureCoding), typeIdentifier: String(kUTTypeURL))
                                }
                        }
                    }
                }
                .frame(maxHeight: .infinity)
            }
            .background(Color.gray.opacity(0.4))
            .onDrop(of: [String(kUTTypeURL)], delegate: viewmodel) // 추가

            Divider()

            ScrollView(.horizontal) {
                HStack {
                    ForEach(viewmodel.droppedImages) { image in
                        if image.image != "" {
                            Image(image.image)
                                .resizable()
                                .frame(height: 150)
                                .cornerRadius(15)
                                .onDrag { // 추가
                                    NSItemProvider(item: .some(URL(string: image.image)! as NSSecureCoding), typeIdentifier: String(kUTTypeURL))
                                }
                        }
                    }
                }
                .frame(maxHeight: .infinity)
            }
            .background(Color.blue.opacity(0.4))
            .onDrop(of: [String(kUTTypeURL)], delegate: viewmodel)
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

class DragAndDropViewModel: ObservableObject, DropDelegate {
    @Published var images: [DragAndDropImageData] = [
        DragAndDropImageData(image: "img1"),
        DragAndDropImageData(image: "img2"),
        DragAndDropImageData(image: "img3"),
        DragAndDropImageData(image: "img4"),
        DragAndDropImageData(image: "img5"),
        DragAndDropImageData(image: "img6")
    ]

    @Published var droppedImages: [DragAndDropImageData] = []

    func performDrop(info: DropInfo) -> Bool {
        for provider in info.itemProviders(for: [String(kUTTypeURL)]) {
            if provider.canLoadObject(ofClass: URL.self) {
                let _ = provider.loadObject(ofClass: URL.self) { url, error in
                    guard let url = url else { return }
                    let imageUrl = "\(url)"

                    DispatchQueue.main.async {
                        withAnimation(.easeIn) {
                            if !self.droppedImages.contains(where: { $0.image == imageUrl }) {
                                self.droppedImages.append(DragAndDropImageData(image: imageUrl))
                                self.images.removeAll(where: { $0.image == imageUrl })
                            } else if !self.images.contains(where: { $0.image == imageUrl }) { // 추가
                                self.images.append(DragAndDropImageData(image: imageUrl))
                                self.droppedImages.removeAll(where: { $0.image == imageUrl })
                            }
                        }
                    }
                }
            }
        }
        return true
    }
}

 

반응형