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
}
}
'Dev-iOS > UI' 카테고리의 다른 글
[UI] Custom Drawer (Side Menu)- (SwiftUI 2.0 - iOS 14 이상) (0) | 2023.07.21 |
---|---|
[UI] 스크롤 시 상단 탑 SearchBar 이동 시키기 - (SwiftUI 2.0 - iOS 14 이상) (0) | 2023.07.20 |
[UI] pull to refresh - (SwiftUI 2.0 - iOS 14 이하) (0) | 2023.07.12 |
[UI] 상단 탭 화면 with Animation (SwiftUI 2.0 - iOS 14 이상) (0) | 2023.07.11 |
[UI] Carousel (SwiftUI 2.0 - iOS 14 이상) (0) | 2023.07.11 |