How to know when text is pasted into a UITextView?
Asked Answered
D

12

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.

Decemvirate answered 17/9, 2012 at 7:31 Comment(0)
H
19

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

Handed answered 19/9, 2012 at 2:30 Comment(0)
N
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;
}
Nahamas answered 20/1, 2016 at 7:0 Comment(2)
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.Jewry
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.Substantialize
C
22

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
 }
Cerebrospinal answered 30/8, 2017 at 12:8 Comment(4)
Technically, this wouldn't detect if the user was pasting one character from the pasteboard (why why why?) or typing a character, but for all intents and purposes, it does the job. It is what I am going to use.Dambrosio
Partially its working but when user try to paste single char it will failDouty
The character count will also be greater than 1 if the user types a word using predictive text.Kwashiorkor
The character count will also be greater than 1 if you're using an input method like Romaji -> Kanji.Demagogic
H
19

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

Handed answered 19/9, 2012 at 2:30 Comment(0)
H
8

try subclasses UITextview,and override this function.

public override func paste(_ sender: Any?) 
Highsounding answered 10/11, 2017 at 12:40 Comment(2)
Overriding the paste function causes the shouldChangeTextIn delegate method not to fire. How would someone go about implementing both the paste and shouldChangeTextIn functions?Bedbug
Don't forget to call super.Decide
D
7

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

Douty answered 26/6, 2018 at 12:6 Comment(5)
This no longer reliably works on iOS 15.0 since the "Scan Text from Camera" pastes text onto the textfield.Substantialize
FYI: Calling UIPasteboard.general.string in this way causes a prompt to appear "[CurrentApp] pasted from [IncomingApp]" every time the user types.Imply
@StephenEmery what is the solution for this?Nosedive
@RahulVyas, Apologies, I do not remember how I got around this. Also the answer has probably changed with the newer OS version releases.Imply
It's okay I got it resolved by overriding paste method in my custom UITextView subclass. Only thing I'm wondering is it's not asking me the permission popup. Skype also has the same behavior no permission popupNosedive
P
6

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
}
Poulterer answered 30/10, 2019 at 8:30 Comment(1)
When you access the pasteboard, the OS might put up a notification. See the comments here: https://mcmap.net/q/445572/-how-to-know-when-text-is-pasted-into-a-uitextviewDemagogic
B
6

textView(_, shouldChangeTextIn, replacementText) can be called in many cases. if you will read UIPasteboard.general.string value every time, user will see system alert that application accessed pasteboard after every letter typed.

To avoid that problem you can create subclass of UITextView and override paste(_) function in it. This function is being called by system once every time when user tries to paste something.

Use next call of textView(_, shouldChangeTextIn, replacementText) to handle paste event.

var isPastingContent = false // helper variable

open override func paste(_ sender: Any?) {
    isPastingContent = true // next call of `shouldChangeTextIn` will be for the paste action
    super.paste(sender)
}

func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
    if isPastingContent { // if we detected paste event
         // paste detected, do what you want here
         let pasteboardContent = UIPasteboard.general.string // you can get pasteboard content here safely. System alert will be shown only once.
         isPastingContent = false // toggle helper value back to false
    }
}
Baritone answered 18/2, 2021 at 17:36 Comment(2)
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.Misti
'ifPastingContent { ' simply checks if the boolean exists, just so you know..Chromatic
N
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;
}
Neral answered 13/4, 2016 at 8:21 Comment(6)
you forgot to do the change you mention in your posted codeCoronet
Thanks, I didn't notice that.Neral
'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.Peisch
@Peisch Hi, it's been a while that I gave this answer. So, should I put it back to isEqualToString? Best regards.Neral
@Neral 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.Peisch
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.Substantialize
S
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)
Selfassured answered 6/4, 2022 at 16:54 Comment(0)
E
1

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
}
Entertaining answered 13/12, 2021 at 19:39 Comment(1)
Calling UIPasteboard.general.string in this way causes a prompt to appear "[CurrentApp] pasted from [IncomingApp]" every time the user types.Islet
C
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;
}
Counterfactual answered 11/4, 2019 at 12:49 Comment(0)
F
-2

In SWIFT 4:

func textViewDidChange(_ textView: UITextView) {

    if(textView.text == UIPasteboard.general.string)
    {
        //Text pasted
    }
}
Fixture answered 12/12, 2017 at 4:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.