Search code examples
swiftvariableslistenerstatusdidset

Variable listener in iOS Swift


I am trying to make an independent class/library that runs on its own and handles errors internally, but also gives feedback (status) to the frontend for possible user interaction. In Android I'd solve this with a Listener:

    class Status(errCode:Int=0, msg:String=""){
    var errCode:Int=0
    var text:String=""
    private var listener: ChangeListener? = null

    init {
        this.errCode=errCode
        this.text=msg
    }

    fun set(errCode:Int, msg:String) {
        this.errCode=errCode
        this.text=msg
        if (listener != null) listener!!.onChange()
    }

    fun getListener(): ChangeListener? {
        return listener
    }

    fun setListener(listener: ChangeListener?) {
        this.listener = listener
    }

    interface ChangeListener {
        fun onChange()
    }
}

Whenever I want to update it, e.g. on Exception, I call:

catch (e: IOException) {
            this.status.set(109,"someException: $e")
        }

And in the MainActivity I just have to handle these changes:

myObj.status.setListener(object : Status.ChangeListener {
            override fun onChange() {
                when(myObj!!.status.errCode) {
                    //errors
                    109 -> log(myObj!!.status.text) //some Exception
                    111 -> myObj!!.restart() //another exc
                    112 -> myObj!!.checkAll() //some other error
...

In Swift I found didSet that seems similar. I read you can attach it to a struct so whenever something in the struct changes, it is called. So I added my "struct Status" to my "struct myObj" like this:

struct myObj {
var status:Status=Status(errCode: 0, msg: "Init")

struct Status {
    var errCode:Int
    var msg:String
    
    mutating func set(err:Int, txt:String){
        errCode=err
        msg=txt
    }
}

and in my code I initialize a new instance of myObj

var mObj:myObj=myObj.init() {
        didSet{
            switch myObj.status.errCode{
            case 1:
                promptOkay()
            case 113:
                obj.errorHandling()
            default:
                log(txt: mObj.status.msg)
            }
        }
    }

However, it never trigger didSet, even though internal functions should change the Status. I read that didSet is not triggered on init, but I do not run anything right now after initializing the class object that should run quite independently. I want to verify that the approach is okay before I go further and have to unravel everything again.


Solution

  • didSet must be declared on the property, not during initialization:

    class MyObj {
        var status: Status = Status(errCode: 0, msg: "Init") {
            didSet {
                // status did change
                print("new error code: \(status.errCode)")
            }
        }
        
        struct Status {
            var errCode:Int
            var msg:String
            
            mutating func set(err:Int, txt:String){
                errCode = err
                msg = txt
            }
        }
    }
    
    let obj = MyObj()
    obj.status.set(err: 10, txt: "error 10") // prints "new error code: 10"
    

    At this point you can react to every changes made to obj.status in the didSetClosure.

    Edit — React to didSet from outside the class

    If you want to respond to changes from outside the MyObj, I would recommend using a closure:

    class MyObj {
        var status: Status = Status(errCode: 0, msg: "Init") {
            didSet {
                statusDidChange?(status)
            }
        }
        
        // closure to call when status changed
        var statusDidChange: ((Status) -> Void)? = nil
        
        struct Status {
            var errCode:Int
            var msg:String
            
            mutating func set(err:Int, txt:String){
                errCode = err
                msg = txt
            }
        }
    }
    

    This way, you can assign a closure from outside to perform custom actions:

    let obj = MyObj()
    obj.statusDidChange = { status in
        // status did change
        print("new error code: \(status.errCode)")
    }
    
    obj.status.set(err: 10, txt: "error 10") // prints "new error code: 10"
    

    Edit 2 — Call didSet closure directly from initialization

    You also can manually call the statusDidChange closure during the init.

    class MyObj {
        var status: Status = Status(errCode: 0, msg: "Init") {
            didSet {
                statusDidChange(status)
            }
        }
        
        // closure to call when status changed
        var statusDidChange: (Status) -> Void
        
        init(status: Status, statusDidChange: @escaping (Status) -> Void) {
            self.status = status
            self.statusDidChange = statusDidChange
            self.statusDidChange(status)
        }
    }
    
    let obj = MyObj(status: MyObj.Status(errCode: 9, msg: "error 09")) { status in
        // status did change
        print("new error code: \(status.errCode)")
    }
    
    obj.status.set(err: 10, txt: "error 10")
    

    This will print

    new error code: 9
    new error code: 10