1

I'm building a reorderable List in SwiftUI. It has been straightforward for the most part (using the onMove closure to update the datasource). However, I'm running into issues understanding how to detect when a user starts and ends moving an item. I want to be able to change the background color on all rows that are not currently selected.

Quick and dirty working example below:

import SwiftUI

struct TestItem: Equatable, Hashable {
    let id = UUID()
    var title: String
    var logo: String? = nil
}

final class ViewModel: ObservableObject {
    
    @Published var datasource: [TestItem] = [
        .init(title: "One"),
        .init(title: "Two"),
        .init(title: "Three"),
        .init(title: "Four"),
        .init(title: "Five"),
    ]
    
    func move(from: IndexSet, to: Int) {
        datasource.move(fromOffsets: from, toOffset: to)
    }
}

@available(iOS 15.0, *)
struct TestView: View {
    @ObservedObject var viewModel: ViewModel
    
    init() {
        viewModel = ViewModel()
    }
    
    var body: some View {
        VStack(spacing: 0) {
            Spacer()
                .frame(height: 24)
            
            List {
                ForEach(viewModel.datasource, id: \.title) { item in
                    TestViewItem(title: item.title)
                }
                .onMove { from, to in
                    viewModel.move(from: from, to: to)
                }
                .listRowBackground(Color.green)
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .listRowSeparator(.hidden)
            .environment(\.editMode, .constant(.active))
            .listStyle(.plain)
        }
        .background(Color.gray.ignoresSafeArea())
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct TestViewItem: View {
    let title: String
    
    var body: some View {
        HStack(spacing: 0) {
            Text(title)
        }
    }
}

Is it possible to change the non-selected row background colors only while moving another row?

Andrew
  • 815
  • 2
  • 13
  • 33

1 Answers1

0

You could try this approach, using .onDrag and a conditional .listRowBackground(...), together with some helper @State var, to change the background color of all list items that are not currently selected.

struct ContentView: View {
    var body: some View {
        TestView()
    }
}

struct TestItem: Identifiable, Equatable, Hashable {
    let id = UUID()
    var title: String
    var logo: String? = nil
}

final class ViewModel: ObservableObject {
    
    @Published var datasource: [TestItem] = [
        .init(title: "One"),
        .init(title: "Two"),
        .init(title: "Three"),
        .init(title: "Four"),
        .init(title: "Five")
    ]
    
    func move(from: IndexSet, to: Int) {
        datasource.move(fromOffsets: from, toOffset: to)
    }
}

@available(iOS 15.0, *)
struct TestView: View {
    @StateObject var viewModel: ViewModel = ViewModel()
    @State var selected = UUID()
    @State var isMoving = false
    
    var body: some View {
        VStack(spacing: 0) {
            Spacer().frame(height: 24)
            List {
                ForEach(viewModel.datasource) { item in
                    TestViewItem(title: item.title)
                        .onDrag {
                            isMoving = true
                            selected = item.id
                            return NSItemProvider()
                        }
                        .listRowBackground(
                            selected == item.id
                            ? Color.green
                            : isMoving ? Color.orange : Color.green)
                }
                .onMove { from, to in
                    isMoving = false
                    DispatchQueue.main.async {
                        viewModel.move(from: from, to: to)
                    }
                }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .listRowSeparator(.hidden)
            .environment(\.editMode, .constant(.active))
            .listStyle(.plain)
        }
        .background(Color.gray.ignoresSafeArea())
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

struct TestViewItem: View {
    let title: String
    
    var body: some View {
        HStack(spacing: 0) {
            Text(title)
        }
    }
}
  • This is closer, there are still a few issues though: The onDrag closure is not reliably triggered. If you light press and drag, it does not fire. If you press and hold, then drag it is. onMove is only triggered if the index changes after drag (long pressing and releasing keeps the isMoving state equal to true). – Andrew May 18 '23 at 16:24