Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
186 views
in Technique[技术] by (71.8m points)

swift - How automatically remove callback from callbacks Array, when it has not caller any more?

I have these classes:

class Callback {
    let callback: () -> Void
    init(callback: @escaping () -> Void) {
        self.callback = callback
    }
}

class CallbackContainer {
    private var callBacks = [Callback]()
    
    func add(callback: @escaping () -> Void) -> Callback {
        let cl = Callback(callback: callback)
        callBacks.append(cl)
        return cl
    }
    
    func callAll() {
        for callback in callBacks {
            callback.callback()
        }
    }
}

class Container {
    let callbackContainer = CallbackContainer()
    
    func executeSomeLongTasks() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 5) {[weak self] in
            self?.callbackContainer.callAll()
        }
    }
}

class AViewController: UIViewController {
    var callback: Callback?
    let container: CallbackContainer
    init(container: CallbackContainer) {
        self.container = container
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        callback = container.add {
            debugPrint("Called callback on A")
        }
    }
    
    deinit {
        debugPrint("Deinited AViewController")
    }
}

I want the CallbackContainer to remove the callbacks automatically when nobody is calling them any more. For example:

let container = Container()

var aVC: AViewController? = AViewController(container: container.callbackContainer)

//on somewhere executed long task
container.executeSomeLongTasks()
aVC = nil
// should not call callback here

Here, when aVC is released - like aVC = nil or popping it from navigation stack - then the callback stored in callbackContainer (created on AViewController viewDidload()), should be removed from the internal array automatically, without the need of removing it on AViewController deinit() manually. Hence, the callback will not be called any more from CallbackContainer.callAll()

It should work like the DisposeBag on RxSwift. I saw DisposeBag code, but did not understand.

Someone has any idea?

question from:https://stackoverflow.com/questions/66062491/how-automatically-remove-callback-from-callbacks-array-when-it-has-not-caller-a

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

What you need is a kind of weak array. As far as I know, there is no such type in the Swift foundation classes: My first thought was to use NSMapTable or something similar with weakToStrongObjects configuration, which is documented to store the keys as weak references, but unfortunately that class does not reorganize itself automatically, see the discussion part:

Use of weak-to-strong map tables is not recommended. The strong values for weak keys > which get zeroed out continue to be maintained until the map table resizes itself.

What you could do is:

  • Create a boxing object that stores a weak reference and the closure
  • Add that box into the callBacks array.
  • When calling callAll, first clean up the callBacks array to remove all boxes with nil references.

That manual cleanup is a drawback, because you will not be informed when a weak reference is nil-led by ARC. But it is done centrally by the Container, so the clients do not need to take care.

For example:

typealias CallbackType = () -> Void

class WeakCallback : Equatable {
    let id = UUID()
    
    weak var refItem: AnyObject?
    let callback:CallbackType
    
    init(refItem: AnyObject, callback:@escaping CallbackType) {
        self.refItem = refItem
        self.callback = callback
    }
    
    static func == (lhs: WeakCallback, rhs: WeakCallback) -> Bool {
        return lhs.id == rhs.id
    }
}


class CallbackContainer {
    var callBacks = [WeakCallback]()
    
    func add(ref:AnyObject, callback: @escaping () -> Void) -> WeakCallback {
        cleanupCallbackArray()
        let cl = WeakCallback(refItem:ref, callback: callback)
        callBacks.append(cl)
        return cl
    }
    
    func callAll() {
        cleanupCallbackArray()
        for cb in callBacks {
            if (cb.refItem != nil) {
                cb.callback()
            } else {
                print ("oops, found nil for (cb.id)")
            }
        }
    }
    func cleanupCallbackArray() {
        callBacks = callBacks.filter { cb -> Bool in
            if (cb.refItem == nil) {
                print ("(cb.id) has nil reference, will be removed")
                return false
            }
            return true
        }
    }
}

And then, in viewDidLoad, add the view controller as a reference to the container:

    override func viewDidLoad() {
        super.viewDidLoad()
        callback = container.add (ref:self) {
            debugPrint("Called callback on A")
        }
    }

To test:

let cbContainer = CallbackContainer()

var aVC: AViewController? = AViewController(container: cbContainer)
aVC?.viewDidLoad()
print ("callAll 1")
cbContainer.callAll()
print ("set to nil")
aVC = nil
print ("callAll 2")
cbContainer.callAll()

gives:

callAll 1

"Called callback on A"

set to nil

"Deinited AViewController"

callAll 2

81C3D375-4B0E-4806-8088-AFE81C3125F3 has nil reference, will be removed

As you can see, the closure will not be called upon "callAll 2".


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...