5
votes

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.

2
Have you solved this question?fujianjin6471

2 Answers

2
votes

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:

  1. 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

  2. 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]

  3. Match the location of touch against the bounding rect of the text and find the desired location in your text

  4. Set textView.selectedRange to NSMakeRange(calculatedLoc, 0)

2
votes

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
}