diff --git a/README.md b/README.md index 60566b2..e7315ab 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ This package contains a generic **ReoderableForEach** component, which can then * Works with any SwiftUI layout. * Binding to dynamically enable/disable reordering functionality. * Custom item rendering with additional info on if the current item is being dragged or not. +* Support for updating a CoreData attribute named "sortIndex" in case an (optional) argument 'context' is passed (preferably the viewContext) ## Installation diff --git a/Sources/SwiftUIReorderableForEach/SwiftUIReorderableForEach.swift b/Sources/SwiftUIReorderableForEach/SwiftUIReorderableForEach.swift index b1a920d..6b5f977 100644 --- a/Sources/SwiftUIReorderableForEach/SwiftUIReorderableForEach.swift +++ b/Sources/SwiftUIReorderableForEach/SwiftUIReorderableForEach.swift @@ -3,138 +3,162 @@ import UniformTypeIdentifiers public struct ReorderableForEach: View where Data : Hashable, Content : View { - @Binding var data: [Data] - @Binding var allowReordering: Bool - private let content: (Data, Bool) -> Content - - @State private var draggedItem: Data? - @State private var hasChangedLocation: Bool = false - - public init(_ data: Binding<[Data]>, - allowReordering: Binding, - @ViewBuilder content: @escaping (Data, Bool) -> Content) { - _data = data - _allowReordering = allowReordering - self.content = content - } - - public var body: some View { - ForEach(data, id: \.self) { item in - if allowReordering { - content(item, hasChangedLocation && draggedItem == item) - .onDrag { - draggedItem = item - return NSItemProvider(object: "\(item.hashValue)" as NSString) - } - .onDrop(of: [UTType.plainText], delegate: DragRelocateDelegate( - item: item, - data: $data, - draggedItem: $draggedItem, - hasChangedLocation: $hasChangedLocation)) - } else { - content(item, false) - } - } - } - - struct DragRelocateDelegate: DropDelegate - where Data : Equatable { - let item: Data @Binding var data: [Data] - @Binding var draggedItem: Data? - @Binding var hasChangedLocation: Bool + @Binding var allowReordering: Bool - func dropEntered(info: DropInfo) { - guard item != draggedItem, - let current = draggedItem, - let from = data.firstIndex(of: current), - let to = data.firstIndex(of: item) - else { - return - } - - hasChangedLocation = true - - if data[to] != current { - withAnimation { - data.move(fromOffsets: IndexSet(integer: from), - toOffset: (to > from) ? to + 1 : to) - } - } + let context: NSManagedObjectContext? // optional, passing context enables CoreData support, requires an attribute 'sortIndex' in CD model + + private let content: (Data, Bool) -> Content + + @State private var draggedItem: Data? + @State private var hasChangedLocation: Bool = false + + public init (_ data: Binding<[Data]>, allowReordering: Binding, context: NSManagedObjectContext? = nil, @ViewBuilder content: @escaping (Data, Bool) -> Content) { + _data = data + _allowReordering = allowReordering + + self.context = context + self.content = content } - func dropUpdated(info: DropInfo) -> DropProposal? { - DropProposal(operation: .move) + public var body: some View { + ForEach(data, id: \.self) { item in + if allowReordering { + content(item, hasChangedLocation && draggedItem == item) + .onDrag { + draggedItem = item + return NSItemProvider(object: "\(item.hashValue)" as NSString) + } + .onDrop(of: [UTType.plainText], delegate: DragRelocateDelegate( + item: item, + context: context, + data: $data, + draggedItem: $draggedItem, + hasChangedLocation: $hasChangedLocation)) + } else { + content(item, false) + } + } } - func performDrop(info: DropInfo) -> Bool { - hasChangedLocation = false - draggedItem = nil - return true + struct DragRelocateDelegate: DropDelegate + where Data : Equatable { + let item: Data + let context: NSManagedObjectContext? + + @Binding var data: [Data] + @Binding var draggedItem: Data? + @Binding var hasChangedLocation: Bool + + func dropEntered(info: DropInfo) { + guard item != draggedItem, + let current = draggedItem, + let from = data.firstIndex(of: current), + let to = data.firstIndex(of: item) + else { + return + } + + hasChangedLocation = true + + if data[to] != current { + // handle UI indices + withAnimation { + data.move(fromOffsets: IndexSet(integer: from), + toOffset: (to > from) ? to + 1 : to) + } + + // support for CoreData (rewrite all sortIndex' in order of UI appearance) + if let context = context, let data = data as? [NSManagedObject] { + var index = 0 + + for preset in data { + preset.setValue(index, forKey: "sortIndex") + index += 1 + } + + if context.hasChanges { + do { try context.save() } + catch { } + } + } + } + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + hasChangedLocation = false + draggedItem = nil + return true + } } - } } +// MARK: tests + struct ReorderingVStackTest: View { - @State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"] - @State private var allowReordering = false - - var body: some View { - VStack { - Toggle("Allow reordering", isOn: $allowReordering) - .frame(width: 200) - .padding(.bottom, 30) - VStack { - ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in - Text(item) - .font(.title) - .padding() - .frame(minWidth: 200, minHeight: 50) - .border(Color.blue) - .background(Color.red.opacity(0.9)) - .overlay(isDragged ? Color.white.opacity(0.6) : Color.clear) + @State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"] + @State private var allowReordering = false + + var body: some View { + VStack { + Toggle("Allow reordering", isOn: $allowReordering) + .frame(width: 200) + .padding(.bottom, 30) + VStack { + ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in + Text(item) + .font(.title) + .padding() + .frame(minWidth: 200, minHeight: 50) + .border(Color.blue) + .background(Color.red.opacity(0.9)) + .overlay(isDragged ? Color.white.opacity(0.6) : Color.clear) + } + } } - } } - } } struct ReorderingVGridTest: View { - @State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"] - @State private var allowReordering = false - - var body: some View { - VStack { - Toggle("Allow reordering", isOn: $allowReordering) - .frame(width: 200) - .padding(.bottom, 30) - LazyVGrid(columns: [ - GridItem(.flexible()), - GridItem(.flexible()) - ]) { - ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in - Text(item) - .font(.title) - .padding() - .frame(minWidth: 150, minHeight: 50) - .border(Color.blue) - .background(Color.red.opacity(0.9)) - .overlay(isDragged ? Color.white.opacity(0.6) : Color.clear) + @State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"] + @State private var allowReordering = false + + var body: some View { + VStack { + Toggle("Allow reordering", isOn: $allowReordering) + .frame(width: 200) + .padding(.bottom, 30) + LazyVGrid(columns: [ + GridItem(.flexible()), + GridItem(.flexible()) + ]) { + ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in + Text(item) + .font(.title) + .padding() + .frame(minWidth: 150, minHeight: 50) + .border(Color.blue) + .background(Color.red.opacity(0.9)) + .overlay(isDragged ? Color.white.opacity(0.6) : Color.clear) + } + } } - } + .padding() } - .padding() - } } struct ReorderingVStackTest_Previews: PreviewProvider { - static var previews: some View { - ReorderingVStackTest() - } + static var previews: some View { + ReorderingVStackTest() + } } struct ReorderingGridTest_Previews: PreviewProvider { - static var previews: some View { - ReorderingVGridTest() - } + static var previews: some View { + ReorderingVGridTest() + } }