Detect UILabel text change in swift - ios

Is there a way to get notified when there is a change in a UILabel's text or would I be better off using a UITextField with userInteractionEnabled set to false and using its UIControlEditingChanged event to fulfil my purpose?
For ex. I need to run certain lines of code every time I change the UILabel's text and accordingly. So instead of writing those 100 lines of almost similar code for every case I change the UILabel's text, I wish to write it together in one place and call it every time the UILabel is changed. I don't even know if that makes any sense. Forgive me but I cannot expose much of the code.

Create a class that inherits from UILabel. Such as:
class YourLabel: UILabel {
override var text: String? {
didSet {
if let text = text {
println("Text changed.")
} else {
println("Text not changed.")
}
}
}
}
Create a outlet of this class for your object.
#IBOutlet weak var label: YourLabel!
label.text = "abc"

Swift 3
First, add an observer to UILabel for key path text.
label.addObserver(self, forKeyPath: "text", options: [.old, .new], context: nil)
Then
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == "text" {
print("old:", change?[.oldKey])
print("new:", change?[.newKey])
}
}
Swift 2
label.addObserver(self, forKeyPath: "text", options: [.Old, .New], context: nil)
Then
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if keyPath == "text" {
print("old:", change["old"])
print("new:", change["new"])
}
}
Console Output
For example console would be:
old: Optional(<null>)
new: Optional(ABC)

You can do this simply with a subclass of UILabel:
class Label : UILabel {
override var text: String? {
didSet {
print("Text changed from \(oldValue) to \(text)")
}
}
}
oldValue is a special value provided by Swift.
See Property Observers

For Swift 3.2 and later, the preferred method is to use a closure-based observer:
#IBOutlet public weak var label: UILabel!
var textObserver: NSKeyValueObservation?
func someAppropriateFunction() {
...
textObserver = label.observe(\.text) { [weak self] (label, observedChange) in
self?.updateStuff()
}
}
The closure will pass you the label instance and an NSKeyValueObservedChange that includes the following properties:
indexes: IndexSet?
isPrior: Bool
kind: NSKeyValueObservedChange<Value>.Kind
newValue: Value?
oldValue: Value?

Swift 5
If you want to observe text changes in the label and make an animation. You need to create subclass UILabel
class ObservedLabelAnimate: SpringLabel {
override var text: String? {
didSet {
if let text = text {
if oldValue != text {
animation = "fadeInLeft"
duration = 1.2
animate()
}
print("This is oldvalue \(oldValue), and this is the new one \(text)")
}
}
}
}
for this example, I subclass 'SpringLabel' that inherits from UILabel
from https://github.com/MengTo/Spring
Basically it will check if the oldValue and text (new one) is different then the animation will be fired
To use:
#IBOutlet weak var label: ObservedLabelAnimate!

Related

KVO infinit loop with 2 way binding

I have two classes. Class A has Class B, but class B does not know of the existence of class A
Both classes can be altered by external factors (such as services or logic)
But I need to keep both classes synchronized with the same value
As class A knows of class B, I can do a direct assignment after its value is changed
To make the class B know the Class A, I decided to implement KVO, in this way class A is notified when class B is changed
My code looks something like this
class A : NSObject {
var b : B
#objc dynamic var anOtherString:String? {
didSet{
b.someString = self.anOtherString
}
}
override init() {
self.b = B()
super.init()
addObserver(self, forKeyPath: #keyPath(b.someString), options: [.old , .new], context: nil)
}
// MARK: - Key-Value Observing
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(b.someString) {
// Update Time Label
anOtherString = b.someString
}
}
}
class B : NSObject {
#objc dynamic var someString: String?
}
The problem is that my code stays in infinite loop
Because whenever my class B is changed it notifies class A, and when class A is updated it changes again the value of class B that creates a new notification and so on...
I already tried to analyze the Thread.callStackSymbols to detect the cycle and stop it but without success.
Since you cannot allow A to update B when A was changed due to B, you could have a boolean variable that tracks if A needs to update B.
class A : NSObject {
var b : B
var bChanged: Bool = false // Logic to track if B made a change
#objc dynamic var anOtherString:String? {
didSet{
// If B did not make a change, then update B
if !bChanged {
b.someString = self.anOtherString
// Otherwise set the flag
}else {
bChanged = false
}
}
}
override init() {
self.b = B()
super.init()
addObserver(self, forKeyPath: #keyPath(b.someString), options: [.old , .new], context: nil)
}
// MARK: - Key-Value Observing
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(b.someString) {
// Update Time Label
bChanged = true // B made a change
anOtherString = b.someString
}
}
}

Observing a value of a static var in a class?

I have a class with a static var where the current online connection status is stored. I want to observe the value of ConnectionManager.online through other classes. I wanted to do this with KVO, but declaring a static variable as dynamic causes an error:
class ConnectionManager: NSObject {
dynamic static var online = false
// adding 'dynamic' declaration causes error:
// "A declaration cannot be both 'final' and 'dynamic'
}
What is a most elegant way of doing this?
Update. This my code for the KVO part:
override func viewDidLoad() {
super.viewDidLoad()
ConnectionManager.addObserver(
self,
forKeyPath: "online",
options: NSKeyValueObservingOptions(),
context: nil
)
}
override func observeValueForKeyPath(keyPath: String?,
ofObject object: AnyObject?,
change: [String : AnyObject]?,
context: UnsafeMutablePointer<Void>) {
if keyPath == "online" {
print("online status changed to: \(ConnectionManager.online)")
// doesn't get printed on value changes
}
}
As for now, Swift cannot have observable class properties. (In fact, static properties are just global variables with its namespace confined in a class.)
If you want to use KVO, create a shared instance (singleton class) which has online property and add observer to the instance.
I solved it with the singleton pattern suggested by #OOper.
class ConnectionManager: NSObject {
static let sharedInstance = ConnectionManager()
private override init() {} // This prevents others from using the default '()' initializer for this class.
#objc dynamic var online = false
}
Then:
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.tableFooterView = UIView()
ConnectionManager.sharedInstance.addObserver(self,
forKeyPath: "online",
options: [.new, .initial],
context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if object is ConnectionManager && keyPath == "online" {
// ...
}
}
Try replacing dynamic static var online = false to #nonobjc static var online = false
What's happening is that because it inherits from NSObject, Swift is trying to generate getters and setters for it. Because you are creating it in swift, using the #nonobjc attribute solves the problem.
EDIT:
I don't believe you can observe static variables through KVO because of how it works
Here is a link and snippet from Apple's Guide on KVO
Unlike notifications that use NSNotificationCenter, there is no
central object that provides change notification for all observers.
Instead, notifications are sent directly to the observing objects when
changes are made.
Perhaps, instead of using KVO, you could declare online like:
static var online = false {
didSet{
//code to post notification through regular notification center
}
}
If you're set on using it, this question might point you towards the right direction — it'll involve diving deeper into how KVO works: Is it possible to set up KVO notifications for static variables in objective C?
I would suggest property wrapper, I tried the example below and worked perfectly for me:
#propertyWrapper
struct StaticObserver<T> {
private var value:T
init(value:T) {
self.value = value
}
var wrappedValue: T {
get {
// Do your thing
return self.value
}
set {
// Do your thing before set
self.value = newValue
// Do your thing after set
}
}
#StaticObserver(value: false)
dynamic static var online:Bool

Observe property change of class instance

Given an instance of a class, how can I observe a property change?
For e.g., I'm building an SDK that initializes a host app's chat view to provide more functionality with a simple inplementation that looks like:
sdk.initialize(chatView)
In that initializing function, I need to track the host app's chat-view's hidden property so that the SDK's view matches.
A simple KVO example for observing hidden:
class SDKViewController : UIViewController {
private var context = 0
private var observingView: UIView?
func initialize(view: UIView) {
removeObservations()
observingView = view
// start observing changes to hidden property of UIView
observingView?.addObserver(self, forKeyPath: "hidden", options: [.New], context: &context)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
if let newValue = change?[NSKeyValueChangeNewKey] as? Bool where context == &self.context {
print("hidden changed: \(newValue)")
}
}
// this is called by deinit
// it should also be called if they can deregister the view from your SDK
func removeObservations() {
if let view = observingView {
view.removeObserver(self, forKeyPath: "hidden")
observingView = nil
}
}
deinit {
removeObservations()
}
}
This is making some assumptions about your configuration, but if you allow initialization of many views, you can adjust easily.
Also, a lot of this is more concise if you use KVOController by Facebook, which is not in Swift.
Edit: Just to note, hidden does work with KVO.
Edit #2: Updated YourSDKClass to SDKViewController (NSObject -> UIViewController)
Here is an example using protocols
protocol MyClassDelegate:class {
func myClassValueDidChange(newValue:Int)
}
class MyClass {
weak var delegate:MyClassDelegate?
var value = 0 {
didSet {
delegate?.myClassValueDidChange(value)
}
}
}
class ViewController:UIViewController,MyClassDelegate {
let myClass = MyClass()
override func viewDidLoad() {
super.viewDidLoad()
myClass.delegate = self
}
func myClassValueDidChange(newValue:Int) {
//Do something
}
}
You can use Key Value Observing (KVO) to monitor changes to general properties on classes, which includes the hidden property on UIView instances. This is done using addObserver:forKeyPath:options:context: defined in the NSKeyValueObserving protocol.
Note that you can also hide a view by removing it from its superview or by setting its alpha to zero.

Swift: How to make UITextField react when its text isn't set by a user?

I have two textfields and editing one of them results in filling the other one:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) {
if textField == self.mgdlTextField {
if let text = textField.text {
if let value = Double(text) {
self.mmolTextField.text = "\(value / 38.6)"
return
}
}
self.mmolTextField.text = ""
} else {
if let text = textField.text {
if let value = Double(text) {
self.mgdlTextField.text = "\(value * 38.6)"
return
}
}
self.mgdlTextField.text = ""
}
}
return true
}
The problem occurs when I want to retrieve the value of the self-filled text field, I add target:
cell.mmolTextField.addTarget(self, action: "updateLDL1:", forControlEvents: UIControlEvents .EditingChanged)
The action above is not called, I suppose this might be the result of the fact that this was not noticed as a control event.
I tried changing .EditingChanged into .AllEvents but this is not the solution.
How would you approach such issue? Maybe NSNotifications may come in handy?
Thanks in advance
You will need to trigger the event yourself:
self.mmolTextField.sendActionsForControlEvents(.EditingChanged)
I believe you assigned a delegate to your UITextField.
The delegate has methods textFieldDidBeginEditing: and textFieldDidEndEditing: that should help you here. No need to implement other touch controls.
You can use Key Value coding:
var textField: UITextField!
var observer: NSObject!
func textFieldObserverTest() {
if textField == nil {
textField = UITextField()
observer = TextFieldObserver()
textField.addObserver(observer, forKeyPath: "text", options: NSKeyValueObservingOptions.New, context: nil)
}
textField.text = "Hello World"
textField.text = "Goodbye World"
textField.text = "Its all over"
}
class TextFieldObserver : NSObject {
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print("\(keyPath!) changed to \(change![NSKeyValueChangeNewKey])")
}
}
Try using UIControlEventValueChanged
this is for Obj-C find equivalent for swift.

Using property observers on NSManaged vars

I have a var declared in a class like so:
#NSManaged var isFavorite: Bool
I would like to declare a property observer, very similar to the one below.
var organization: String {
didSet { postNotificationWithName( "newData" ) }
}
However, Swift tells me that having property observers on NSManaged vars is not allowed. Is there any way I can implement such a feature or something similar for my isFavorite variable?
Yes-- delete the #NSManaged. It's not absolutely required, but if you delete it you unfortunately need to implement get and set for the property. You would need to add something like
The #objc is only needed if you want to be able to do KVO on the property.
#objc public var newData: String? {
set {
willChangeValue(forKey: "newData")
setPrimitiveValue(newValue, forKey: "newData")
didChangeValue(forKey: "newData")
}
get {
willAccessValue(forKey: "newData")
let text = primitiveValue(forKey: "newData") as? String
didAccessValue(forKey: "newData")
return text
}
}
It's kind of annoying to implement both of these if you don't actually need them but that's the way it is for now.
Since you'll have a set, you might not need a didSet, but you can still add a didSet if you want one.
Whoops! Paul Patterson is right. What you're supposed to use is Key Value Observing - which is exactly what it says you're supposed to do in the link I suggested.
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html
See also swift notes:
https://developer.apple.com/library/mac/documentation/Swift/Conceptual/BuildingCocoaApps/AdoptingCocoaDesignPatterns.html
(use the 'On This Page' menu at the top right of the page for Key-Value Observing)
So something like
objectToObserve.addObserver(self, forKeyPath: "organization", options: .New, context: &myContext)
paired with
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
Override NSManagedObject's func didChangeValue(forKey key: String) see (https://developer.apple.com/documentation/coredata/nsmanagedobject/1506976-didchangevalue)

Resources