I'm implementing drag and drop in my app.
One of the main screens where drag and drop will be applied (mostly drop) is in a UITextView
.
I've added a drop interaction to this UITextView
with the following code:
let dropInteraction = UIDropInteraction(delegate: self)
inputTextView.addInteraction(dropInteraction)
By doing so I now accept drops in it.
In order to accept only images and text as drop I've implemented the following delegate method:
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String, kUTTypeText as String]) && session.items.count == 1
}
As required I've implemented as well the following required delegate method to return a UIDropProposal
:
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
let dropProposal: UIDropProposal = UIDropProposal(operation: .copy)
dropProposal.isPrecise = true
return dropProposal
}
Then in the performDrop delegate method I handle the dropped content and attach it to the UITextView
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
// Access the dropped object types: UIImage, NSString
session.loadObjects(ofClass: UIImage.self) { (imageItems) in
let images = imageItems as! [UIImage]
guard var image = images.first else { return }
// scale the image
image = UIImage(cgImage: image.cgImage!, scale: 4, orientation: .up)
let currentText: NSMutableAttributedString = NSMutableAttributedString(attributedString: self.inputTextView.attributedText)
let textAttachment = NSTextAttachment()
textAttachment.image = image
let imageText = NSAttributedString(attachment: textAttachment).mutableCopy() as! NSMutableAttributedString
let imageStyle = NSMutableParagraphStyle()
imageStyle.alignment = .center
imageText.addAttribute(NSAttributedStringKey.paragraphStyle, value: imageStyle, range: NSMakeRange(0, imageText.length))
// Prepares the string to append with line breaks and the image centered
let attributedStringToAppend: NSMutableAttributedString = NSMutableAttributedString(attributedString: NSAttributedString(string: "\n\n"))
attributedStringToAppend.append(imageText)
attributedStringToAppend.append(NSAttributedString(string: "\n\n"))
// Applies text attributes to the NSMutableAttrString as the default text input attributes
let range: NSRange = NSRange.init(location: 0, length: attributedStringToAppend.length)
let style = NSMutableParagraphStyle()
style.lineSpacing = 4
style.alignment = .center
let font = Font(.installed(.OpenSansRegular), size: .custom(18)).instance
let attr: [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.paragraphStyle.rawValue): style, NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue): font, NSAttributedStringKey(rawValue: NSAttributedStringKey.foregroundColor.rawValue): UIColor.rgb(red: 52, green: 52, blue: 52)]
attributedStringToAppend.addAttributes(attr, range: range)
let location = self.inputTextView.selectedRange.location
currentText.insert(attributedStringToAppend, at: location)
self.inputTextView.attributedText = currentText
}
session.loadObjects(ofClass: NSString.self) { (stringItems) in
let strings = stringItems as! [NSString]
guard let text = strings.first else { return }
self.inputTextView.insertText(text as String)
}
}
By doing all of this both images and text are dropped as expected into the desired field but I have one problem here.
In the New Mail Composer
in the mail app, when you're dragging either items or images to it, it shows a tracking cursor so you know where the drop content goes to, but I cannot do this in my example.
The drops goes to the end of the existing text.
If I want to drop to a specific location, I have to before place the cursor in the desired position and then perform the drop, so in the performDrop
function as you see, I retrieve the cursor location by the selectedRange
.
I want to see the cursor flow as the drop animation is occurring, just like the mail app or the notes app, where you can choose where the drop object goes to.
Does anyone have a hint on how to achieve this?
Thanks in advance.
You can move the cursor in a textView or textField by setting the selectedRange
to a NSRange
with a valid location and a length of 0
.
I won't spoil you with finished code, but rather give a step by step explanation on how to solve the problem yourself:
Compute the current location of the touch in the superview, for instance you could use touchesMoved:
, then convert the location of the touch into the textView's coordinate system
Calculate the bounding rect of your textView's text
by applying the text properties of your textView (font, size etc.) like so:
[textView.text boundingRectWithSize:textView.bounds.size options:NSStringDrawingUsesLineFragmentOrigin attributes:textAttributesDict context:nil]
Match the location of touch against the bounding rect of the text and find the desired location in your text
Set textView.selectedRange
to NSMakeRange(calculatedLoc, 0)
The easiest way I've found to do this is to update the cursor in sessionDidUpdate
.
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
inputTextView.becomeFirstResponder()
let point = session.location(in: textView)
if let position = inputTextView.closestPosition(to: point) {
inputTextView.selectedTextRange = inputTextView.textRange(from: position, to: position)
}
let dropProposal: UIDropProposal = UIDropProposal(operation: .copy)
dropProposal.isPrecise = true
return dropProposal
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With