1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[SwiftUI]ドラッグ&ドロップによる並び替えと値を渡しながらモーダル遷移

Last updated at Posted at 2024-10-23

はじめに

下記を実装しました。

  • Transferableを利用してドラッグアンドドロップで並び替える
  • タップで値を渡しながらモーダル遷移をする

drag&drop.gif

環境

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できるようになる。

Item
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
    アイコンでも、文字でも表示したいものを設定する。
    ちなみにこういうのもトレイリングクロージャ
トレイリングクロージャ

関数の引数の最後がクロージャの時、クロージャを()の外に書くことができる記法

サンプル1
Button(action: { print("タップ") }, label: { Text("カウント") })



Button(action: { print("タップ")}) {
 Text("カウント")
}
サンプル2
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:) の存在になかなか気づけませんでした。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?