見出し画像

SwiftUI の List を使った並べ替え実装 Tips

くふうAIスタジオで iOS 版の Zaim アプリの開発を担当している ponmiso です。

Zaim では UIKit から SwiftUI への置き換えを進めています。その中で List を使った並べ替えを実装したので Tips としてまとめようと思います。

List を使った並べ替えの基本的な実装

はじめに、List を使った並べ替えの基本的な実装は以下のようになります。

import SwiftUI

struct ContentView: View {
    @State private var fruits: [String] = ["りんご", "みかん", "バナナ"]

    var body: some View {
        List {
            ForEach(fruits, id: \.self) {
                Text($0)
            }
            .onMove { from, to in
                fruits.move(fromOffsets: from, toOffset: to)
            }
        }
        .environment(\.editMode, .constant(.active))
    }
}
List を使用した並べ替えの基本的な実装

上記の実装では、常に並べ替えられます。
ユーザ操作に応じて並べ替えの開始と終了を切り替えるには、EditButton を使用します。
EditButton をタップした時に並べ替えの状態が切り替わります。

import SwiftUI

struct ContentView: View {
    // ・・・

    var body: some View {
        VStack {
            List {
                // ・・・
            }

            EditButton()
        }
    }
}
List と EditButton を使用した並べ替えの基本的な実装

以上が List を使用した並べ替えの基本的な実装でした。
ここからは実装中に直面した課題に対しての Tips を記載します。

Tips1 ユーザ操作で並べ替えを開始・終了する

ユーザ操作で並べ替えの開始や終了は、EditButton を使用することで簡単に実装できます。
ただ、EditButton 以外のViewをタップしたときに並べ替えを開始させたり、任意のタイミングで並べ替えを終了させるためには、環境変数の editMode を切り替える必要があります。

以下の例はTextをタップした時に並べ替えの開始と終了を切り替えています。

import SwiftUI

struct ContentView: View {
    @State private var editMode: EditMode = .inactive
    @State private var fruits: [String] = ["りんご", "みかん", "バナナ"]

    var body: some View {
        VStack {
            List {
                // ・・・
            }
            .environment(\.editMode, $editMode)

            Text(editMode.isEditing ? "終了" : "編集")
                .onTapGesture {
                    if editMode.isEditing {
                        editMode = .inactive
                    } else {
                        editMode = .active
                    }
                }
        }
    }
}
ユーザ操作で並べ替えを開始・終了する例

Tips2 List のセルに下線を引く

List はデフォルトでセルに下線が引いてあります。この下線の色や太さなどをカスタマイズするには独自で実装する必要があります。

まずは、セル内の View として実装してみます。

import SwiftUI

struct ContentView: View {
    @State private var fruits: [String] = ["りんご", "みかん", "バナナ"]

    var body: some View {
        VStack {
            List {
                ForEach(fruits, id: \.self) { name in
                    ZStack(alignment: .bottom) {
                        Text(name)
                            .frame(height: 44)
                            .frame(maxWidth: .infinity, alignment: .leading)
                        if name != fruits.last {
                            Divider()
                                .frame(height: 2)
                                .background(.blue)
                        }
                    }
                    .listRowSeparator(.hidden) // デフォルトの下線を消す
                    .listRowInsets(.init(top: 0, leading: 16, bottom: 0, trailing: 16)) // 標準のデザインに寄せるための実装
                }
                .onMove { from, to in
                    fruits.move(fromOffsets: from, toOffset: to)
                }
            }

            EditButton()
        }
    }
}
並び替え前
並び替え中

上記の実装で下線をカスタマイズできましたが、並べ替え時に下線が3本線アイコンまで伸びておらず、見た目が気になります。
下線を3本線アイコンまで伸ばすためには、listRowBackground を使用し下線をセルの背景として表示します。

import SwiftUI

struct ContentView: View {
    @State private var fruits: [String] = ["りんご", "みかん", "バナナ"]

    var body: some View {
        VStack {
            List {
                ForEach(fruits, id: \.self) { name in
                    ZStack(alignment: .bottom) {
                        Text(name)
                            .frame(height: 44)
                            .frame(maxWidth: .infinity, alignment: .leading)
                    }
                    .listRowSeparator(.hidden) // デフォルトの下線を消す
                    .listRowInsets(.init(top: 0, leading: 16, bottom: 0, trailing: 16)) // 標準のデザインに寄せる実装
                    .listRowBackground(listRowBackground(shouldShowDivider: name != fruits.last))
                }
                .onMove { from, to in
                    fruits.move(fromOffsets: from, toOffset: to)
                }
            }

            EditButton()
        }
    }

    private func listRowBackground(shouldShowDivider: Bool) -> some View {
        ZStack(alignment: .bottom) {
            Color.white
            if shouldShowDivider {
                Divider()
                    .frame(height: 2)
                    .background(.blue)
                    .padding(.horizontal, 16)
            }
        }
    }
}
List のセル背景に下線を引く例

Tips3 通常時で並べ替えられないようにする

List の onMove を実装すると、editMode.isEditing が false の場合でも並べ替えられてしまいます。
並べ替えられないようにするためには、moveDisabled にtrueを設定します。

ただ、moveDisabled を実装するだけだと、並べ替えを開始した時にセルに3本線のアイコンが表示されません。
そこで、ideditMode を設定し、並べ替えの状態が切り替わった時に View がリロードされるようにしています。

import SwiftUI

struct ContentView: View {
    @Environment(\.editMode) var editMode
    @State private var fruits: [String] = ["りんご", "みかん", "バナナ"]

    var body: some View {
        VStack {
            List {
                ForEach(fruits, id: \.self) {
                    Text($0)
                }
                .onMove { from, to in
                    fruits.move(fromOffsets: from, toOffset: to)
                }
                .moveDisabled(editMode?.wrappedValue.isEditing == false)
            }
            .id(editMode?.wrappedValue ?? .inactive)

            EditButton()
        }
    }
}

おわりに

今回、SwiftUI の List を使った並べ替え実装の Tips をご紹介しました。
誰もがつまずくポイントが詰まっていると思うので、ぜひ参考にしてみてください。

くふうAIスタジオでは、採用活動を行っています。

当社は「AX で 暮らしに ひらめきを」をビジョンに、2023年7月に設立されました。
(AX=AI eXperience(UI/UX における AI/AX)とAI Transformation(DX におけるAX)の意味を持つ当社が唱えた造語)
くふうカンパニーグループのサービスの企画開発運用を主な事業とし、非エンジニアさえも当たり前にAIを使いこなせるよう、積極的なAI利活用を推進しています。
(サービスの一例:累計DL数1,000万以上の家計簿アプリ「Zaim」、月間利用者数1,600万人のチラシアプリ「トクバイ」等)
AXを活用した未来を一緒に作っていく仲間を募集中です。
ご興味がございましたら、以下からカジュアル面談のお申込みやご応募等お気軽にお問合せください。

参考