UITextField in Swift – text didSet not called when typing - ios

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.

Related

Subscribe to a variable of a custom UIView

I have a custom View, and in this custom View I declared var isSelected: false that is gonna be toggle when taping on the view.
After I add two of those custom Views in my ViewController.
What I need is: When I select one of view, the other one is immediately deselected, so only one can be selected at the same time.
I don't have much knowledge about it, but I assume that with RxCocoa (or ideally RxSwift) it might be possible to set this isSelected variable of each view as an observable, and then in the subscription, set the other one to false once it turns true.
Help will be much appreciated, thank you in advance.
I know what you are asking for seems like a reasonable idea, but it's not. Your isSelected boolean won't change state unless you specifically write code to make it change state. That begs the question, why is other code monitoring your view's isSelected boolean rather than the event that causes the boolean to change state? Why the middle man? Especially a middle-man that is part of your View system, not your Model.
The appropriate solution is to have an Observable in your model that is bound to your two views...
Better would be something like:
class CustomView: UIView {
var isSelected: Bool = false
}
class Example {
let viewA = CustomView()
let viewB = CustomView()
let disposeBag = DisposeBag()
func example(model: Observable<Bool>) {
disposeBag.insert(
model.bind(to: viewA.rx.isSelected),
model.map { !$0 }.bind(to: viewB.rx.isSelected)
)
}
}
extension Reactive where Base: CustomView {
var isSelected: Binder<Bool> {
Binder(base, binding: { view, isSelected in
view.isSelected = isSelected
})
}
}

Is there a callback for setting text in a UITextField in code?

I want to detect if a UITextField's text is set in code. e.g. I need a callback for someTextField.text = "Some Text". I don't want to register the notification for .editingChanged as this will also be called every single time the user types something. It should only be called when the value is set in code.
Is there a way to do this? Any pointers would be greatly appreciated! Thanks!
You can check with the textfields .isEmpty method so
if someTextField.isEmpty {
//do something
}
You can override the text var in UITextField by subclassing
class ObservableTextField: UITextField {
var action : (()->Void)?
override var text: String?{
didSet{
action?()
}
}
}
Use
#IBOutlet weak var obsTextField: ObservableTextField!
override func viewDidLoad() {
super.viewDidLoad()
obsTextField.action = {
debugPrint("changed")
}
}

How to get UITextField cursor change event?

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)

How does one define a var that could be one of two objects?

Real simple, I have two button types that vary slightly. I'm trying to create a variable that can handle both type at a later time in the code. Something like this:
var button: Any
If(condition one)
button = NavButton()
else
button = IconButton()
My buttons have unique functions for setup. As a crude example:
button.setup(name: String)
button.setup(int: Int)
When I call them, the superclass / protocol doesn't recognize them.
How is this done best? Or is this not done using a single variable? I'm building a factory class that is virtually identical in function save ONE call and ONE parameter.
Most obvious way is to declare it as UIButton, assuming both are actual buttons.
var button : UIButton
My solution is what Paulw11 mentioned. I needed to create two unique super functions for each of my parameter types. Probably obvious to most of you, but if not, there's the answer.
I created a protocol, then extended that into a super class with two functions. I think used the super class as an initial type, then changed them in subsequent lines, the gated the unique methods.
protocol MyButton {
func setupString(s: String)
func setupInt(i: Int)
}
public class CustomButton: UIButton, MyButton {
func setupString(s: String) {}
func setupInt(i: Int) {}
}
class ButtonString:CustomButton {
override func setupString(s: String) {}
}
class ButtonInt:CustomButton {
override func setupInt(i: Int) {}
}
Then gated their use like so:
var myButton: CustomButton = CustomButton()
if(string) {
myButton = ButtonString()
myButton.setupString("Hello World")
} else {
myButton = ButtonInt()
myButton.setupInt(42)
}
Hope this helps someone.

iOS adding design-time text on storyboard

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

Resources