├── .DS_Store
├── Images
├── previewGrid.gif
└── previewStack.gif
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Package.swift
├── SwiftUIReorderableForEach.podspec
├── LICENSE
├── .gitignore
├── README.md
└── Sources
└── SwiftUIReorderableForEach
└── SwiftUIReorderableForEach.swift
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globulus/swiftui-reorderable-foreach/HEAD/.DS_Store
--------------------------------------------------------------------------------
/Images/previewGrid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globulus/swiftui-reorderable-foreach/HEAD/Images/previewGrid.gif
--------------------------------------------------------------------------------
/Images/previewStack.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/globulus/swiftui-reorderable-foreach/HEAD/Images/previewStack.gif
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SwiftUIReorderableForEach",
8 | platforms: [
9 | .iOS(.v15), .macOS(.v12)
10 | ],
11 | products: [
12 | .library(
13 | name: "SwiftUIReorderableForEach",
14 | targets: ["SwiftUIReorderableForEach"]),
15 | ],
16 | dependencies: [
17 | ],
18 | targets: [
19 | .target(
20 | name: "SwiftUIReorderableForEach",
21 | dependencies: []),
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/SwiftUIReorderableForEach.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = 'SwiftUIReorderableForEach'
3 | s.version = '1.0.0'
4 | s.summary = 'Drag & drop to reorder items in SwiftUI.'
5 | s.homepage = 'https://github.com/globulus/swiftui-reorderable-foreach'
6 | s.license = { :type => 'MIT', :file => 'LICENSE' }
7 | s.author = { 'Gordan Glavaš' => 'gordan.glavas@gmail.com' }
8 | s.source = { :git => 'https://github.com/globulus/swiftui-reorderable-foreach.git', :tag => s.version.to_s }
9 | s.ios.deployment_target = '15.0'
10 | s.swift_version = '5.0'
11 | s.source_files = 'Sources/SwiftUIReorderableForEach/**/*'
12 | end
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Gordan Glavaš
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftUIReorderableForEach
2 |
3 | Easily implement **drag & drop to reorder items in SwiftUI**.
4 |
5 | This package contains a generic **ReoderableForEach** component, which can then be plugged into **any layout**, such as `VStack`, `LazyVGrid`, etc. The end result looks like this:
6 |
7 |
8 |
9 |  |
10 |  |
11 |
12 |
13 |
14 | ## Features
15 |
16 | * Supports any `Hashable` data.
17 | * Works with any SwiftUI layout.
18 | * Binding to dynamically enable/disable reordering functionality.
19 | * Custom item rendering with additional info on if the current item is being dragged or not.
20 |
21 | ## Installation
22 |
23 | This component is distrubuted as a **Swift package**. Just add this URL to your package list:
24 |
25 | ```text
26 | https://github.com/globulus/swiftui-reorderable-foreach
27 | ```
28 |
29 | You can also use **CocoaPods**:
30 |
31 | ```ruby
32 | pod 'SwiftUIReorderableForEach', '~> 1.0.0'
33 | ```
34 |
35 | ## Sample usage
36 |
37 | ### VStack
38 |
39 | ```swift
40 | struct ReorderingVStackTest: View {
41 | @State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"]
42 | @State private var allowReordering = false
43 |
44 | var body: some View {
45 | VStack {
46 | Toggle("Allow reordering", isOn: $allowReordering)
47 | .frame(width: 200)
48 | .padding(.bottom, 30)
49 | VStack {
50 | ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in
51 | Text(item)
52 | .font(.title)
53 | .padding()
54 | .frame(minWidth: 200, minHeight: 50)
55 | .border(Color.blue)
56 | .background(Color.red.opacity(0.9))
57 | .overlay(isDragged ? Color.white.opacity(0.6) : Color.clear)
58 | }
59 | }
60 | }
61 | }
62 | }
63 | ```
64 |
65 | ### LazyVGrid
66 |
67 | ```swift
68 | struct ReorderingVGridTest: View {
69 | @State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"]
70 | @State private var allowReordering = false
71 |
72 | var body: some View {
73 | VStack {
74 | Toggle("Allow reordering", isOn: $allowReordering)
75 | .frame(width: 200)
76 | .padding(.bottom, 30)
77 | LazyVGrid(columns: [
78 | GridItem(.flexible()),
79 | GridItem(.flexible())
80 | ]) {
81 | ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in
82 | Text(item)
83 | .font(.title)
84 | .padding()
85 | .frame(minWidth: 150, minHeight: 50)
86 | .border(Color.blue)
87 | .background(Color.red.opacity(0.9))
88 | .overlay(isDragged ? Color.white.opacity(0.6) : Color.clear)
89 | }
90 | }
91 | }
92 | .padding()
93 | }
94 | }
95 | ```
96 |
97 | ## Recipe
98 |
99 | Check out [this recipe](https://swiftuirecipes.com/blog/swiftui-drag-to-reorder-foreach-stack-grid) for in-depth description of the component and its code. Check out [SwiftUIRecipes.com](https://swiftuirecipes.com) for more **SwiftUI recipes**!
100 |
101 | ## Changelog
102 |
103 | * 1.0.0 - Initial release.
104 |
--------------------------------------------------------------------------------
/Sources/SwiftUIReorderableForEach/SwiftUIReorderableForEach.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import UniformTypeIdentifiers
3 |
4 | public struct ReorderableForEach: View
5 | where Data : Hashable, Content : View {
6 | @Binding var data: [Data]
7 | @Binding var allowReordering: Bool
8 | private let content: (Data, Bool) -> Content
9 |
10 | @State private var draggedItem: Data?
11 | @State private var hasChangedLocation: Bool = false
12 |
13 | public init(_ data: Binding<[Data]>,
14 | allowReordering: Binding,
15 | @ViewBuilder content: @escaping (Data, Bool) -> Content) {
16 | _data = data
17 | _allowReordering = allowReordering
18 | self.content = content
19 | }
20 |
21 | public var body: some View {
22 | ForEach(data, id: \.self) { item in
23 | if allowReordering {
24 | content(item, hasChangedLocation && draggedItem == item)
25 | .onDrag {
26 | draggedItem = item
27 | return NSItemProvider(object: "\(item.hashValue)" as NSString)
28 | }
29 | .onDrop(of: [UTType.plainText], delegate: DragRelocateDelegate(
30 | item: item,
31 | data: $data,
32 | draggedItem: $draggedItem,
33 | hasChangedLocation: $hasChangedLocation))
34 | } else {
35 | content(item, false)
36 | }
37 | }
38 | }
39 |
40 | struct DragRelocateDelegate: DropDelegate
41 | where Data : Equatable {
42 | let item: Data
43 | @Binding var data: [Data]
44 | @Binding var draggedItem: Data?
45 | @Binding var hasChangedLocation: Bool
46 |
47 | func dropEntered(info: DropInfo) {
48 | guard item != draggedItem,
49 | let current = draggedItem,
50 | let from = data.firstIndex(of: current),
51 | let to = data.firstIndex(of: item)
52 | else {
53 | return
54 | }
55 |
56 | hasChangedLocation = true
57 |
58 | if data[to] != current {
59 | withAnimation {
60 | data.move(fromOffsets: IndexSet(integer: from),
61 | toOffset: (to > from) ? to + 1 : to)
62 | }
63 | }
64 | }
65 |
66 | func dropUpdated(info: DropInfo) -> DropProposal? {
67 | DropProposal(operation: .move)
68 | }
69 |
70 | func performDrop(info: DropInfo) -> Bool {
71 | hasChangedLocation = false
72 | draggedItem = nil
73 | return true
74 | }
75 | }
76 | }
77 |
78 | struct ReorderingVStackTest: View {
79 | @State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"]
80 | @State private var allowReordering = false
81 |
82 | var body: some View {
83 | VStack {
84 | Toggle("Allow reordering", isOn: $allowReordering)
85 | .frame(width: 200)
86 | .padding(.bottom, 30)
87 | VStack {
88 | ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in
89 | Text(item)
90 | .font(.title)
91 | .padding()
92 | .frame(minWidth: 200, minHeight: 50)
93 | .border(Color.blue)
94 | .background(Color.red.opacity(0.9))
95 | .overlay(isDragged ? Color.white.opacity(0.6) : Color.clear)
96 | }
97 | }
98 | }
99 | }
100 | }
101 |
102 | struct ReorderingVGridTest: View {
103 | @State private var data = ["Apple", "Orange", "Banana", "Lemon", "Tangerine"]
104 | @State private var allowReordering = false
105 |
106 | var body: some View {
107 | VStack {
108 | Toggle("Allow reordering", isOn: $allowReordering)
109 | .frame(width: 200)
110 | .padding(.bottom, 30)
111 | LazyVGrid(columns: [
112 | GridItem(.flexible()),
113 | GridItem(.flexible())
114 | ]) {
115 | ReorderableForEach($data, allowReordering: $allowReordering) { item, isDragged in
116 | Text(item)
117 | .font(.title)
118 | .padding()
119 | .frame(minWidth: 150, minHeight: 50)
120 | .border(Color.blue)
121 | .background(Color.red.opacity(0.9))
122 | .overlay(isDragged ? Color.white.opacity(0.6) : Color.clear)
123 | }
124 | }
125 | }
126 | .padding()
127 | }
128 | }
129 |
130 | struct ReorderingVStackTest_Previews: PreviewProvider {
131 | static var previews: some View {
132 | ReorderingVStackTest()
133 | }
134 | }
135 |
136 | struct ReorderingGridTest_Previews: PreviewProvider {
137 | static var previews: some View {
138 | ReorderingVGridTest()
139 | }
140 | }
141 |
--------------------------------------------------------------------------------