3

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?

3
  • With Xcode 12 / iOS 14 - no error, but does not work either. Commented Jul 20, 2020 at 13:58
  • @Asperi Do you know if I should somehow disable animations just for changing the order of the sections? Or maybe is there another way to do it? Commented Jul 20, 2020 at 14:35
  • No, the problem here is different. Just add init() { print("created") } for ViewModel and perform your scenario - you'll see. With @StateObject in SwiftUI 2.0 it is not better. You've got into very interesting SwiftUI bug. )) Commented Jul 20, 2020 at 15:02

2 Answers 2

2

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)
        }
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

I checked your code and your workaround looks better than mine. However, in iOS 13 it doesn't work - I'm still getting "Attempt to create two animations for cell" error. Thanks again for your help - I'll report a bug to Apple.
0

A possible workaround solution for SwiftUI 1.0

I found a workaround to disable animations for the List by adding .id(UUID()):

var list: some View {
    List {
        ...
    }
    .id(UUID())
}

This, however, messes the transition animations for NavigationLinks created with NavigationLink(destination:tag:selection:): Transition animation gone when presenting a NavigationLink in SwiftUI.

And all other animations (like onDelete) are missing as well.

The even more hacky solution is to disable list animations conditionally:

class ViewModel: ObservableObject {
    ...
    @Published var isReorderingSections = false
    ...
}
struct OrderingView: View {
    @ObservedObject var viewModel: ViewModel

    var body: some View {
        NavigationView {
            ...
        }
        .onAppear {
            self.viewModel.isReorderingSections = true
        }
        .onDisappear {
            self.viewModel.isReorderingSections = false
        }
    }
}
struct ContentView: View {
    ...
    var list: some View {
        List {
            ...
        }
        .id(viewModel.isReorderingSections ? UUID().hashValue : 1)
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.