はじめに
下記を実装しました。
- Transferableを利用してドラッグアンドドロップで並び替える
- タップで値を渡しながらモーダル遷移をする
環境
Xcode16.0
iOS18
ドラッグ&ドロップで配置入れ替え
Transferable
draggable(_:preview:)
dropDestination(for:action:)
import SwiftUI
struct ContentView: View {
@State private var sampleItemList: [Item] = [
.init(id: 1, title: "あ"),
.init(id: 2, title: "い"),
.init(id: 3, title: "う"),
.init(id: 4, title: "え"),
.init(id: 5, title: "お"),
.init(id: 6, title: "か"),
.init(id: 7, title: "き"),
.init(id: 8, title: "く"),
.init(id: 9, title: "け"),
]
@State private var draggingItem: Item?
@State private var selectedItem: Item? = nil
var body: some View {
NavigationStack {
GeometryReader{ geometry in
let itemSize = max((geometry.size.width - 40) / 3, 0)
let columns = Array(repeating: GridItem(spacing: 20), count: 3)
LazyVGrid(columns: columns, spacing: 20, content: {
ForEach(sampleItemList, id: \.self.id) { sampleItem in
RoundedRectangle(cornerRadius: 10)
.stroke(Color.orange, lineWidth: 6)
.background(RoundedRectangle(cornerRadius: 10).fill(Color.orange.opacity(0.1)))
.frame(width: itemSize, height: itemSize)
.overlay(
HStack{
Text("\(sampleItem.id)")
.font(.system(size: itemSize / 2.5))
.fontWeight(.bold)
.foregroundColor(.black)
Text(sampleItem.title)
.font(.system(size: itemSize / 2.5))
.fontWeight(.bold)
.foregroundColor(.black)
}
.padding()
)
.onTapGesture {
selectedItem = sampleItem
}
.draggable(sampleItem) {
RoundedRectangle(cornerRadius: 10)
.fill(.ultraThinMaterial)
.frame(width: itemSize, height: itemSize)
.onAppear{
draggingItem = sampleItem
}
}
.dropDestination(for: Item.self) { items, location in
draggingItem = nil
return false
} isTargeted: { status in
if let draggingItem, status, draggingItem != sampleItem {
if let draggingIndex = sampleItemList.firstIndex(of:draggingItem),
let destinationIndex = sampleItemList.firstIndex(of: sampleItem){
withAnimation(.bouncy) {
let dropItem = sampleItemList.remove(at: draggingIndex)
sampleItemList.insert(dropItem, at:destinationIndex)
}
}
}
}
}
})
}
.padding(20)
.navigationTitle("ドラッグ&ドロップ")
.sheet(item: $selectedItem) { selectedItem in
Modal(selectedId: selectedItem.id)
}
}
}
}
Transferable
これに準拠させた要素がdrag and dropできるようになる。
import SwiftUI
struct Item: Codable, Identifiable, Equatable {
let id: Int
let title: String
}
extension Item: Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: Item.self, contentType: .data)
}
}
draggable(_:preview:)
- 第1引数:ドラッグしているデータ
Transferableに準拠した型でないとダメ。 - 第2引数preview:ドラッグ中に表示するview
アイコンでも、文字でも表示したいものを設定する。
ちなみにこういうのもトレイリングクロージャ
トレイリングクロージャ
関数の引数の最後がクロージャの時、クロージャを()の外に書くことができる記法
Button(action: { print("タップ") }, label: { Text("カウント") })
↓
Button(action: { print("タップ")}) {
Text("カウント")
}
sample(parameter: 1, handler: { string in
print(string)
})
↓
sample(parameter: 1) { string in
print(string)
}
.draggable(sampleItem) {
RoundedRectangle(cornerRadius: 10)
.fill(.ultraThinMaterial)
.frame(width: itemSize, height: itemSize)
.onAppear{
draggingItem = sampleItem
}
}
プレビューが現れた時に、ドラッグしている要素をststeに保持する
dropDestination(for:action:isTargetted)
- 第1引数for:受け取るデータの型
Transferableに準拠した型でないとダメ。 - 第2引数action:ドロップされたデータを処理する
- 第3引数isTargetted:ドロップ領域にアイテムがドラッグされているかどうかをbooleanで返してくれる
こちらの記事ではドラッグ可能になった場合に赤枠を表示している
.dropDestination(for: Item.self) { items, location in
draggingItem = nil
return false
} isTargeted: { status in
if let draggingItem, status, draggingItem != sampleItem {
if let draggingIndex = sampleItemList.firstIndex(of:draggingItem),
let destinationIndex = sampleItemList.firstIndex(of: sampleItem){
withAnimation(.bouncy) {
let dropItem = sampleItemList.remove(at: draggingIndex)
sampleItemList.insert(dropItem, at:destinationIndex)
}
}
}
}
ドロップ時に、ドロップ箇所の要素を削除して、ドラッグしてた要素を削除した位置に挿入する。
if let draggingItem, status, draggingItem != sampleItem
この条件を間違えるとドロップ時の挙動がおかしくなるので注意。
値を渡しながらモーダル遷移
import SwiftUI
struct ContentView: View {
@State private var selectedItem: Item? = nil
...
var body: some View {
NavigationStack {
GeometryReader {
...
.onTapGesture {
selectedItem = sampleItem
}
...
}
.padding(20)
.navigationTitle("ドラッグ&ドロップ")
.sheet(item: $selectedItem) { selectedItem in
AnswerModal(selectedId: selectedItem.id)
}
}
}
}
struct AnswerModal: View {
@Environment(\.dismiss) private var dismiss
var selectedId: Int
var body: some View {
VStack {
Text("選択したID: \(selectedId)")
.font(.title)
.padding()
Button("閉じる") {
dismiss()
}
}
.navigationTitle("モーダル")
.navigationBarTitleDisplayMode(.inline)
}
}
モーダル遷移に使用する.sheetは2種類あります。
-
sheet(isPresented:onDismiss:content:)
isPresented: boolean
で表示非表示
モーダル閉じると自動的にfalseになる -
sheet(item:onDismiss:content:) ← 値を渡したい場合はこっち
item
がnilじゃなくなるとモーダル表示、nilで非表示
モーダル閉じると自動的にnilになる
itemはIdentifiable
に準拠していないといけない
最初はsheet(isPresented:onDismiss:content:)
を使ってましたが、1回目の遷移では値がnilになってしまいます。2回目以降は値が渡りました。
けっこう苦しんでからsheet(item:onDismiss:content:)
の存在を知って解決しました。
これはitemがnilでなくなったら、モーダル表示するのでうまく値が渡りました。
こちらの記事で今回発生した事象と解決方法を説明してくれていました。
おわりに
sheet
について検索するとsheet(isPresented:onDismiss:content:)
を使用した実装例のほうが多くて、sheet(item:onDismiss:content:)
の存在になかなか気づけませんでした。