{"id":19093052,"url":"https://github.com/ryanlintott/iliketomoveit","last_synced_at":"2025-04-30T12:43:17.536Z","repository":{"id":179583110,"uuid":"663297828","full_name":"ryanlintott/ILikeToMoveIt","owner":"ryanlintott","description":"Accessible move actions for SwiftUI Lists and easy custom drag and drop for older iOS","archived":false,"fork":false,"pushed_at":"2024-10-05T01:41:57.000Z","size":297,"stargazers_count":29,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-14T11:26:21.312Z","etag":null,"topics":["a11y","accessibility","ios","swift","swiftui","swiftui-lists"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ryanlintott.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-07T02:05:22.000Z","updated_at":"2024-10-05T01:40:53.000Z","dependencies_parsed_at":null,"dependency_job_id":"ffb31d7b-4e69-4661-8ab2-b4f7575e92c9","html_url":"https://github.com/ryanlintott/ILikeToMoveIt","commit_stats":null,"previous_names":["ryanlintott/iliketomoveit"],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FILikeToMoveIt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FILikeToMoveIt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FILikeToMoveIt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ryanlintott%2FILikeToMoveIt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ryanlintott","download_url":"https://codeload.github.com/ryanlintott/ILikeToMoveIt/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251702677,"owners_count":21630071,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["a11y","accessibility","ios","swift","swiftui","swiftui-lists"],"created_at":"2024-11-09T03:23:10.231Z","updated_at":"2025-04-30T12:43:17.519Z","avatar_url":"https://github.com/ryanlintott.png","language":"Swift","funding_links":["https://ko-fi.com/X7X04PU6T"],"categories":[],"sub_categories":[],"readme":"\u003cpicture\u003e\n  \u003csource srcset=\"https://github.com/ryanlintott/ILikeToMoveIt/assets/2143656/fb28d9e9-7e1c-4c05-9f00-130daf64a513\" media=\"(prefers-color-scheme: dark)\"\u003e\n  \u003cimg width=\"600\" src=\"https://github.com/ryanlintott/ILikeToMoveIt/assets/2143656/e7df51f5-f74a-4d3e-ad03-a13b77c305a9\"\u003e\n\u003c/picture\u003e\n\n\n[![Swift Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fryanlintott%2FILikeToMoveIt%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/ryanlintott/ILikeToMoveIt)\n[![Platform Compatibility](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fryanlintott%2FILikeToMoveIt%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/ryanlintott/ILikeToMoveIt)\n![License - MIT](https://img.shields.io/github/license/ryanlintott/ILikeToMoveIt)\n![Version](https://img.shields.io/github/v/tag/ryanlintott/ILikeToMoveIt?label=version)\n![GitHub last commit](https://img.shields.io/github/last-commit/ryanlintott/ILikeToMoveIt)\n[![Mastodon](https://img.shields.io/badge/mastodon-@ryanlintott-5c4ee4.svg?style=flat)](http://mastodon.social/@ryanlintott)\n[![Twitter](https://img.shields.io/badge/twitter-@ryanlintott-blue.svg?style=flat)](http://twitter.com/ryanlintott)\n\n# Overview\n- Add [accessible move actions](#accessibilitymoveable) to any array of items in a SwiftUI List or ForEach.\n- Make drag-and-drop operations easier for custom types in iOS 14 and 15 using [`Providable`](#providable)\n- Make drag-to-create-a-new-window operations easier in iPadOS using [`UserActivityProvidable`](#useractivityprovidable)\n\n# Demo\nThe `Example` folder has an app that demonstrates the features of this package and how to set up [Drag and Drop for Custom Types](#drag-and-drop-for-custom-types).\n\n\u003ca href=\"https://mastodon.social/@ryanlintott/110690143602729594\"\u003e\u003cimg width=\"250\" alt=\"ILikeToMoveIt demo app with the logo at the top and two lists at the bottom. The left list contains a number of birds. Chicken is dragged up a few spaces. Cardinal is dragged to the empty list on the right. Robin, Goose, and Swan are picked up from the left list and dropped on the right list. Text reading StringBird above the list is dragged onto the right list. Switching over to the reminders app, two reminders named Crow and Finch are picked up and dragged back into the right list of ILikeToMoveIt. VoiceOver is turned on and Robin is moved up and down using accessibility actions. Each time the move and the final position above Chicken or below Blue Jay is reported along with At Top or At Bottom if applicable.\" src=\"https://github.com/user-attachments/assets/6ecf445f-82d8-4cc2-8135-bb374fe9d7af\"\u003e\u003c/a\u003e\n\n# Installation and Usage\n1. In Xcode go to `File -\u003e Add Packages`\n2. Paste in the repo's url: `https://github.com/ryanlintott/ILikeToMoveIt` and select by version.\n3. Import the package using `import ILikeToMoveIt`\n\n# Platforms\nThis package is compatible with iOS 14+ but the accessibility move feature only works for iOS 15+.\n\n# Support iLikeToMoveIt\nIf you like this package, buy me a coffee to say thanks!\n\n[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/X7X04PU6T)\n\nOr you can buy a t-shirt with the iLikeToMoveIt logo\n\n\u003ca href=\"https://cottonbureau.com/p/44WXMN/shirt/i-like-to-move-it#/18025527\"\u003e\u003cimg width=\"256\" alt=\"ShapeUp T-Shirt\" src=\"https://cottonbureau.com/mockup?vid=18025527\u0026hash=1055\u0026w=512\"\u003e\u003c/a\u003e\n\n- - -\n# Details\n## AccessibilityMoveable\n*\\*iOS 15+*\n\nTwo modifiers are required to enable accessible move actions. One for each item and one for the list itself.\n\n```swift\nList {\n  ForEach(items) { item in\n    Text(item.name)\n      .accessibilityMoveable(item)\n  }\n}\n.accessibilityMoveableList($items, label: \\.name)\n```\n\n### `.accessibilityMoveable`\nAdding this modifier will add accessibility actions to move the item up, down, to the top of the list and to the bottom. If you want to customize these actions you can supply your own array.\n\nExample: If you have a short list and only want up and down.\n```swift\n.accessibilityMoveable(item, actions: [.up, .down])\n```\n\nExample: If you have a long list and want options to move items more than one step at a time.\n```swift\n.accessibilityMoveable(item, actions: [.up, .down, .up(5), .down(5), .toTop, .toBottom])\n```\n\nWhen the user triggers an accessibility action the following results are reported back via a UIAccessibility announcement:\n- \"moved up\", \"moved down\", or \"not moved\"\n- \"by [number of spaces]\" if moved by more than one space.\n- \"above [item label]\" if moved down and \"below [item label]\" if moved up. Only if a label keypath is was provided.\n- \"At top\" or \"At bottom\" if at the top or bottom of the list.\n\n### `.accessibilityMoveableList`\nThis modifier applies the changes from the move actions to the list and adjusts the accessibility focus to ensure it stays on the correct item.\n\nYou pass in a binding to the array of items and an optional label keypath. This label will be read out after moving an item to let the user know what item is directly below after moving up or directly above after moving down.\n\n```swift\n.accessibilityMoveableList($items, label: \\.name)\n```\n\n### Known issues\n- Moving the same item again immediately after moving it may cause the accessibility focus to lag and another item will be moved instead.\n\n## Providable\nThis protocol allows for easier drag and drop for `Codable` objects in iOS 14 and 15\n\nDrag and drop operations were made much easier in iOS 16 by the `Transferable` protocol. Older methods use `NSItemProvider` and were cumbersome to set up.\n\n### How to use it\nConform your object to `Providable`. Add readable and writable types, then add functions to transform your object to and from those types.\n```swift\nextension Bird: Providable {\n    static let writableTypes: [UTType] = [.bird]\n\n    static let readableTypes: [UTType] = [.bird, .plainText]\n\n    func data(type: UTType) async throws-\u003e Data? {\n        switch type {\n        case .bird:\n            return try JSONEncoder().encode(self)\n        default:\n            return nil\n        }\n    }\n\n    init?(type: UTType, data: Data) throws {\n        switch type {\n        case .bird:\n            self = try JSONDecoder().decode(Bird.self, from: data)\n        case .plainText:\n            let string = String(decoding: data, as: UTF8.self)\n            self = Bird(name: string)\n        default:\n            return nil\n        }\n    }\n}\n```\n\nYou will need to add any custom types to your project.\nProject \u003e Target \u003e Info \u003e ExportedTypeIdentifiers\n\n### Adding drag and drop operations\n\nAdd a drag option to a view like this:\n```swift\n.onDrag { bird.provider }\n```\n\nAnd a drop option like this:\n```swift\n.onDrop(of: Bird.readableTypes) { providers, location in\n  providers.loadItems(Bird.self) { bird, error in\n    if let bird {\n        birds.append(bird)\n    }\n  }\n  return true\n}\n```\n\nAnd even an insert option like this:\n```swift\n.onInsert(of: Bird.readableTypes) { index, providers in\n  providers.loadItems(Bird.self) { bird, error in\n    if let bird {\n      birds.insert(bird, at: index)\n    }\n  }\n}\n```\n\n## UserActivityProvidable\nExtension to the `Providable` protocol to add easy drag to new window (a feature not supported by `Transferable`) on iPadOS 16+\n\nAdd your activity type string to plist under `NSUserActivityTypes` and then add the same string to the activityType parameter on your codable type.\n\n```swift\nextension Bird: UserActivityProvidable {\n  static let activityType = \"com.ryanlintott.draganddrop.birdDetail\"\n}\n```\n\nUse the `onContinueUserActivity` overload function that takes a `UserActivityProvidable` object to handle what your app does when opened via this activity.\n\n```swift\n.onContinueUserActivity(Bird.self) { bird in\n  /// Adjust state based on your object.\n}\n```\n\nYou can also target a separate WindowGroup for your object. Make sure you still use `onContinueUserActivity` in your view to ensure the object gets loaded.\n\n```swift\nWindowGroup {\n  BirdDetailView()            \n}\n.handlesExternalEvents(matching: [Bird.activityType])\n```\n\n- - -\n# Drag and Drop for Custom Types\n\n## Making a new draggable type\n- Start with a `Codable` object that you want to drag and drop.\n\n```swift\nstruct Bird: Codable {\n    let name: String\n}\n```\n\n- Add your custom object info to your Project\n\nProject \u003e Target \u003e Info \u003e Exported Type Identifiers\n\n\u003cimg width=\"814\" alt=\"Exported Type Identifiers with the description Bird, Identifier com.ryanlintott.draganddrop.bird and conforms to public.data\" src=\"https://github.com/user-attachments/assets/af4dd135-a657-40e6-a6ac-670c308c6d08\"\u003e\n\n- Add your type as an extension to UTType\n\n```swift\nimport UniformTypeIdentifiers\n\nextension UTType {\n    static let bird = UTType(\"com.ryanlintott.draganddrop.bird\") ?? .data\n}\n```\n\n## Draggable custom types in iOS 14 \u0026 15\n\n- Add this package to your project and follow instructions to conform your object to [`Providable`](#providable).\n\n## Draggable custom types in iOS 16\n\n- Conform your object to `Transferable`\n- Add the `transferRepresetation` property and include a `CodableRepresentation` for your custom type along with a `DataRepresentation` for any other compatible types.\n\n```swift\nstatic var transferRepresentation: some TransferRepresentation {\n    CodableRepresentation(contentType: .bird)\n\n    DataRepresentation(importedContentType: .plainText) { data in\n        let string = String(decoding: data, as: UTF8.self)\n        return Bird(name: string)\n    }\n}\n```\n\n## Adding drag and drop to your SwiftUI views\nOnce your type conforms to `Transferable`, adding SwiftUI drag and drop modifiers is easy!\n\n### draggable(\\_:)\nMake any view draggable by adding this modifier\n```swift\n.draggable(bird)\n```\n\n### dropDestination(for:action:isTargetted:)\nAny view can be a drop destination. Add the dropped items using the action, use the location for animation if you like, and use the isTargeted closure to animate the view when droppable content is hovering.\n```swift\n.dropDestination(for: Bird.self) { droppedBirds, location in\n    birds.append(contentsOf: droppedBirds)\n    return true\n} isTargeted: {\n    isTargetted = $0\n}\n```\n\n### dropDestination(for:action:)\nWhen added to ForEach dropped items can be inserted in-between other items.\n```\n.dropDestination(for: Bird.self) { droppedBirds, offset in\n    birds.insert(contentsOf: droppedBirds, at: offset)\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanlintott%2Filiketomoveit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fryanlintott%2Filiketomoveit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fryanlintott%2Filiketomoveit/lists"}