Search code examples
swiftuiswiftui-list

Reorder list dynamic sections from another view


I have a simple List with sections that are stored inside an ObservableObject. I'd like to reorder them from another view.

This is my code:

class ViewModel: ObservableObject {
    @Published var sections = ["S1", "S2", "S3", "S4"]
    
    func move(from source: IndexSet, to destination: Int) {
        sections.move(fromOffsets: source, toOffset: destination)
    }
}
struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()
    @State var showOrderingView = false

    var body: some View {
        VStack {
            Button("Reorder sections") {
                self.showOrderingView = true
            }
            list
        }
        .sheet(isPresented: $showOrderingView) {
            OrderingView(viewModel: self.viewModel)
        }
    }

    var list: some View {
        List {
            ForEach(viewModel.sections, id: \.self) { section in
                Section(header: Text(section)) {
                    ForEach(0 ..< 3, id: \.self) { _ in
                        Text("Item")
                    }
                }
            }
        }
    }
}
struct OrderingView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        NavigationView {
            List {
                ForEach(viewModel.sections, id: \.self) { section in
                    Text(section)
                }
                .onMove(perform: viewModel.move)
            }
            .navigationBarItems(trailing: EditButton())
        }
    }
}

But in the OrderingView when trying to move sections I'm getting this error: "Attempt to create two animations for cell". Likely it's because the order of the sections has changed.

How can I change the order of the sections?


Solution

  • The problem of this scenario is recreated many times ViewModel, so modifications made in sheet just lost. (The strange thing is that in SwiftUI 2.0 with StateObject these changes also lost and EditButton does not work at all.)

    Anyway. It looks like here is a found workaround. The idea is to break interview dependency (binding) and work with pure data passing them explicitly into sheet and return them back explicitly from it.

    Tested & worked with Xcode 12 / iOS 14, but I tried to avoid using SwiftUI 2.0 features.

    class ViewModel: ObservableObject {
        @Published var sections = ["S1", "S2", "S3", "S4"]
    
        func move(from source: IndexSet, to destination: Int) {
            sections.move(fromOffsets: source, toOffset: destination)
        }
    }
    
    struct ContentView: View {
        @ObservedObject var viewModel = ViewModel()
        @State var showOrderingView = false
    
        var body: some View {
            VStack {
                Button("Reorder sections") {
                    self.showOrderingView = true
                }
    
                list
            }
            .sheet(isPresented: $showOrderingView) {
                OrderingView(sections: viewModel.sections) {
                    self.viewModel.sections = $0
                }
            }
        }
    
        var list: some View {
            List {
                ForEach(viewModel.sections, id: \.self) { section in
                    Section(header: Text(section)) {
                        ForEach(0 ..< 3, id: \.self) { _ in
                            Text("Item")
                        }
                    }
                }
            }
        }
    }
    
    struct OrderingView: View {
        @State private var sections: [String]
        let callback: ([String]) -> ()
    
        init(sections: [String], callback: @escaping ([String]) -> ())
        {
            self._sections = State(initialValue: sections)
            self.callback = callback
        }
    
        var body: some View {
            NavigationView {
                List {
                    ForEach(sections, id: \.self) { section in
                        Text(section)
                    }
                    .onMove {
                        self.sections.move(fromOffsets: $0, toOffset: $1)
                    }
                }
                .navigationBarItems(trailing: EditButton())
            }
            .onDisappear {
                self.callback(self.sections)
            }
        }
    }