在SwiftUI列表中移动项目时不需要的动画

时间:2020-05-03 08:10:41

标签: ios swift swiftui swiftui-list

我有一个SwiftUI列表,如下面的示例代码所示。

struct ContentView: View {
    @State var numbers = ["1", "2", "3"]
    @State var editMode = EditMode.inactive

    var body: some View {
        NavigationView {
            List {
                ForEach(numbers, id: \.self) { number in
                    Text(number)
                }
                .onMove {
                    self.numbers.move(fromOffsets: $0, toOffset: $1)
                }
            }
            .navigationBarItems(trailing: EditButton())
        }
    }
}

当我进入编辑模式并将项目上移一个位置时,放下项目后会发生奇怪的动画(请参见下面的gif)。看起来被拖动的项目返回到其原始位置,然后再次移动到目标位置(带有动画)

strange animation when reordering items

有趣的是,如果将项目拖动到列表的下方或向上拖动多个位置,则不会发生。

我想这是因为即使状态列表中的项目已经通过拖放方式在视图侧重新排序,但当状态中的项目重新排序时,列表也会执行动画。但是显然,除了将项目向上移动一个位置以外,它在所有情况下都能很好地处理它。

关于如何解决此问题的任何想法?也许这是一个已知的错误?

我正在使用XCode 11.4.1,构建目标是iOS 13.4

(还请注意,在“现实世界”应用中,我正在使用Core Data,并且在移动项目时,其顺序在DB中进行了更新,然后在状态中进行了更新,但是动画的问题看起来完全相同。 )

4 个答案:

答案 0 :(得分:3)

这是解决方案(已通过Xcode 11.4 / iOS 13.4测试)

var body: some View {
    NavigationView {
        List {
            ForEach(numbers, id: \.self) { number in
                HStack {
                    Text(number)
                }.id(UUID())        // << here !!
            }
            .onMove {
                self.numbers.move(fromOffsets: $0, toOffset: $1)
            }
        }
        .navigationBarItems(trailing: EditButton())
    }
}

答案 1 :(得分:0)

我有同样的问题。我不知道是否有错误,但是我找到了解决方法。

class Number: ObservableObject, Identifiable {
    var id = UUID()
    @Published var number: String

    init(_ number: String) {
        self.number = number
    }
}

class ObservedNumbers: ObservableObject {
    @Published var numbers = [ Number("1"), Number("2"), Number("3") ]

    func onMove(fromOffsets: IndexSet, toOffset: Int) {
        var newNumbers = numbers.map { $0.number }
        newNumbers.move(fromOffsets: fromOffsets, toOffset: toOffset)

        for (newNumber, number) in zip(newNumbers, numbers) {
            number.number = newNumber
        }

        self.objectWillChange.send()
    }
}

struct ContentView: View {
    @ObservedObject var observedNumbers = ObservedNumbers()
    @State var editMode = EditMode.inactive

    var body: some View {
        NavigationView {
            List {
                ForEach(observedNumbers.numbers) { number in
                    Text(number.number)
                }
                .onMove(perform: observedNumbers.onMove)
            }
            .navigationBarItems(trailing: EditButton())
        }
    }
}

在CoreData的情况下,我只使用NSFetchedResultsControllerObservedNumbers.onMove()方法的实现如下:

        guard var hosts = frc.fetchedObjects else {
            return
        }

        hosts.move(fromOffsets: set, toOffset: to)
        for (order, host) in hosts.enumerated() {
            host.orderPosition = Int32(order)
        }

        try? viewContext.save()

在委托中:

    internal func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any,
                             at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
    {
        switch type {
        case .delete:
            hosts[indexPath!.row].stopTrack()
            hosts.remove(at: indexPath!.row)
        case .insert:
            let hostViewModel = HostViewModel(host: frc.object(at: newIndexPath!))
            hosts.insert(hostViewModel, at: newIndexPath!.row)
            hostViewModel.startTrack()
        case .update:
            hosts[indexPath!.row].update(host: frc.object(at: indexPath!))
        case .move:
            hosts[indexPath!.row].update(host: frc.object(at: indexPath!))
            hosts[newIndexPath!.row].update(host: frc.object(at: newIndexPath!))
        default:
            return
        }
    }

答案 2 :(得分:0)

这是基于Mateusz K在接受的答案中的评论的解决方案。我结合了顺序和数字的哈希。我使用一个复杂的对象代替数字,该对象会动态更新。这样可以确保在基础对象发生更改时刷新列表项。

class HashNumber : Hashable{
        
        var order : Int
        var number : String
        
        init(_ order: Int, _ number:String){
            self.order = order
            self.number = number
        }
        
        static func == (lhs: HashNumber, rhs: HashNumber) -> Bool {
            return lhs.number == rhs.number && lhs.order == rhs.order
        }
        //
        func hash(into hasher: inout Hasher) {
            hasher.combine(number)
            hasher.combine(order)
        }
    }
    
    func createHashList(_ input : [String]) -> [HashNumber]{
        
        var r : [HashNumber] = []
        
        var order = 0
        for i in input{
            let h = HashNumber(order, i)
            r.append(h)
            order += 1
        }
        
        return r
    }
    
    struct ContentView: View {
        @State var numbers = ["1", "2", "3"]
        @State var editMode = EditMode.inactive
        
        var body: some View {
            NavigationView {
                List {
                    ForEach(createHashList(numbers), id: \.self) { number in
                        Text(number.number)
                    }
                    .onMove {
                        self.numbers.move(fromOffsets: $0, toOffset: $1)
                    }
                }
                .navigationBarItems(trailing: EditButton())
            }
        }
    }

答案 3 :(得分:0)

就我而言(再次无法解释原因,但可能可以帮助某人) 我更改了ForEach(self.houses, id: \.id) { house in ... 进入

ForEach(self.houses.indices, id: \.self) { i in
    Text(self.houses[i].name)
        .id(self.houses[i].id)
}