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 iOS Drag&Drop #1690

Merged
merged 9 commits into from
Dec 13, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package androidx.compose.mpp.demo.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.content.contentReceiver
import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
import androidx.compose.foundation.layout.Arrangement
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package androidx.compose.mpp.demo.components

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.draganddrop.dragAndDropSource
import androidx.compose.foundation.draganddrop.dragAndDropTarget
Expand All @@ -38,14 +37,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draganddrop.DragAndDropEvent
import androidx.compose.ui.draganddrop.DragAndDropTarget
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.draganddrop.DragAndDropTransferData
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.uikit.fromString
import androidx.compose.ui.uikit.loadString
import androidx.compose.ui.unit.dp
import platform.UIKit.UIDragItem

@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalComposeUiApi::class)
@Composable
actual fun DragAndDropExample() {
Column(
Expand Down Expand Up @@ -74,15 +77,10 @@ actual fun DragAndDropExample() {
.padding(16.dp)
.fillMaxWidth(1f)
.height(200.dp)
.dragAndDropSource(
drawDragDecoration = {
drawRect(Color.Gray, Offset(0f, 0f), size)
},
transferData = { offset ->
// TODO: Implement iOS specific transfer data creation
null
}
)
.dragAndDropSource {
addLog("Sent: $text")
DragAndDropTransferData(listOf(UIDragItem.fromString(text)))
}
.background(Color.DarkGray),
color = Color.White,
text = text
Expand All @@ -102,20 +100,11 @@ actual fun DragAndDropExample() {
shouldStartDragAndDrop = { true },
target = object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
// TODO: finalize event and transferable API

// stubs for `DragAndDropEvent` UIDragItem integration support
// CoroutineScope(Dispatchers.Main).launch {
// for (item in event.session.items) {
// val dragItem = item as UIDragItem
// dragItem.decodeString()?.let {
// dropText = it
// }
// }
// }

addLog("DragAndDropTarget.onDrop with $event")

addLog("Dropped")
event.forEachString {
dropText = it
addLog("Received: $it")
}
return true
}
}
Expand All @@ -131,4 +120,13 @@ actual fun DragAndDropExample() {
}
}
}
}
}

@OptIn(ExperimentalComposeUiApi::class)
private fun DragAndDropEvent.forEachString(block: (String) -> Unit) {
items.forEach {
it.loadString { s, _ ->
s?.let(block)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ NS_ASSUME_NONNULL_BEGIN

@end

/// Helper category for decoding `UIDragItem` into some typed object
@interface UIDragItem (CMPDecoding)
/// Helper category for decoding `NSItemProvider` into some typed object
@interface NSItemProvider (CMPDecoding)

- (void)cmp_loadString:(void (^)(NSString * _Nullable result, NSError *error))completionHandler;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,18 @@ - (void)sessionDidExit:(id<UIDropSession>)session interaction:(UIDropInteraction

@end

@implementation UIDragItem (CMPDecoding)
@implementation NSItemProvider (CMPDecoding)

- (void)cmp_loadString:(void (^)(NSString * _Nullable result, NSError *_Nullable error))completionHandler {
if ([self.itemProvider canLoadObjectOfClass:NSString.class]) {
[self.itemProvider loadObjectOfClass:NSString.class completionHandler:completionHandler];
if ([self canLoadObjectOfClass:NSString.class]) {
[self loadObjectOfClass:NSString.class completionHandler:completionHandler];
} else {
completionHandler(nil, nil);
}
}

- (void)cmp_loadAny:(Class)objectClass onCompletion:(void (^)(id _Nullable result, NSError *_Nullable error))completionHandler {
// Check that an object of objectClass can be loaded from UIDragItem
// Check that an object of objectClass can be loaded from NSItemProvider
if (![objectClass conformsToProtocol:@protocol(NSItemProviderReading)]) {
NSDictionary *userInfo = @{
@"description" : [NSString stringWithFormat:@"%@ doesn't conform to protocol NSItemProviderReading and thus can't be loaded", objectClass.description]
Expand All @@ -91,9 +91,9 @@ - (void)cmp_loadAny:(Class)objectClass onCompletion:(void (^)(id _Nullable resul
code:0
userInfo:userInfo];
completionHandler(nil, error);
} else if ([self.itemProvider canLoadObjectOfClass:objectClass]) {
} else if ([self canLoadObjectOfClass:objectClass]) {
// Try loading the object of this class
[self.itemProvider loadObjectOfClass:objectClass completionHandler:completionHandler];
[self loadObjectOfClass:objectClass completionHandler:completionHandler];
} else {
// This UIDragItem does't contain object of `objectClass`
completionHandler(nil, nil);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package androidx.compose.ui.draganddrop

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.uikit.density
import androidx.compose.ui.unit.asDpOffset
Expand All @@ -35,15 +36,22 @@ actual class DragAndDropEvent internal constructor(

internal val session: UIDropSessionProtocol
get() = dropSessionContext.session

@ExperimentalComposeUiApi
val items: List<UIDragItem>
get() = session.items.filterIsInstance<UIDragItem>()
}

/**
* On iOS drag and drop session data is represented by [UIDragItem]s, which contains
* information about how data can be transferred across processes boundaries and an optional
* local object to be used in the same app.
*/
actual class DragAndDropTransferData internal constructor (
internal val items: List<UIDragItem>
actual class DragAndDropTransferData
@ExperimentalComposeUiApi
constructor(
@property:ExperimentalComposeUiApi
val items: List<UIDragItem>
)

/**
Expand All @@ -55,4 +63,4 @@ internal actual val DragAndDropEvent.positionInRoot: Offset
session
.locationInView(view)
.asDpOffset()
.toOffset(view.density)
.toOffset(view.density)
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package androidx.compose.ui.uikit

import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.uikit.utils.cmp_itemWithAny
import androidx.compose.ui.uikit.utils.cmp_itemWithString
import androidx.compose.ui.uikit.utils.cmp_loadAny
import androidx.compose.ui.uikit.utils.cmp_loadString
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ObjCClass
import platform.Foundation.NSError
import platform.UIKit.UIDragItem
import platform.darwin.NSObject

/**
* Encodes [String] to [UIDragItem] for use in drag-and-drop operations.
*
* @param string The string representation of a drag item.
* @return A new [UIDragItem] instance created from the provided string.
*/
@ExperimentalComposeUiApi
fun UIDragItem.Companion.fromString(string: String): UIDragItem =
UIDragItem.cmp_itemWithString(string)

/**
* Encodes [NSObject] into [UIDragItem] for use in drag-and-drop operations.
*
* @param objectClass The [ObjCClass] representing the class of the object to be used in the drag item.
* @param nsObject The [NSObject] instance to be associated with the created [UIDragItem]. Can be null.
* @return A [UIDragItem] constructed using the provided [objectClass] and [nsObject]. Throws an exception if the resulting drag item is null.
*/
@OptIn(BetaInteropApi::class)
@ExperimentalComposeUiApi
fun <T: NSObject, NSItemProviderWriting> UIDragItem.Companion.fromNSObject(
objectClass: ObjCClass,
nsObject: T
): UIDragItem =
requireNotNull(UIDragItem.cmp_itemWithAny(objectClass, nsObject))

/**
* Loads a string provided by the drag-and-drop session.
*
* @param block Callback function that receives the loaded string and an [NSError] if applicable.
*/
@ExperimentalComposeUiApi
fun UIDragItem.loadString(block: (String?, NSError?) -> Unit) {
itemProvider.cmp_loadString(block)
}

/**
* Loads a specified [NSObject] class from a [UIDragItem]'s item provider, invoking the provided block
* upon completion.
*
* @param objectClass The [ObjCClass] that represents the class of the object to be loaded.
* @param block A callback function that will receive the loaded object of type [T] and an associated [NSError] if the operation fails. The loaded object may be null.
*/
@OptIn(BetaInteropApi::class)
@ExperimentalComposeUiApi
fun <T: NSObject, NSItemProviderReading> UIDragItem.loadNSObject(
objectClass: ObjCClass,
block: (T?, NSError?) -> Unit
) {
itemProvider.cmp_loadAny(objectClass) { any: Any?, nsError: NSError? ->
@Suppress("UNCHECKED_CAST")
block(any as T?, nsError)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.window.DialogProperties
import kotlinx.cinterop.CValue
import platform.CoreGraphics.CGSize

/**
* Properties that are used to configure the behavior of the interop view.
Expand Down