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 7 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 @@ -20,6 +20,7 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
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.gestures.detectDragGestures
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,19 @@ 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.draganddrop.forEachDataItem
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable
actual fun DragAndDropExample() {
Column(
Expand Down Expand Up @@ -74,15 +79,14 @@ 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 {
val data = DragAndDropTransferData {
encodeString(text)
addLog("Sent: $text")
}
)

data
}
.background(Color.DarkGray),
color = Color.White,
text = text
Expand All @@ -103,18 +107,17 @@ actual fun DragAndDropExample() {
target = object : DragAndDropTarget {
override fun onDrop(event: DragAndDropEvent): Boolean {
// TODO: finalize event and transferable API
addLog("Dropped")
val coroutineScope = CoroutineScope(Dispatchers.Main)

// 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")
event.forEachDataItem {
coroutineScope.launch {
decodeString()?.let {
dropText = it
addLog("Received: $it")
}
}
}

return true
}
Expand Down
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,13 +16,20 @@

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
import androidx.compose.ui.unit.toOffset
import platform.UIKit.UIDragItem
import platform.UIKit.UIDropSessionProtocol
import platform.UIKit.UIView
import androidx.compose.ui.uikit.utils.cmp_itemWithString
import androidx.compose.ui.uikit.utils.cmp_loadString
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
import platform.Foundation.NSError

/**
* A representation of an event sent by the platform during a drag and drop operation.
Expand All @@ -37,14 +44,76 @@ actual class DragAndDropEvent internal constructor(
get() = dropSessionContext.session
}

@ExperimentalComposeUiApi
interface DragAndDropTransferDataEncodingScope {
fun encodeString(value: String)
}

@ExperimentalComposeUiApi
interface DragAndDropTransferDataItemDecodingScope {
/**
* Returns a String if a String was encoded in the current item. Otherwise returns null.
*/
suspend fun decodeString(): String?
}

/**
* Perform a decoding in the context of each item contained in the [DragAndDropEvent].
*/
@ExperimentalComposeUiApi
fun DragAndDropEvent.forEachDataItem(block: DragAndDropTransferDataItemDecodingScope.() -> Unit) {
// Session will reset its items and associated providers on next run loop tick, so they need to
// be saved before the `block` will start executing decoding operations (which are async)
val providers = session.items.map {
val item = it as UIDragItem
item.itemProvider
}

for (provider in providers) {
object : DragAndDropTransferDataItemDecodingScope {
override suspend fun decodeString(): String? =
suspendCoroutine { continuation ->
provider.cmp_loadString { string, nsError ->
if (nsError != null) {
continuation.resumeWithException(nsError.asThrowable())
} else {
continuation.resume(string)
}
}
}
}.block()
}
}

/**
* 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>
)
) {
@ExperimentalComposeUiApi
constructor(block: DragAndDropTransferDataEncodingScope.() -> Unit) : this(
object : DragAndDropTransferDataEncodingScope {
val items = mutableListOf<UIDragItem>()

override fun encodeString(value: String) {
val item = UIDragItem.cmp_itemWithString(value)
items.add(item)
}
}.apply(block).items
)
}

/**
* Adapter allowing [NSError] to participate in the Kotlin exception machinery.
*/
internal class ThrowableNSError(val error: NSError): Throwable(error.toString())

internal fun NSError.asThrowable(): Throwable {
return ThrowableNSError(this)
}

/**
* Returns the position of this [DragAndDropEvent] relative to the root Compose View in the
Expand Down
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
Loading