I really want to be able to detect a paste event in a UITextView, however it appears this cannot be done.
I originally tried subclassing a UITextView and overriding the paste: method, but it never gets called on a paste event.
Has anyone been able to do this? A previous question on the same ilk didn't have an answer back in August...
The text view doesn't catch the paste: event because it wasn't the actual responder is not the text view, but the private web view (UIWebDocumentView) that powers the text view.
However, on paste, the web view will call the text view's (private) -[UITextView keyboardInput:shouldInsertText:isMarkedText:], and in turn, the text view's delegate's -textView:shouldChangeTextInRange:replacementText:.
Therefore, you just need to implement -textView:shouldChangeTextInRange:replacementText: in the text view's delegate.
(Of course, normal keyboard input will trigger this method too. There's no perfect way to distinguish them.)
#KennyTM what I did for one of my applications was keep up with the current text length and the previous text length. If the (currentTextLength - previousTextLength) was greater than 1, then the user must have pasted something
With iOS 14 you have to do this in two parts to avoid showing the user notification that you are checking the UIPasteboard. In my case I did not want to do anything bad with the user data but I did want to do some special formating when the user did paste into the UITextView.
Step 1: Create a custom UITextView and override paste()
import UIKit
protocol TouchableTextViewDelegate : class{
func touchesDidBegin()
func pasting()
}
class TouchableTextView: UITextView {
weak var touchableDelegate : TouchableTextViewDelegate?
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.isFirstResponder{
return true
}
touchableDelegate?.touchesDidBegin()
return false
}
override func paste(_ sender: Any?) {
touchableDelegate?.pasting()
super.paste(sender)
}
}
Step 2: In the file location where you handle the shouldChangeTextIn create a variable and be sure to set the delegate for the TouchableTextView. In my case
//top of the view
var isPasting : Bool = false
//also when creating UITextView use both delegates
textView.touchableDelegate = self
//add the normal delegate
textView.delegate = self
extension SliderTextView : TouchableTextViewDelegate{
func pasting() {
self.isPaste = true
}
func touchesDidBegin() {
sliderEditingDelegate?.touchesDidBegin(sliderTextView: self)
}
}
Step 3: Inside shouldChangeTextIn I handle the action like this
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
let isPaste = self.isPaste
//be sure to set this to false
self.isPaste = false
if isPaste,
let pt = UIPasteboard.general.string,
text.contains(pt){
//you will see the paste notification and that is good for the user
// but only when the user pastes
// do whatever special thing or formatting you want to do
}
return true
}
The good is that you will not trigger the notification unless the user is pasting in the UITextView.
To detect if a user is parsing a text in a textView, compare the replacementText in the shouldChangeTextInRange delegate with the text the user is currently holding in the UIPasteboard. Then take action depending on requirements.
for code, see my answer in the following question:
how to know when text is pasted into UITextView
Related
(This is a revised question - including answer - following on from macOS: Take emoji from characterPalette which describes the problems encountered in more detail)
Background/use case
I have an app where, instead of creating and maintaining an icon library, I let users type an emoji as a placeholder graphic. This works beautifully within the context of my app, but I am not happy with the input mechanism I use.
Problem
I would like to simplify this so I open the characterPalette, select an emoji, and display it either as the button's StringValue or in a Label (=non-editable NSTextField).
This does not seem possible. Unlike NSColorPanel or NSFontPanel, the characterPanel is not exposed to the Cocoa framework, so I cannot take its selectedValue, set its action, or catch a notification. The documentation for orderFrontCharacterPalette simply says Opens the character palette which ... is not helpful.
Attempted solutions and problems encountered
I tried to work with making my receiver the firstResponder, but unlike NSTextView, NSTextField cannot process emoji. I found a workaround using an NSTextView with an NSBox in front, making it the firstResponder, and using NSApp.orderFrontCharacterPalette(sender)but found that under various circumstances which all seem to involve an extra drawing call – setting the button's title, showing a label in SystemFont Mini size (regular size worked fine) the CharacterPalette will open (=the system menu now offers 'Hide Emoji & Symbols') without being displayed. (This persists until the application closes, even if you try to open the CharacterPalette through the regular menu/shortcut)
For the partial solution involving NSTextInputClient (the no-show seems to be a persistent bug), see answer below.
The emoji picker needs a minimal implementation of NSTextInputClient. For example a button:
class MyButton: NSButton, NSTextInputClient {
override var acceptsFirstResponder: Bool {
get {
return true
}
}
override func becomeFirstResponder() -> Bool {
return true
}
override func resignFirstResponder() -> Bool {
return true
}
func insertText(_ string: Any, replacementRange: NSRange) {
// this method is called when the user selects an emoji
if let string = string as? String {
self.title = string
}
}
func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
}
func unmarkText() {
}
func selectedRange() -> NSRange {
return NSMakeRange(0, 0)
}
func markedRange() -> NSRange {
return NSMakeRange(NSNotFound, 0)
}
func hasMarkedText() -> Bool {
return false
}
func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
return nil
}
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
return []
}
func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
// the emoji picker uses the returned rect to position itself
var rect = self.bounds
rect.origin.x = NSMidX(rect)
rect.size.width = 0
return self.window!.convertToScreen(self.convert(rect, to:nil))
}
func characterIndex(for point: NSPoint) -> Int {
return 0
}
}
NSTextInputClient needs a NSTextInputContext. NSView returns a context from inputContext if the class conforms to NSTextInputClient unless isEditable is implemented and returns false. A label doesn't return a NSTextInputContext, the solution is to override inputContext:
class MyTextField: NSTextField, NSTextInputClient {
var myInputContext : NSTextInputContext?
override var inputContext: NSTextInputContext? {
get {
if myInputContext == nil {
myInputContext = NSTextInputContext(client:self)
}
return myInputContext
}
}
// and the same methods as the button, set self.stringValue instead of self.title in insertText(_:replacementRange:)
}
Willeke pointed me at NSTextInputClient which has provided the best solution so far. Apple's only example is in ObjectiveC, convoluted, and overly complex for what I was trying to do, so I am reproducing my code here.
Caveat: this is not a full implementation of NSTextInputClient, just enough to capture emoji input
I have created an NSButton subclass:
class TextReceiverButton: NSButton, NSTextInputClient {
//specific methods
func setButtonTitle(_ string: String?){
self.title = string ?? 🦊
}
//NSTextInputClient methods
func insertText(_ string: Any, replacementRange: NSRange) {
let receivedText = string as? String
setButtonTitle(receivedText)
}
func validAttributesForMarkedText() -> [NSAttributedString.Key] {
return [.font, .paragraphStyle, .writingDirection]
}
//Omitted: For anything else that wants a value, I return NSMakeRange(0, 0)/NSRect.zero or 0 as well as false for marked text and nil for attributed substring
}
(If you add the protocol to your class, it will offer stubs for the other methods)
The full set for NSAttributedString.Key is
[.font, .foregroundColor, .glyphInfo, .kern, .ligature, .link, .markedClauseSegment, .obliqueness, .paragraphStyle, .shadow, .spellingState, .strikethroughColor, .strikethroughStyle, .strokeColor, .strokeWidth, .superscript, .textAlternatives, .textEffect, .toolTip, .underlineColor, .underlineStyle, .verticalGlyphForm, .writingDirection]
(I have tested the short form with simple and composite emoji and nothing else seems necessary.)
The button's action is
#IBAction func displayEmojiInButton(_ sender: Any) {
NSApp.orderFrontCharacterPalette(self)
view.window?.makeFirstResponder(textReceiverButton)
}
Problems/Bugs
The NSTextInputClient document says 'you can subclass NSView' and Apple's code turns an NSView into a fully functional (receiving and drawing) text view class (I can't built it, but I assume it worked). So theoretically, you should be able to use the same code for NSTextField, which also ultimately inherits from NSView.
However, it turns out that NSTextField displays the 'CharacterPalette allegedly opens but never displays' bug I talked about earlier; though it does work with NSView. (I have not tested this further).
Furthermore, NSTextInputClient is not a complete replacement for NSTextView: it does not receive input from the keyboard viewer. (See Willecke's answer/comment for explanation/solution to these).
Verdict
NSApp.orderFrontCharacterPalette(self) fails 95% of the time when called from a view in the vincinity of a tab view (in splitView next to TabViewController, embedded in TabViewController), so while this code may be correct, it's also useless a lot of the time, at least under 10.13.
How to let user insert only one decimal in the textfield in language Swift.There are quite a lot of help for this on the site,but none of them work as they are all Swift 1 code. Thank you in advance.
First, you should make your class conform the to the UITextFieldDelegate protocol like this:
class YourViewController: UIViewController, UITextFieldDelegate
Then, you should set the delegate of your textField to self:
yourTextField.delegate = self
Finally, whenever the content of your UITextField changes, this delegate method will be called, so add it to your view controller.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
//handle changes here
}
If you want only one character to appear you could disallow changes to the content of your UITextField when it is already filled:
if yourTextField.text?.characters.count > 0 {
return false
}
return true
If you only want to allow decimal characters you could check for this too, and add the condition to the if query.
How would I make it so a button that uses the segue to send you to the next view controller not work if nothing is entered in the text field above it?
Add this to the viewdidload, replacing textfieldVerb with the name of your textbox, and nextVerbOutlet with the name of your button (as a outlet)
self.textFieldVerb.addTarget(self, action: "textFieldChanged:", forControlEvents: .EditingChanged)
self.textFieldVerb.addTarget(self, action: "textFieldChanged:", forControlEvents: .EditingChanged)
nextVerbOutlet.enabled = false
and then add this replacing textfieldVerb with the name of your textbox, and nextVerbOutlet with the name of your button (as a outlet). This doesn't go in the viewdidload, but under it.
func textFieldChanged(sender: UITextField) {
// simple validation
if textFieldVerb.text?.characters.count > 0
&& textFieldVerb.text?.characters.count > 0 {
self.nextVerbOutlet.enabled = true // re-enable your button
}
}
You could implement optional func textField(_ textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool of the UITextFieldDelegate. It will get triggered every time user enters or deletes character in the text field. You can examine the contents there and enable/disable your button from there appropriately.
You could check for the content of the TextField:
if myTF.text != "" || myTF.text != nil {
//TextField contains something
//enable button
//segue to next ViewController
} else {
//TextField empty
//disable button
}
If you implement that into textFieldDidEndEditing() it will check each time the user is done typing.
Make sure to include the UITextFieldDelegate in your class like:
class myClass: UIViewController, UITextFieldDelegate {...}
Hope that helps :)
Implement the UITextFieldDelegate protocol and set your VC as the delegate.
Then implement the
func textField(_ textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String)
method of that protocol.
You use this method to detect an empty string. You can choose to be thorough and handle copy/paste in which case you will need to calculate the value of the new string after the replacement using stringByReplacingCharactersInRange, or you can choose to not handle copy paste and just check that range.location > 0.
Based upon the the above you can set the enabled property of the button accordingly.
The textFieldShouldReturn function is not being called at all: there are no errors but the keyboard does not respond at all.
My case is different from How to hide keyboard in swift on pressing return key? as in my case nothing is happening at all and other cases are in Objective-C.
Here is my code:
import UIKit
class ViewController: UIViewController {
#IBOutlet var textField: UITextField!
func textFieldShouldReturn(textField: UITextField) -> Bool {
resignFirstResponder()
return true
}
}
textField is an outlet to a text field on my storyboard. I also tried self.endEditing instead of resignFirstResponder.
The rest of this answer is still very useful, and I'll leave it there as it can potentially help other askers... but here, I missed the obvious problem with this specific example...
We're not calling resignFirstResponder on the text field. We're calling it on the view controller. We need to call it on the text field, so modify your code to look like this:
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
A UITextField will only call the textFieldShouldReturn property on the object which is its delegate.
We can fix this programmatically by adding a viewDidLoad method to set that:
override func viewDidLoad() {
super.viewDidLoad()
self.textField.delegate = self
}
But we can also set this up via the storyboard at build time.
Right click on the textfield to check and see whether or not the delegate has been set:
If that circle next to delegate is unfilled, we haven't set the delegate for our UITextField yet.
To set the delegate, hover over this circle. It will change to a plus sign. Now click and drag to the view controller that you want to delegate the text field (the view controller the text field is part of).
When you've appropriately hooked the view controller up as a delegate, this menu should look like this:
If using Swift 3+, you have to add an underscore before the first property. Like:
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
This is also very well documented in the Apple Documentation.
https://developer.apple.com/documentation/uikit/uitextfielddelegate/1619603-textfieldshouldreturn
I'm enrolled in a Swift 4 Udemy course, and the instructor said to add the UITextFieldDelegate class for the ViewController in addition to Cntrl - dragging from the textField to the ViewController button and selecting delegate.
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
Well, in my case. I accidentally enable hardware keyboard. Make sure you unchecked "Connect to hardware keyboard" in order for the keyboard to show up in the simulator.
Hardware -> Keyboard -> Connect to hardware keyboard
Hope this will help others too!
You can set all textFields delegate in a loop:
var tF: [UITextField] = []
tf = [my1TextField, my2TextField, my3TextField]
for textField in tf {
textField.delegate = self
}
This problem has been driving me crazy. It seems no matter what I try I cannot get the keyboard to hide once it is shown in my simple Swift program.
It does not work when I resignFirstResponder() in textFieldShouldReturn nor does it work when handing a background touch by calling endEditing() from touchesBegan(...). I can see these respective methods are all being called when I set debugger break points to them, so the delegate is properly set and being called as expected.
Here are the specific steps I took:
Create a new single-view application Swift project
Drag a UITextField onto the view, wire up the IBOutlet, and set the ViewController up as UITextFieldDelegate.
Run in iPhone 6 Simulator or on an iPhone 6 device
Once the keyboard is presented, it is never dismissed!!
I am out of ideas - what am I missing???
Here is the entire contents of my ViewController:
import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var myTextField: UITextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
myTextField.delegate = self
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
self.view.endEditing(true)
}
func textFieldDidBeginEditing(textField: UITextField!) {
return
}
func textFieldShouldEndEditing(textField: UITextField!) -> Bool {
return false
}
func textFieldShouldReturn(textField: UITextField!) -> Bool {
textField.resignFirstResponder()
return true
}
}
You are returning 'false' from 'textFieldShouldEndEditing'. The following is an excerpt from the 'UITextFieldDelegate' documentation:
Return Value
YES if editing should stop; otherwise, NO if the editing session
should continue
Discussion
This method is called when the text field is asked to resign the first
responder status. This might occur when your application asks the text
field to resign focus or when the user tries to change the editing
focus to another control. Before the focus actually changes, however,
the text field calls this method to give your delegate a chance to
decide whether it should.
Normally, you would return YES from this method to allow the text
field to resign the first responder status. You might return NO,
however, in cases where your delegate detects invalid contents in the
text field. By returning NO, you could prevent the user from switching
to another control until the text field contained a valid value.
Source
So, either return 'true' from it or remove the method completely, unless you really need to do something useful inside it. 'textFieldDidBeginEditing'and 'touchesBegan' should also probably be removed. I'm really surprised that the same code worked in Objective-C.