Skip to content

Commit

Permalink
Added UserActivityProvidable protocol to allow dragging items to crea…
Browse files Browse the repository at this point in the history
…te a new window

Removed accessibilityMoveableIfAvailable and accessibilityMoveableListIfAvailable

Example App:
Added BirdUserActivityProvidableView to handle onContinueUserActivity for Bird objects by opening a new window
Added BirdDetailView to show info from a single bird
Added MoreInfo view to summarize what Providable and Transferable demo views can do.
Added ifAvailable modifier to help with version branching code
  • Loading branch information
ryanlintott committed Jul 24, 2023
1 parent 983f8c1 commit dd743e9
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 37 deletions.
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

# Overview
- Add [accessible move actions](#accessibilitymoveable) to any array of items in a SwiftUI List or ForEach.
- Make drag and drop operations easier for custom types in iOS 14 and 15 using [`Providable`](#providable)
- Make drag-and-drop operations easier for custom types in iOS 14 and 15 using [`Providable`](#providable)
- Make drag-to-create-a-new-window operations easier in iPadOS 16+ using [`UserActivityProvidable`](#useractivityprovidable)

# DragAndDrop (example app)
Check out the [example app](https://github.com/ryanlintott/DragAndDrop) to see how you can use this package in your iOS app.
Expand Down Expand Up @@ -83,7 +84,7 @@ You pass in a binding to the array of items and an optional label keypath. This
- Moving the same item again immediately after moving it may cause the accessibility focus to lag and another item will be moved instead.

## Providable
This protocol allows for easier drag and drop for `Codable` objects in iOS 14 and 15.
This protocol allows for easier drag and drop for `Codable` objects in iOS 14 and 15

Drag and drop operations were made much easier in iOS 16 by the `Transferable` protocol. Older methods use `NSItemProvider` and were cumbersome to set up.

Expand Down Expand Up @@ -155,4 +156,37 @@ And even an insert option like this:
}
```

## UserActivityProvidable
Extension to the `Providable` protocol to add easy drag to new window (a feature not supported by `Transferable`) on iPadOS 16+

Add your activity type string to plist under `NSUserActivityTypes` and then add the same string to the activityType parameter on your codable type.

```swift
extension Bird: UserActivityProvidable {
static var activityType: String {
"com.ryanlintott.draganddrop.birdDetail"
}
}
```

Use the `onContinueUserActivity` overload function that takes a `UserActivityProvidable` object to handle what your app does when opened via this activity.

```swift
.onContinueUserActivity(Bird.self) { bird in
guard let bird else { return }
/// Do something like open a new window with your codable type as the value.
openWindow(value: bird)
}
```

Example of a new window you could add:

```swift
WindowGroup(for: Bird.self) { $bird in
if let bird {
BirdDetailView(bird: bird)
} else {
Text("No bird.")
}
}
```
32 changes: 0 additions & 32 deletions Sources/ILikeToMoveIt/AccessibilityMoveable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,23 +123,6 @@ public extension View {
}
}

public extension View {
/// Adds accessibility move actions that allow a user to move the item up and down in a list for iOS 15+ and macOS 12+
///
/// Requires a single `.accessibilityMoveableList` modifier on a parent view to apply accessibility move actions.
/// - Parameters:
/// - item: The item to move.
/// - actions: An array of move actions made available to the user.
/// - Returns: A view of an item that can be moved up and down in a list via accessibility actions for iOS 15+ and macOS 12+.
func accessibilityMoveableIfAvailable<Item: Hashable>(_ item: Item, actions: [AccessibilityMoveAction] = [.up, .down, .toTop, .toBottom]) -> some View {
if #available(iOS 15, macOS 12, *) {
return accessibilityMoveable(item, actions: actions)
} else {
return self
}
}
}

/// A View Modifier that applies accessibility move actions from child views that use `AccessibilityMoveableViewModifier`
@available(iOS 15, macOS 12, *)
struct AccessibilityMoveableListViewModifier<Item: Hashable>: ViewModifier {
Expand Down Expand Up @@ -241,18 +224,3 @@ public extension View {
modifier(AccessibilityMoveableListViewModifier(items: items, label: label))
}
}

public extension View {
/// Applies accessibility move actions from child views that use `accessibilityMoveable` for iOS 15+ and macOS 12+
/// - Parameters:
/// - items: Array of items that will be modified by accessibility move actions.
/// - label: Optional keypath to the name of an item. If used, the names of items that are directly below a move up or above a move down will be annouced after a move.
/// - Returns: A view that applies accessibility move actions from child views for iOS 15+ and macOS 12+
func accessibilityMoveableListIfAvailable<Item: Hashable>(_ items: Binding<Array<Item>>, label: KeyPath<Item, String>? = nil) -> some View {
if #available(iOS 15, macOS 12, *) {
return accessibilityMoveableList(items, label: label)
} else {
return self
}
}
}
11 changes: 8 additions & 3 deletions Sources/ILikeToMoveIt/Providable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//
// Providable.swift
// DragAndDrop
// ILikeToMoveIt
//
// Created by Ryan Lintott on 2022-10-12.
//
Expand All @@ -15,7 +15,6 @@ public protocol Providable: Codable {
/// An array of types that this object can be read from.
static var readableTypes: [UTType] { get }


/// Returns a data representation of this object based on the specified type.
/// - Parameter type: Type to use when converting to Data.
/// - Returns: A data representation of this object based on the specified type.
Expand All @@ -31,7 +30,13 @@ public protocol Providable: Codable {
extension Providable {
/// An `NSItemProvider` object based on this object.
public var provider: NSItemProvider {
.init(object: ItemProvider(self))
let provider = NSItemProvider(object: ItemProvider(self))

if let userActivity = (self as? UserActivityProvidable)?.userActivity {
provider.registerObject(userActivity, visibility: .all)
}

return provider
}

public static func load(from provider: NSItemProvider, completionHandler: @escaping (Self?, Error?) -> Void) {
Expand Down
55 changes: 55 additions & 0 deletions Sources/ILikeToMoveIt/UserActivityProvidable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// UserActivityProvidable.swift
// ILikeToMoveIt
//
// Created by Ryan Lintott on 2023-07-13.
//

import SwiftUI

/// A `Providable`object with that has an `NSUserActivity` property that will help it create new windows on iPadOS.
///
/// Make sure to add your activity type string to plist under `NSUserActivityTypes` and then use the `onContinueUserActivity` overload function that takes a `UserActivityProvidable` object to handle what your app does when opened via this activity.
public protocol UserActivityProvidable: Providable {
/// Type identifier for an associated user activity.
///
/// Add this string value to your plist under NSUserActivityTypes
static var activityType: String { get }
}

public extension UserActivityProvidable {
init?(activity: NSUserActivity) {
guard activity.activityType == Self.activityType else { return nil }
guard
let data = activity.targetContentIdentifier?.data(using: .utf8),
let item = try? JSONDecoder().decode(Self.self, from: data)
else {
return nil
}
self = item
}

var userActivity: NSUserActivity? {
if Self.activityType.isEmpty { return nil }
guard
let data = try? JSONEncoder().encode(self)
else { return nil }
let activity = NSUserActivity(activityType: Self.activityType)
let string = String(data: data, encoding: .utf8)
activity.targetContentIdentifier = string
return activity
}
}

public extension View {
/// Registers a handler to invoke when a new scene is created by dropping the specified `UserActivityProvidable` type.
/// - Parameters:
/// - item: The type of object that will envoke this handler.
/// - action: The handler that will run when the new scene is created with an optional item that was dropped. The item will be nil if there was an error in the encoding or decoding process.
func onContinueUserActivity<T: UserActivityProvidable>(_ item: T.Type, perform action: @escaping (T?) -> Void ) -> some View {
onContinueUserActivity(T.activityType) { activity in
let item = T(activity: activity)
action(item)
}
}
}

0 comments on commit dd743e9

Please sign in to comment.