LoginSignup
15
14

【SwiftUI】iPhoneのホーム画面のアプリのような並び替えを実装する

Posted at

はじめに

Twitterを見てたら、iPhoneのホーム画面のアプリのような並び替えどうやってやるんだろう的なツイートが流れてきて気になったので実装してみました。

サンプルアプリ

Simulator Screen Recording - iPhone 15 Pro - 2024-02-18 at 22.30.53.gif

データ構造体

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を使用して実装する感じだったらしいです。

参考記事

15
14
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
15
14