IT工房くまや

通称くまが運営するIT工房サイト

SwiftData @Modelを使ってList項目の追加・移動・削除を実装してみました

SwiftDataの@Modelを使って、Listの項目を移動させる処理を作ってみました。@Modelはクラスに適用することで、アプリを終了させてもプロパティの値を保持させることができます。ただしアプリを削除したらプロパティの値は消滅します。

まず、@Modelを適用させるクラスです。

import Foundation
import SwiftData

@Model
final class Item: Identifiable {
    var name: String
    //チェックボックス表示切り替え用
    var check: Bool
    //移動処理時にソートをかける対象
    var sortNumber: Int
    //移動処理をかけるときにIdentifiableプロトコルを準拠させるのに必要
    var id = UUID()
        
    init(name: String, sortNumber: Int) {
        self.name = name
        self.check = false
        self.sortNumber = sortNumber
    }
}

Itemの配列をリストとして表示し項目の追加、移動、削除の処理を可能にしたViewがこちらです。

import SwiftUI
import SwiftData

struct ItemListView: View {
    //@Model class Item を modelContextとしてしようできるようにします
    //これでmodelContextの insert delete が利用できます
    @Environment(\.modelContext) var modelContext
    //Listのエディットモードを切り替える変数 editModeを設定
    @Environment(\.editMode) var editMode
    //ItemのsortNumberプロパティを並び替えの対象としてItemの配列をnumbersとして設定
    @Query(sort: \Item.sortNumber) private var numbers: [Item]
    @State var shoppingItem: String = ""
    @State var modeName: String = "追加削除モード開始"

    var body: some View {
        VStack {
            if editMode?.wrappedValue.isEditing == true {
                HStack{
                    //買い物リストのアプリにしているので↓
                    TextField("ここに買い物するものを入力してください", text: $shoppingItem)
                        .font(.system(size: 30))
                        .background(Color.yellow)
                    Button{
                        let i = numbers.count
                        modelContext.insert(Item(name: shoppingItem, sortNumber: i))
                        shoppingItem = ""
                    } label: {
                        Text("追加")
                            .font(.system(size: 24))
                    }
                }
                .padding()
            }
            //[Item]のnumbersをListで表示
            List {
                ForEach(numbers) { number in
                    HStack {
                        //チェックボックスの表示を切り替えると表示文字の大きさと色を変更
                        if number.check == false {
                            Text(number.name)
                                .font(.system(size: 24, weight: .bold, design: .rounded))
                                .foregroundColor(.black)
                        } else {
                            Text(number.name)
                                .font(.system(size: 18))
                                .foregroundColor(.gray)
                        }
                        Spacer()
                        //チェックボックスの表示を切り替え
                        if number.check == false {
                            Image(systemName: "square")
                                .scaledToFill()
                                .onTapGesture {
                                    number.check = true
                                }
                        } else {
                            Image(systemName: "checkmark.square")
                                .scaledToFill()
                                .onTapGesture {
                                    number.check = false
                                }
                        }
                    }
                }
                //移動時の処理
                .onMove{ fromOffSet, newOffset in
                    var fromIndex: Int = 0
                    
                    //fromOffSetの配列オブジェクトから並び替え用の数値を取得
                    //これが移動対象の並び替え用の数値
                    for itm in fromOffSet {
                        let obj = numbers[itm]
                        fromIndex = obj.sortNumber
                    }
                    
                    var toIndex = 0
                    
                    if newOffset < numbers.count - 1 {
                        toIndex = numbers[newOffset].sortNumber
                    } else {
                        toIndex = numbers[numbers.count - 1].sortNumber + 1
                    }
                    
                    //移動対象と移動先の関係で条件付けし、移動後の並び替え用数値を変化させます
                    //これにより移動処理後に項目の並び替えが行えます
                    if fromIndex > toIndex {
                        numbers[fromIndex].sortNumber = toIndex
                        var toi = 0
                        if toIndex > 1 {
                            toi = toIndex
                        }
                        for i in toi..<fromIndex {
                            numbers[i].sortNumber += 1
                        }
                    } else if fromIndex < toIndex {
                        numbers[fromIndex].sortNumber = toIndex
                        let fromi = fromIndex
                        for i in fromi..<toIndex {
                            numbers[i].sortNumber -= 1
                        }
                    }
                }
                //削除時の処理
                .onDelete{ indexSet in
                    //
                    for item in indexSet {
                        let obj = numbers[item]
                        modelContext.delete(obj)
                        //削除処理後並び替え用の数値を変化させます
                        //これにより移動処理後に項目の並び替えが行えます
                        //項目追加時に配列の個数を元に並び替え用数値を設定しているのでここで全体の数値を整理しています
                        if obj.sortNumber < numbers.count - 1 {
                            for i in obj.sortNumber..<numbers.count {
                                numbers[i].sortNumber -= 1
                            }
                        }
                    }
                }
                .listRowBackground(Color.yellow)
            }
            .scrollContentBackground(.hidden)
            .background(.clear)

            Button {

                if editMode?.wrappedValue.isEditing == true {
                    editMode?.wrappedValue = .inactive
                    modeName = "追加削除モード開始"
                } else {
                    editMode?.wrappedValue = .active
                    modeName = "追加削除モード終わり"
                }

            }label: {
                Text(modeName)
                    .padding()
                    .font(.system(size: 24))
                    .foregroundColor(.black)
                    .background(Color.green)
                    .cornerRadius(10)
            }
        }
        .padding()
    }
}