32

What event is fired when a block of text is pasted into a UITextView? I need to modify the frame of my textView when the text is pasted in.

Thanks for reading.

OWolf
  • 5,012
  • 15
  • 58
  • 93

12 Answers12

37

Here is what i use to detect paste events in UITextView:

 // Set this class to be the delegate of the UITextView. Now when a user will paste a text in that textview, this delegate will be called.
-(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {

    // Here we check if the replacement text is equal to the string we are currently holding in the paste board
    if ([text isEqualToString:[UIPasteboard generalPasteboard].string]) {

        // code to execute in case user is using paste

    } else {

        // code to execute other wise
    }

    return YES;
}
carlos16196
  • 706
  • 1
  • 7
  • 10
  • 16
    You probably don't want to do this kind of trick - iOS 14 will show a notification indicating that application accessed pasteboard and with this approach it will happen on each text view change. – wonder.mice Jun 26 '20 at 16:42
  • This no longer reliably works on iOS 15.0 since the "Scan Text from Camera" pastes text onto the textfield, which does not involve clipboard at all. – badhanganesh Nov 11 '21 at 11:33
21

Checking the pasteboard's string by if string == UIPasteboard.general.string takes a couple of seconds if you have long sentence in the pasteboard. The user sees the keypad is frozen while this check. My solution is to check if the length of new characters is longer than 1. If it is longer than 1, the string is from the pasteboard.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if string.characters.count > 1{
            //User did copy & paste

        }else{
            //User did input by keypad
        }            
         return true
 }
18

Your UITextView will call its UITextViewDelegate method

 - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text

if a delegate has been set up. This gets called both when a character is typed on the keyboard, and when text is pasted into the text view. The text pasted in is the replacementText argument.

See http://developer.apple.com/library/ios/#documentation/uikit/reference/UITextViewDelegate_Protocol/Reference/UITextViewDelegate.html#//apple_ref/occ/intf/UITextViewDelegate

joseph.hainline
  • 24,829
  • 18
  • 53
  • 70
8

This is working Perfect in

Xcode 11x Swift 5x

 func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    if text.contains(UIPasteboard.general.string ?? "") {
        return false
    }
    return true
}

When ever the user try to Paste into text field the if condition will execute
This code will stop pasting

Govindharaj Murugan
  • 414
  • 1
  • 5
  • 13
  • 2
    This no longer reliably works on iOS 15.0 since the "Scan Text from Camera" pastes text onto the textfield. – badhanganesh Nov 11 '21 at 11:30
  • 3
    FYI: Calling `UIPasteboard.general.string` in this way causes a prompt to appear "[CurrentApp] pasted from [IncomingApp]" every time the user types. – Stephen Emery Aug 16 '22 at 23:23
7

It is for Swift5.1

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    if let paste = UIPasteboard.general.string, text == paste {
       print("paste")
    } else {
       print("normal typing")
    }
    return true
}
User18474728
  • 363
  • 2
  • 11
  • When you access the pasteboard, the OS might put up a notification. See the comments here: https://stackoverflow.com/a/34893463/211292 – ThomasW Jan 18 '23 at 04:16
6

try subclasses UITextview,and override this function.

public override func paste(_ sender: Any?) 
slboat
  • 502
  • 6
  • 14
6

In iOS 14 there is notification which is triggered every time when application gets value from UIPasteboard.general.string
So the proper way of detect if user pasted something is overriding paste(_) function:

var isPastingContent = false

open override func paste(_ sender: Any?) {
    isPastingContent = true
    super.paste(sender)
}

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    if isPastingContent {
         // do something
    }
    isPastingContent = false
}
  • 1
    This is the way...For others using this solution, I had to also make a subclass of UITextView to host the "paste(_ sender: Any?)" function. It did not work when I placed it in my UIViewController which hosts the UITextView and the UITextView delegate. – Julio Dec 24 '21 at 06:39
  • 'ifPastingContent { ' simply checks if the boolean exists, just so you know.. – Cody Sep 18 '22 at 05:14
3

carlos16196 was a good approach, but I would also tweak it by changing [text isEqualToString:[UIPasteboard generalPasteboard].string] to [text containsString:[UIPasteboard generalPasteboard].string]

By doing this, you will detect when user pastes in the textview after other typed text that is not in the UIPasteboard.

This is the code:

-(BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {

// Here we check if the replacement text is equal to the string we are currently holding in the paste board
if ([text containsString:[UIPasteboard generalPasteboard].string]) {

    // code to execute in case user is using paste

} else {

    // code to execute other wise
}

return YES;
}
danielrosero
  • 596
  • 8
  • 14
  • 1
    you forgot to do the change you mention in your posted code – Juan Boero Apr 15 '16 at 18:01
  • Thanks, I didn't notice that. – danielrosero May 27 '16 at 05:32
  • 1
    'text' is always the new text being added, not the complete text of textView. So if it was pasted it will always match and should not be checked using 'containsString' which could actually give false positives. – aronspring Aug 27 '21 at 09:00
  • 1
    @aronspring Hi, it's been a while that I gave this answer. So, should I put it back to isEqualToString? Best regards. – danielrosero Sep 08 '21 at 10:57
  • 1
    @danielrosero I would suggest that 'isEqualToString' is preferable. But as has been mentioned elsewhere in the answers, if the string in your pasteboard is copied from another app, or even another device, you'll trigger a notification banner to appear every time you read from the pasteboard. So presumably this would happen every time a character is typed, which would be quite a poor UX. – aronspring Sep 09 '21 at 11:08
  • 1
    This no longer reliably works on iOS 15.0 since the "Scan Text from Camera" pastes text onto the textfield, which does not come from clipboard. – badhanganesh Nov 11 '21 at 11:31
2

iOS 13 with Combine

public extension UITextView {
    var textDidChangePublisher: AnyPublisher<String, Never> {
        NotificationCenter.default
            .publisher(for: UITextView.textDidChangeNotification, object: self)
            .compactMap { $0.object as? UITextView }
            .compactMap(\.text)
            .eraseToAnyPublisher()
    }
    
    var attributedTextDidChangePublisher: AnyPublisher<NSAttributedString, Never> {
        NotificationCenter.default
            .publisher(for: UITextView.textDidChangeNotification, object: self)
            .compactMap { $0.object as? UITextView }
            .compactMap(\.attributedText)
            .eraseToAnyPublisher()
    }
}
var cancellable = Set<AnyCancellable>()

textView.textDidChangePublisher
    .removeDuplicates()
    .sink { [weak self] newValue in
        guard let self = self else { return }
        // what you want
    }
    .store(in: &cancellable)
hstdt
  • 5,652
  • 2
  • 34
  • 34
0

This is what I use to detect pasted images:

- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text
{
    if (UIPasteboard.generalPasteboard.image &&
        [UIPasteboard.generalPasteboard.string.lowercaseString isEqualToString:text.lowercaseString]) {

       //Pasted image

        return NO;
    }

    return YES;
}
0

This is the only way that I was able to get it to work. I used a textField but the same concept should still work for a textView.

In the shouldChangeCharactersIn delegate method below I casted the string argument to a NSString, then back to a String. Then I compared it to whatever was pasted. Everything else is in the comments above the code.

// 1. class property for anything that was copied and will be pasted
var pasted: String?

// 2. When the user first taps the textfield set the above class property to the copied text (if there is any)
func textFieldDidBeginEditing(_ textField: UITextField) {

    // 3. set it here
    pasted = UIPasteboard.general.string // this is what was copied and will be pasted
}

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

    guard let safeText = textField.text else { return true }

    let currentString: NSString = safeText as NSString
    let newString: NSString = currentString.replacingCharacters(in: range, with: string) as NSString

    let str = newString as String

    // 4. compare the above str constant to the pasted variable
    if str == self.pasted {
        print("pasted")

    } else {

        print("typed")
    }

    return true
}

func textFieldDidEndEditing(_ textField: UITextField) {

    // 5. when the user is finished with the textField set the pasted property to nil
    pasted = nil
}
Lance Samaria
  • 17,576
  • 18
  • 108
  • 256
-2

In SWIFT 4:

func textViewDidChange(_ textView: UITextView) {

    if(textView.text == UIPasteboard.general.string)
    {
        //Text pasted
    }
}
Nupur Sharma
  • 1,106
  • 13
  • 15