69

I need to execute a function when a variable value changes.

I have a singleton class containing a shared variable called labelChange. Values of this variable are taken from another class called Model. I have two VC classes, one of them has a button and a label and the second only a button.

When the button in the first VC class is pressed I am updating the label with this func:

func updateLabel(){
    self.label.text = SharingManager.sharedInstance.labelChange
}

But I want to call the same method whenever the value of the labelChange is changed. So in button click I will only update the labelChange value and when this thing happen I want to update the label with the new value of the labelChange. Also in the second VC I am able to update the labelChange value but I am not able to update the label when this value is changed.

Maybe properties are the solution but can anyone show me how to do so.

Edited second time:

Singleton Class:

class SharingManager {
    func updateLabel() {
        println(labelChange)
        ViewController().label.text = SharingManager.sharedInstance.labelChange     
    }
    var labelChange: String = Model().callElements() {
        willSet {
            updateLabel()
        }
    }
    static let sharedInstance = SharingManager()
}

First VC:

class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!
    @IBAction func Button(sender: UIButton) {    
       SViewController().updateMessageAndDismiss()
    }
}

Second VC:

func updateMessageAndDismiss() {
        SharingManager.sharedInstance.labelChange = modelFromS.callElements()
        self.dismissViewControllerAnimated(true, completion: nil)
    }
@IBAction func b2(sender: UIButton) { 
        updateMessageAndDismiss()
}

I made some improvements but I need to reference a label from the first VC class in singleton. Therefore I will update that label of VC in singleton.

When I print the value of labelChange the value is being updated and everything is fine. But when I try to update that value on label from singleton I receive an error:

unexpectedly found nil while unwrapping an Optional value

and the error is pointing in 4th line of singleton class.

bneely
  • 9,083
  • 4
  • 38
  • 46
Ilir V. Gruda
  • 1,562
  • 5
  • 17
  • 23

6 Answers6

65

You can simply use a property observer for the variable, labelChange, and call the function that you want to call inside didSet (or willSet if you want to call it before it has been set):

class SharingManager {
    var labelChange: String = Model().callElements() {
        didSet {
            updateLabel()
        }
    }
    static let sharedInstance = SharingManager()
}

This is explained in Property Observers.

I'm not sure why this didn't work when you tried it, but if you are having trouble because the function you are trying to call (updateLabel) is in a different class, you could add a variable in the SharingManager class to store the function to call when didSet has been called, which you would set to updateLabel in this case.


Edited:

So if you want to edit a label from the ViewController, you would want to have that updateLabel() function in the ViewController class to update the label, but store that function in the singleton class so it can know which function to call:

class SharingManager {
    static let sharedInstance = SharingManager()
    var updateLabel: (() -> Void)?
    var labelChange: String = Model().callElements() {
        didSet {
            updateLabel?()
        }
    }
}

and then set it in whichever class that you have the function that you want to be called, like (assuming updateLabel is the function that you want to call):

SharingManager.sharedInstance.updateLabel = updateLabel

Of course, you will want to make sure that the view controller that is responsible for that function still exists, so the singleton class can call the function.

If you need to call different functions depending on which view controller is visible, you might want to consider Key-Value Observing to get notifications whenever the value for certain variables change.

Also, you never want to initialize a view controller like that and then immediately set the IBOutlets of the view controller, since IBOutlets don't get initialized until the its view actually get loaded. You need to use an existing view controller object in some way.

Hope this helps.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Dennis
  • 3,506
  • 5
  • 34
  • 42
  • Is there a possibility to add Property Observer in the first VC. Because when I am trying to put it on the first VC nothing is happening is like the Property Observers do not exist. Moreover I am putting Property Observer in singleton class is working but I am not being able to call updateLabel because I am having error in the first VC in update label saying _unexpectedly found nil while unwrapping an Optional value_. – Ilir V. Gruda Jun 04 '15 at 09:29
  • 1
    A property observer needs to be defined with the property's declaration, so no, you would not be able to declare a property in one class and then define a property observer like didSet in a different class. Could you share the parts where you are adding those code and where you are getting the error? – Dennis Jun 04 '15 at 15:59
  • 1
    No problem. In that singleton class, you shouldn't be initializing a new UIViewController every time you want to access the label. That is not going be the same ViewController that you are trying to use. Also, IBOutlets in UIViewControllers won't get initialized until the view has actually been loaded. – Dennis Jun 04 '15 at 16:19
  • I thought that I am not referencing the same label, but do u have any suggestion how can I accomplish that, if yes can you please update that in your code ? – Ilir V. Gruda Jun 04 '15 at 16:26
  • Is it like that the first view controller is always visible (with the label) on the screen and then the other view controllers get added on the screen? You should be referencing the same label from a single view controller object if you are trying to update the label that is visible on the screen. – Dennis Jun 04 '15 at 16:50
  • Yes I am trying to do so but I am not being able to, I am quite new in programming – Ilir V. Gruda Jun 04 '15 at 16:53
  • One more advice, Sorry. Now is working fine for the first VC but how can I call the same function in different VC (to change the label in the first VC). I tried to call it like in the first VC `haringManager.sharedInstance.updateLable = updateLable` but it dose not work. Moreover the function is in the first VC – Ilir V. Gruda Jun 04 '15 at 22:19
  • You don't have to set it from other classes, which is the purpose of storing it in the singleton object from the first view controller before calling them in the other view controllers. As I said before, this would only work if the first view controller still exists when other view controllers are calling the function. – Dennis Jun 04 '15 at 22:34
  • I stored it in singleton because I thought is easy to call it from other VC. The main purpose of all this is to change the label in first VC when I click button on second VC. But without using segue – Ilir V. Gruda Jun 04 '15 at 22:37
  • I was talking about storing the function object in the singleton, which I suggested. Anyways, this should work if you've done it as I've done above and set the value in other view controllers (without replacing the updateLable again). I just tested it myself and it worked without any problem. – Dennis Jun 04 '15 at 22:46
  • [link](http://www.filedropper.com/mvc-test) can you please just have a look in this link if it is OK with you – Ilir V. Gruda Jun 04 '15 at 22:49
  • Actually I just need it to call also in `viewDidLoad ` the function. Thank you a million time – Ilir V. Gruda Jun 04 '15 at 22:59
21

In Swift 4 you can use Key-Value Observation.

label.observe(\.text, changeHandler: { (label, change) in
    // text has changed
})

This is basically it, but there is a little catch. "observe" returns an NSKeyValueObservation object that you need to hold! - when it is deallocated, you’ll receive no more notifications. To avoid that we can assign it to a property which will be retained.

var observer:NSKeyValueObservation?
// then assign the return value of "observe" to it
observer = label.observe(\.text, changeHandler: { (label, change) in
    // text has changed,
})

You can also observe if the the value has changed or has been set for the first time

observer = label.observe(\.text, changeHandler: { (label, change) in
    // just check for the old value in "change" is not Nil
    if let oldValue = change.oldValue {
        print("\(label.text) has changed from \(oldValue) to \(label.text)")
    } else {
        print("\(label.text) is now set")
    }

})

For More Information please consult Apples documentation here

Community
  • 1
  • 1
Andy
  • 866
  • 9
  • 14
11

Apple provide these property declaration type :-

1. Computed Properties:-

In addition to stored properties, classes, structures, and enumerations can define computed properties, which do not actually store a value. Instead, they provide a getter and an optional setter to retrieve and set other properties and values indirectly.

var otherBool:Bool = false
public var enable:Bool {
    get{
        print("i can do editional work when setter set value  ")
        return self.enable
    }
    set(newValue){
        print("i can do editional work when setter set value  ")
        self.otherBool = newValue
    }
}

2. Read-Only Computed Properties:-

A computed property with a getter but no setter is known as a read-only computed property. A read-only computed property always returns a value, and can be accessed through dot syntax, but cannot be set to a different value.

var volume: Double {
    return volume
}

3. Property Observers:-

You have the option to define either or both of these observers on a property:

willSet is called just before the value is stored.
didSet is called immediately after the new value is stored.

public  var totalSteps: Int = 0 {
    willSet(newTotalSteps) {
        print("About to set totalSteps to \(newTotalSteps)")
    }
    didSet {
        if totalSteps > oldValue  {
            print("Added \(totalSteps - oldValue) steps")
        }
    }
}

NOTE:- For More Information go on professional link https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Properties.html

David
  • 3,285
  • 1
  • 37
  • 54
Abhimanyu Daspan
  • 1,095
  • 16
  • 20
6

There is another way of doing so, by using RxSwift:

  1. Add RxSwift and RxCocoa pods into your project

  2. Modify your SharingManager:

    import RxSwift
    
    class SharingManager {
        static let sharedInstance = SharingManager()
    
        private let _labelUpdate = PublishSubject<String>()
        let onUpdateLabel: Observable<String>? // any object can subscribe to text change using this observable
    
        // call this method whenever you need to change text
        func triggerLabelUpdate(newValue: String) {
            _labelUpdate.onNext(newValue)
        }
    
        init() {
            onUpdateLabel = _labelUpdate.shareReplay(1)
        }
    }
    
  3. In your ViewController you can subscribe to value update in two ways:

    a. subscribe to updates, and change label text manually

    // add this ivar somewhere in ViewController
    let disposeBag = DisposeBag()
    
    // put this somewhere in viewDidLoad
    SharingManager.sharedInstance.onUpdateLabel?
        .observeOn(MainScheduler.instance) // make sure we're on main thread
        .subscribeNext { [weak self] newValue in
            // do whatever you need with this string here, like:
            // self?.myLabel.text = newValue
        }
        .addDisposableTo(disposeBag) // for resource management
    

    b. bind updates directly to UILabel

    // add this ivar somewhere in ViewController
    let disposeBag = DisposeBag()
    
    // put this somewhere in viewDidLoad
    SharingManager.sharedInstance.onUpdateLabel?
        .distinctUntilChanged() // only if value has been changed since previous value
        .observeOn(MainScheduler.instance) // do in main thread
        .bindTo(myLabel.rx_text) // will setText: for that label when value changed
        .addDisposableTo(disposeBag) // for resource management
    

And don't forget to import RxCocoa in ViewController.

For triggering event just call

SharingManager.sharedInstance.triggerLabelUpdate("whatever string here")

HERE you can find example project. Just do pod update and run workspace file.

Valerii Lider
  • 1,774
  • 16
  • 21
4
var item = "initial value" {
    didSet { //called when item changes
        print("changed")
    }
    willSet {
        print("about to change")
    }
}
item = "p"
David
  • 3,285
  • 1
  • 37
  • 54
msk_sureshkumar
  • 343
  • 4
  • 10
  • While this may answer the question, it doesn't provide any context to explain how or why. Consider adding a sentence or two to explain your answer. – brandonscript Sep 29 '16 at 19:01
  • 1
    This works good if initial value is some integer or string etc. But how to handle if its an optional? – Satyam Aug 29 '17 at 06:46
1
override var isHighlighted: Bool {
        get { super.isHighlighted }
        set {
            super.isHighlighted = newValue
            if newValue {
                label.textColor = highlightedTextColor
                contentView.backgroundColor = highlightedBackgroundColor
            } else {
                label.textColor = normalTextColor
                contentView.backgroundColor = normalBackgroundColor
            }
        }
    }
Rhm Akbari
  • 362
  • 3
  • 12