I'm working on Android TV Remote Controller - iOS version
I need to detect cursor change event in UITextField and send this event to Android TV.
I can't find any Delegate or notification will send UITextfield cursor change event.
Is there any way to get this event?
Many thanks.
As far as I know, you can KVO or subclass.
Since #NSLeader gave the answer for KVO, I'll explain the latter.
Here is a subclass example:
class MyUITextFieldThatEmitsCursorChangeEvents: UITextField
{
//Override this, but don't prevent change to its default behavior by
//calling the super getter and setter.
override var selectedTextRange: UITextRange? {
get {return super.selectedTextRange}
set {
self.emitNewlySetCursor(event: newValue) //<- Intercept the value
super.selectedTextRange = newValue //Maintain normal behavior
}
}
//I'm going to use a closure to pass the cursor position out,
//but you can use a protocol, NotificationCenter, or whatever floats your
//boat.
weak var cursorPosnDidChangeEvent: ((Int) -> ())?
//I'm going to abstract the logic here to keep the previous code slim.
private func emitNewlySetCursor(event range: UITextRange?)
{
//Now you have access to the start and end in range.
//If .start and .end are different, then it means text is highlighted.
//If you only care about the position where text is about to be
//entered, then only worry about .start.
//This is an example to calculate the cursor position.
if let rawRangeComponent = range?.start
{
let cursorPosition = offset(from: beginningOfDocument,
to: rawRangeComponent)
//Emit the value to whoever cares about it
self.cursorPosnDidChangeEvent?(cursorPosition)
}
}
}
Then, for example, if we're in a UIViewController:
override func viewDidLoad()
{
super.viewDidLoad()
let tf = MyUITextFieldThatEmitsCursorChangeEvents(frame: .zero)
self.view.addSubview(tf)
tf.cursorPosnDidChangeEvent = { newCursorPosn in
print(newCursorPosn) //( ͡° ͜ʖ ͡°)
}
}
Observe selectedTextRange property.
Example on RxSwift:
textField.rx.observeWeakly(UITextRange.self, "selectedTextRange")
.observeOn(MainScheduler.asyncInstance)
.skip(1)
.bind { (newTextRange) in
print(newTextRange)
}
.disposed(by: disposeBag)
Related
I am writing a custom UITextField right now in Swift, and encountered the following:
class MyField: UITextField {
open override var text: String? {
didSet {
// some logic that normalizes the text
}
}
private func setup() { //called on init
addTarget(self, action: #selector(textEditingChanged(_:)), for: .editingChanged)
}
#objc func textEditingChanged(_ sender: UITextField) {
}
}
Now, when testing this, I observed that, when the user is typing something, textEditingChanged is called, but text.didSet is not. Neither is text.willSet, for that matter. However, in textEditingChanged, the textfield's text will already be updated to the new value.
Can anyone explain what is going on here? Is Swift circumventing the setter on purpose? How am I supposed to know when/if the setter is called, is there any logic to this in UIKit?
The text property of the UITextField is only for external use, when you are typing UIKit handles the update internally. Can't really tell whats going on under the hood, as we do not have access to UIKit implementation but one possible answer to the question is that UITextField is using an other internal variable for storing the text property. When you are getting the text property, you are actually getting that internal value, and when you are setting it, you are setting that internal value also. Of course under the hood it is a bit more complicated but it may look something like this(SampleUITextField represents the UITextField):
// This how the UITextField implementation may look like
class SampleUITextField {
private var internalText: String = ""
var text: String {
get {
internalText
} set {
internalText = newValue
}
}
// This method is just to demonstrate that when you update the internalText didSet is not called
func updateInternalTextWith(text: String) {
internalText = text
}
}
And when you subclass it it looks like:
class YourTextField: SampleUITextField {
override var text: String {
didSet {
print("DidSet called")
}
}
}
Now when you set the text property directly the didSet is called because the text value updates. You can check it:
let someClass = YourTextField()
someClass.text = "Some new text"
// didSet is called
But now when you call updateInternalTextWith the didSet is not called:
let someClass = YourTextField()
someClass.updateInternalTextWith(text: "new text")
// didSet is not called
That's because you are not updating the text value directly, but just the internal text property. A similar method is called to update the internal text variable of the UITextField when you are typing, and that's the reason the didSet is not called in your case.
For that reason, it is not enough to override the text variable when we want to be notified when the text properties changes, and we need to use delegate(UITextFieldDelegate) methods or notifications (textFieldDidChange) to catch the actual change.
What kind of formatting are you trying to perform on the UITextField?
Shouldn't it be the responsibility of a ViewModel (or any model managing the business logic for the view containing this text field) instead?
Also, you shouldn't use didSet to perform changes, its mainly here to allow you to respond to changes, not chain another change.
I'm trying to add accessibility to a simple UISlider. I read the Apples adjustable document and saw that I need to implement two functions from the UIAccessibilityAction protocol; accessibilityIncrement() and accessibilityDecrement().
The problem I'm having is that even if I set the slider to be an accessibility element in viewDidLoad and setting slider.accessibilityTraits = .adjustable, the two override functions aren't called even if I change the values.
I also tried to set slider.accessibilityLabel = "test", but it's still not reading the label. Only how far the slider has come. For instance "80%".
Any idea on how I can make this work? I also read these two posts on stackOverflow, but none of them worked for me. accessibilityIncrement / Decrement not called and Accessibility accessibilityDecrement() not getting called
I can also mention that I also tried setting breakpoints at the accessibilityIncrement() and accessibilityDecrement(), but nothing happened.
My code
override func viewDidLoad() {
super.viewDidLoad()
slider.isAccessibilityElement = true
slider.accessibilityLabel = "test"
slider.accessibilityTraits = .adjustable
}
override func accessibilityIncrement() {
slider.accessibilityValue = textField.text!
}
override func accessibilityDecrement() {
slider.accessibilityValue = textField.text!
}
#IBAction func sliderValueChanged(_ sender: UISlider) {
guard let unit = question?.unit else { return }
let currentValue = Int(sender.value + 0.5)
textField.text = "\(currentValue) \(unit)"
slider.accessibilityLabel = textField.text!
}
You implement the accessibilityIncrement() and accessibilityDecrement() methods in your view controller but they should belong to the created slider whose trait should be .adjustable.
I suggest you take a look at this accessibility site where a complete example about adjustable values with code snippets and illustrations is provided for both ObjC and Swift.
Following this example will allow to call the accessibilityIncrement() and accessibilityDecrement() methods with your slider.
I have a sub class of UITextField and I want the sub class to control which input values are valid. That I have solved by overriding the shouldChangeText(in:, replacementText:) -> Bool method in the sub class. But this is only called when using the on-screen keyboard. If I use a hardware keyboard it doesn't get called.
The textField(_ textField:, shouldChangeCharactersIn:, replacementString:) -> Bool is called on the UITextFieldDelegate, when using a hardware keyboard. But I do not want to assign the delegate to the text field itself, since I need the delegate in some view controllers for other purposes. So I need an alternative method to validate the input values, like the shouldChangeText(in:, replacementText:) -> Bool gives me for the on-screen keyboard.
I can see on the stack trace from the delegate method, that the system has called a [UITextField keyboardInput:shouldInsertText:isMarkedText:], but I can't override that.
Is there any way to solve this, without assigning the delegate?
Here's an alternate solution that allows both your "real" text field delegate and your custom text field class to implement one more delegate methods.
The code below allows this custom text field to implement any of the needed UITextField delegate methods while still allowing the real delegate to implement any that it needs. The only requirement is that for any delegate method implemented inside the custom text field, you must check to see if the real delegate also implements it and call it as needed. Any delegate methods implemented in the real delegate class should be written normally.
This simple custom text field example is setup to only allow numbers. But it leaves the real delegate of the text field to do other validations such as only allowing a certain length of numbers or a specific range or whatever.
import UIKit
class MyTextField: UITextField, UITextFieldDelegate {
private var realDelegate: UITextFieldDelegate?
// Keep track of the text field's real delegate
override var delegate: UITextFieldDelegate? {
get {
return realDelegate
}
set {
realDelegate = newValue
}
}
override init(frame: CGRect) {
super.init(frame: frame)
// Make the text field its own delegate
super.delegate = self
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
// Make the text field its own delegate
super.delegate = self
}
// This is one third of the magic
override func forwardingTarget(for aSelector: Selector!) -> Any? {
if let realDelegate = realDelegate, realDelegate.responds(to: aSelector) {
return realDelegate
} else {
return super.forwardingTarget(for: aSelector)
}
}
// This is another third of the magic
override func responds(to aSelector: Selector!) -> Bool {
if let realDelegate = realDelegate, realDelegate.responds(to: aSelector) {
return true
} else {
return super.responds(to: aSelector)
}
}
// And the last third
// This only allows numbers to be typed into the text field.
// Of course this can be changed to do whatever validation you need in this custom text field
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if string.rangeOfCharacter(from: CharacterSet.decimalDigits.inverted) != nil {
return false // Not a number - fail
} else {
// The string is valid, now let the real delegate decide
if let delegate = realDelegate, delegate.responds(to: #selector(textField(_:shouldChangeCharactersIn:replacementString:))) {
return delegate.textField!(textField, shouldChangeCharactersIn: range, replacementString: string)
} else {
return true
}
}
}
}
The only other complication I leave as an exercise is a case where the custom text field wants to modify the changing text and then allow the real delegate to determine if that modified string/range should be allowed.
I am attempting to have a UIDatePicker come up as a keyboard when the user hits a UIButton. I was able to get it to work with a textfield, but I don't like how the cursor is visible and the user could enter in any text if they had an external keyboard. Here is my code:
#IBAction func dateFieldStart(sender: UITextField) {
var datePickerStartView : UIDatePicker = UIDatePicker()
datePickerStartView.datePickerMode = UIDatePickerMode.Time
sender.inputView = datePickerStartView // error when sender is UIButton
}
I tried changing the sender to UIButton but it gave this error on the line that is marked above:
Cannot assign to 'inputView' in 'sender'
I have tried researching it and no one else seems to have had a problem with it. Anyone know how to trigger a UIDatePicker inputView using a UIButton or anything that might work better that the user cannot type into? Thanks!
This is years after the original question, but for anyone who may be looking for solution to this you can subclass UIButton and provide a getter and setter for the inputView property. Be sure to call becomeFirstResponder in the setter and override canBecomeFirstResponder. For example:
class MyButton: UIButton {
var myView: UIView? = UIView()
var toolBarView: UIView? = UIView()
override var inputView: UIView? {
get {
myView
}
set {
myView = newValue
becomeFirstResponder()
}
}
override var inputAccessoryView: UIView? {
get {
toolBarView
}
set {
toolBarView = newValue
}
}
override var canBecomeFirstResponder: Bool {
true
}
}
let tempInput = UITextField( frame:CGRect.zero )
tempInput.inputView = self.myPickerView // Your picker
self.view.addSubview( tempInput )
tempInput.becomeFirstResponder()
It's a good idea to keep a reference to tempInput so you can clean-up on close
I wanted to do the same thing, I ended up just overlaying a UITextField over the button and using the inputView of that instead.
Tip: set tintColor of the UITextField to UIColor.clearColor() to hide the cursor.
You can create a view for the picker off screen view and move it on screen when you need it. Here's another post on this.
Is it possible to set a design-time text for UILabel (or image if using UIImageView) on iOS 8? if so, how to?
Basically what I need is not to empty all of my labels before compiling so that it doesn't show dummy data before loading from the network the actual data. An algorithm to programatically clear all outlets isn't really a good solution as it is unnecessary code.
You could try subclassing the classes you want to have design-time attributes. Here is an example of that for UILabel:
import UIKit
class UIPrototypeLabel: UILabel {
#IBInspectable var isPrototype: Bool = false
override func awakeFromNib() {
if (isPrototype) {
self.text = "test"
}
}
Then, in IB, you will see isPrototype, and you can set it to true or false.
You can also change the default from false to true in the isPrototype:Bool = false line if you want. You can also change what happens if isPrototype is true. I had it make the text "test" so I could see feedback when testing this out, so you could change it to nil or "" or whatever.
You can also just eschew the isPrototype bool and have this class always reset the text. I just thought the IBInspectable attribute was cool, but if you just want this class to always clear the label text then you would just delete the bool and the check and just self.text=nil every time.
The con to this approach is you need to make all of your labels UIPrototypeLabel to get this functionality.
There is a second, scarier approach, that will add this functionality to all of your UILabels, and that is extending UILabel.
import ObjectiveC
import UIKit
// Declare a global var to produce a unique address as the assoc object handle
var AssociatedObjectHandle: UInt8 = 0
extension UILabel {
#IBInspectable var isPrototype:Bool {
get {
var optionalObject:AnyObject? = objc_getAssociatedObject(self, &AssociatedObjectHandle)
if let object:AnyObject = optionalObject {
return object as! Bool
} else {
return false // default value when uninitialized
}
}
set {
objc_setAssociatedObject(self, &AssociatedObjectHandle, newValue, objc_AssociationPolicy(OBJC_ASSOCIATION_RETAIN_NONATOMIC))
}
}
public override func awakeFromNib() {
if (isPrototype) {
self.text = "test"
}
}
}
credit to Is there a way to set associated objects in Swift? for some of that code