Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for updating a CoreData attribute named "sortIndex" in case an (optional) argument 'context' is passed (preferably the viewContext) #3

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
246 changes: 135 additions & 111 deletions Sources/SwiftUIReorderableForEach/SwiftUIReorderableForEach.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,138 +3,162 @@ import UniformTypeIdentifiers

public struct ReorderableForEach<Data, Content>: 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<Bool>,
@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<Data>: 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<Bool>, 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<Data>: 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()
}
}