はじめに
Twitterを見てたら、iPhoneのホーム画面のアプリのような並び替えどうやってやるんだろう的なツイートが流れてきて気になったので実装してみました。
サンプルアプリ
データ構造体
Colorを使いたかったのですが、Codableに準拠してなくて、ちょっとめんどそうだったので、RGBそれぞれをDoubleで持っています。
struct AppIcon: Codable, Identifiable {
var id = UUID()
let red: Double
let green: Double
let blue: Double
}
extension AppIcon: Transferable {
static var transferRepresentation: some TransferRepresentation {
CodableRepresentation(for: AppIcon.self, contentType: .application)
}
}
実装
import SwiftUI
struct ContentView: View {
/// 現在ドラッグしているアプリアイコン
@State private var draggingAppIcon: AppIcon?
/// アプリアイコン一覧
@State private var appIcons: [AppIcon] = [
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
.init(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)),
]
/// アプリアイコンの丸角サイズ
private let appCornerRadius = 10.0
/// ホーム画面のグリッドカラム
private let columns = Array(repeating: GridItem(.flexible(), spacing: 10), count: 4)
var body: some View {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(appIcons) { appIcon in
appIconView(color: .init(red: appIcon.red, green: appIcon.green, blue: appIcon.blue))
.draggable(appIcon) {
appIconView(color: .init(red: appIcon.red, green: appIcon.green, blue: appIcon.blue))
.contentShape(.dragPreview, .rect(cornerRadius: appCornerRadius))
.onAppear {
draggingAppIcon = appIcon
}
}
.dropDestination(for: AppIcon.self) { _, _ in
true
} isTargeted: { isTargeted in
// ターゲットがない時は何もしない
guard isTargeted else { return }
// ドラッグしてない時は何もしない
guard let draggingAppIcon else { return }
// ドラッグ中のIDとターゲットのIDが一緒であれば何もしない
guard draggingAppIcon.id != appIcon.id else { return }
// ドラッグ中のインデックス
guard let draggingAppIconIndex = appIcons.firstIndex(where: { $0.id == draggingAppIcon.id }) else {
return
}
// ターゲットのインデックス
guard let targetedAppIconIndex = appIcons.firstIndex(where: { $0.id == appIcon.id }) else {
return
}
withAnimation {
let sourceItem = appIcons.remove(at: draggingAppIconIndex)
appIcons.insert(sourceItem, at: targetedAppIconIndex)
}
}
}
}
.padding(.horizontal, 20)
}
private func appIconView(color: Color) -> some View {
RoundedRectangle(cornerRadius: appCornerRadius)
.frame(width: 70, height: 70)
.foregroundStyle(color)
.shadow(radius: 1)
}
}
おわり
iOS16からドラッグ&ドロップの実装方法がかなり変わったようです。
それまではDropDelegateを使用して実装する感じだったらしいです。
参考記事