SwiftUI - Drag & Drop(1)

Marble·2025년 6월 8일

Drag & Drop

목록 보기
1/1

이번 글에서는 swiftUI에서 drag & drop을 구현하는 방법 중에 List의 기본 modifier인 onMove, onDelete에 대해 알아볼게요.

예제 코드부터 살펴보겠습니다.

import SwiftUI

struct SimpleListView: View {
    @State private var items = ["Apple", "Banana", "Cherry", "Date", "Elderberry"]

    var body: some View {
        NavigationView {
            List {
                ForEach(items, id: \.self) { item in
                    Text(item)
                }
                .onMove(perform: moveItem)
                .onDelete(perform: deleteItem)
            }
            .navigationTitle("Fruits")
            .toolbar {
                EditButton() // 드래그 & 삭제를 위한 Edit 모드 버튼
            }
        }
    }

    // 아이템 이동
    private func moveItem(from source: IndexSet, to destination: Int) {
        items.move(fromOffsets: source, toOffset: destination)
    }

    // 아이템 삭제
    private func deleteItem(at offsets: IndexSet) {
        items.remove(atOffsets: offsets)
    }
}


위 이미지는 예제 코드를 실행한 화면인데요 보시다시피 리스트의 아이템을 꾹 눌러서 드래그 & 드랍을 하여 위치를 옮길 수 있고, 옆으로 스와이프 하여 아이템을 삭제할 수 있었습니다.

이는 onMoveonDelete를 이용한 결과인데요. 각각에 대해 알아보겠습니다.

onMove

onMove는 UIKit의 tableView(_:moveRowAt:to:)를 사용하여 구현된 코드입니다. 그래서 인자로 드래그한 아이템의 index, 드롭한 위치의 index를 인자로 전달해줘야 합니다.

작동 흐름 계층도를 보면 다음과 같습니다.

SwiftUI View
└── List
    └── ForEach
        ├── ViewBuilder로 생성된 Cell Views
        └── .onMove(perform:) ← 사용자가 정의한 클로저
            ↓ (내부 연결)
        └── UIHostingController
            └── UITableView (UIKit 기반)
                ├── UITableViewDataSource
                │   └── tableView(_:moveRowAt:to:)
                │       └── SwiftUI가 perform 클로저 호출
                │           └── 사용자가 정의한 move 로직 실행
                └── UITableViewDelegate (선택적으로 연결)

위 코드에서는 함수를 만들어서 이용했지만 함수를 생성하지 않고 다음과 같이 코드를 작성하여 이용할 수도 있습니다.

.onMove { indices, newOffset in
    items.move(fromOffsets: indices, toOffset: newOffset)
}

onDelete

onDelete 또한 onMove처럼 UIKit의 tableView(_:commit:forRowAt:)를 사용하여 구현된 코드이며, tableView의 코드는 다음과 같은 방식으로 구현돼있습니다.

func tableView(_ tableView: UITableView, 
               commit editingStyle: UITableViewCell.EditingStyle, 
               forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        data.remove(at: indexPath.row)
    }
}

onDelete의 작동 흐름 계층도는 다음과 같습니다.

SwiftUI View
└── List
    └── ForEach
        └── .onDelete(perform:) ← 사용자가 정의한 삭제 클로저
            ↓ (내부 연결)
        └── UIHostingController
            └── UITableView (UIKit 기반)
                └── UITableViewDataSource
                    └── tableView(_:commit:forRowAt:)
                        └── if editingStyle == .delete {
                                SwiftUI가 perform 클로저 호출
                                └── 사용자가 정의한 삭제 로직 실행
                            }

onDelete 또한 함수를 생성하지 않고 직접 사용할 수 있으며, 다음 코드처럼 작성하여 사용하면 됩니다.

.onDelete { indexSet in
    items.remove(atOffsets: indexSet)
}

처음 예제 코드에 구현하지 않은 EditButton을 사용했는데 이는 iOS 13.0+부터 제공하는 기본 뷰 입니다.

다음은 이를 사용한 화면입니다.

버튼을 통해 편집모드로 변환되는데 이때 onMove 또는 OnDelete 하나라도 구현되어 있어야합니다. 만약 둘다 구현되지 않았다면 버튼을 눌러도 변화가 없습니다. 버튼을 수정하고 싶다면 다음과 같이 수정할 수 있습니다.

@Environment(\.editMode) private var editMode

Button("편집") {
    if editMode?.wrappedValue == .active {
        editMode?.wrappedValue = .inactive
    } else {
        editMode?.wrappedValue = .active
    }
}

위 메서드들을 사용할 때 주의할 점으로는 사용하는 배열의 타입들은 Identifiable 또는 Hashable 타입이어야하며 ForEach 내부에서 List와 같이 쓸때만 사용 가능합니다. List가 아니어도 컴파일은 되지만 List에 최적화되어 있기 때문에 List를 쓰지 않을 경우에는 별도로 구현을 해줘야 합니다.

profile
개발자가 되고 싶은 공돌이

0개의 댓글