Find Delegate in a Swift Array of Delegates

Find delegate in a swift Array of delegates

Update for Swift 4.2:

Assuming that the delegates are actually instances of a class, you could require that in the protocol by "inheriting" from "class":

protocol LocationManagerDelegate: class {
// ...
}

and then use the firstIndex(where:) method, using the "identity operator
===:

class LocationManager: NSObject {
private var _delegates = [LocationManagerDelegate]()

func removeDelegate(delegate:LocationManagerDelegate) {
if let index = _delegates.firstIndex(where: { $0 === delegate }) {
_delegates.remove(at: index)
}
}
}

Old answer (Swift 1):

There are two slightly different contains() functions:

func contains<S : SequenceType where S.Generator.Element : Equatable>(seq: S, x: S.Generator.Element) -> Bool

func contains<S : SequenceType, L : BooleanType>(seq: S, predicate: (S.Generator.Element) -> L) -> Bool

You are using the first one, which requires that the sequence elements conform to
the Equatable protocol, i.e. they can be compared with ==.

Assuming that the delegates are actually instances of a class, you could require
that in the protocol by "inheriting" from "class":

protocol LocationManagerDelegate : class {
// ...
}

and then use the second, predicate-based version of contains() with the
identity operator ===:

func removeDelegate(delegate:LocationManagerDelegate) {
if contains(_delegates, { $0 === delegate }) {
// Remove delegate
}
}

To remove the object from the array you'll have to get its index, so you might use
the findIdenticalObject() function from https://stackoverflow.com/a/25543084/1187415:

func findIdenticalObject<T : AnyObject>(array: [T], value: T) -> Int? {
for (index, elem) in enumerate(array) {
if elem === value {
return index
}
}
return nil
}

and then find and remove from the array with

func removeDelegate(delegate:LocationManagerDelegate) {
if let index = findIdenticalObject(_delegates, delegate) {
_delegates.removeAtIndex(index)
}
}

How to avoid a retain cycle when using an array of delegates in Swift

The problem is that weakDelegates is a strong reference and its reference to its elements of type WeakDelegateContainer is a strong reference.

Your situation is why the class NSHashTable exists. Initialize using weakObjects(). This will give you a set of ARC-weak references, each of which will be nilified and removed when the referenced object goes out of existence (with no need for any extra bookkeeping on your part, and no need for your WeakDelegateContainer type).

Your set will have to be typed as holding AnyObject, but you can easily mediate to ensure that you are supplying and retrieving SomeDelegate-conformant objects:

let list = NSHashTable<AnyObject>.weakObjects()
func addToList(_ obj:SomeDelegate) {
list.add(obj)
}
func retrieveFromList(_ obj:SomeDelegate) -> SomeDelegate? {
if let result = list.member(obj) as? SomeDelegate {
return result
}
return nil
}
func retrieveAllFromList() -> [SomeDelegate] {
return list.allObjects as! [SomeDelegate]
}

The function retrieveAllFromList() lists only objects that still exist. Any object that has gone out existence has been changed to nil in the NSHashTable and is not included in allObjects. That is what I mean by "no extra bookkeeping"; the NSHashTable has already done the bookkeeping.

Here is code that tests it:

func test() {
let c = SomeClass() // adopter of SomeDelegate
self.addToList(c)
if let cc = self.retrieveFromList(c) {
cc.someFunction()
}
print(self.retrieveAllFromList()) // one SomeClass object
delay(1) {
print(self.retrieveAllFromList()) // empty
}
}

Alternatively, you can use NSPointerArray. Its elements are pointer-to-void, which can be a little verbose to use in Swift, but you only have to write your accessor functions once (credit to https://stackoverflow.com/a/33310021/341994):

let parr = NSPointerArray.weakObjects()
func addToArray(_ obj:SomeDelegate) {
let ptr = Unmanaged<AnyObject>.passUnretained(obj).toOpaque()
self.parr.addPointer(ptr)
}
func fetchFromArray(at ix:Int) -> SomeDelegate? {
if let ptr = self.parr.pointer(at:ix) {
let obj = Unmanaged<AnyObject>.fromOpaque(ptr).takeUnretainedValue()
if let del = obj as? SomeDelegate {
return del
}
}
return nil
}

Here is code to test it:

    let c = SomeClass()
self.addToArray(c)
for ix in 0..<self.parr.count {
if let del = self.fetchFromArray(at:ix) {
del.someFunction() // called
}
}
delay(1) {
print(self.parr.count) // 1
for ix in 0..<self.parr.count {
if let del = self.fetchFromArray(at:ix) {
del.someFunction() // not called
}
}
}

Interestingly, after our SomeClass goes out of existence, our array's count remains at 1 — but cycling through it to call someFunction, there is no call to someFunction. That is because the SomeClass pointer in the array has been replaced by nil. Unlike NSHashTable, the array is not automatically purged of its nil elements. They do no harm, because our accessor code has guarded against error, but if you would like to compact the array, here's a trick for doing it (https://stackoverflow.com/a/40274426/341994):

    self.parr.addPointer(nil)
self.parr.compact()

Swift: using delegates to send data to another view controller

You don't need delegates in this case. You are sending data forwards, so just do it like this:

class ViewController: UIViewController {

private var model: Users = ViewController.createAccount()
var exampleDelegate: ExampleDelegate?

@IBAction func showUsers(_ sender: UIButton) {
let showUsersVC = storyboard?.instantiateViewController(identifier: "ThirdViewController") as! ThirdViewController

var userList: Array<User> = model.listOfUsers
showUsersVC.usersList = userList /// pass the data!

present(showUsersVC, animated: true)

}
}

Also in Swift you should lowercase objects like userList, as well as functions like showUsers.

How to remove specific object from array?

  1. Is there a less complicated way to do this?

You could omit self when accessing the delegates member, and bake the resulting index of the index(where:) call into a call to the map method of Optional:

func removeDelegate(_ delegate: MyDelegate) {
delegates.index(where: { $0 == delegate})
.map { delegates.remove(at: $0) }
}

In case no such delegate object is found, the expression above simply results nil (non-captured result).


This conditional, "{ $0 == delegate }", is giving causing the error,
"Cannot convert value of type '(OptionalNilComparisonType) -> Bool' to
expected argument type '() -> Bool'". How can I fix this? I've tried
adding ? and ! but still not fully understanding Swift's optional
concept.

This is yet another example of a kind of obscure error message in Swift. The core error is that MyDelegate does not have an == operator defined (does not conform to Equatable).

After your edit you showed, however, that MyDelegate is a protocol, so if you let this conform to Equatable, you wont be able to (as it will be contain a Self type requirement) use MyDelegate as a concrete type (only as e.g. type constraint on a generic).

If your concrete delegate objects are reference ones (class), and you want to test object equality in the sense of testing if both refer to the same object (object reference), you could make use of the the Object​Identifier available to class instances. Constraining MyDelegate (after your edit you showed it to be a protocol) to only classes,

protocol MyDelegate: class { /* ... */ }

and testing the equality of the ObjectIdentifier in the index(where:) call above:

func removeDelegate(_ delegate: MyDelegate) {
delegates.index(where: { ObjectIdentifier($0) == ObjectIdentifier(delegate) })
.map { delegates.remove(at: $0) }
}

Looking at the source code for ObjectIdentifier, we see that this will compare the underlying raw ptr values of two delegate instances; from swift/stdlib/public/core/ObjectIdentifier.swift:

public static func == (x: ObjectIdentifier, y: ObjectIdentifier) -> Bool {
return Bool(Builtin.cmp_eq_RawPointer(x._value, y._value))
}

As mentioned by @MartinR in the comments to the question above, rather than going via ObjectIdentifier, you could also use the === identity operator for class instances directly.

func removeDelegate(_ delegate: MyDelegate) {
delegates.index(where: { $0 === delegate })
.map { delegates.remove(at: $0) }
}

For completeness, we may verify that === make use of the same Builtin method as the == operator for ObjectIdentifier, by having a look at swift/stdlib/public/core/Equatable.swift:

public func === (lhs: AnyObject?, rhs: AnyObject?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return Bool(Builtin.cmp_eq_RawPointer(
Builtin.bridgeToRawPointer(Builtin.castToUnknownObject(l)),
Builtin.bridgeToRawPointer(Builtin.castToUnknownObject(r))
))
case (nil, nil):
return true
default:
return false
}
}

Passing an array using protocols/delegates

You seem to have confusion around the role of a delegate and where it should be implemented. You don't need to use a delegate to pass data from SendingVC to RecevingVC, you can simply set the property on the destination view controller in prepareForSegue;

class SendingVC: UIViewController {

func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destVC = segue.destinationViewController as? ReceivingVC {
destVC.incomingCarDetails = self.carDetails
}
}
}

If you do want to use a delegate, then you will set the SendingVC instance as the delegate of your ReceivingVC in prepareForSegue and change your protocol so that the delegate method returns data, rather than accepts data:

protocol SenderVCDelegate {
func passArrayData() -> [String]
}

Then, you can implement the delegate method and set the delegate in prepareForSegue

class SendingVC: UIViewController, SenderVCDelegate {

func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let destVC = segue.destinationViewController as? ReceivingVC {
destVC.senderDelegate = self
}
}

func passArrayData() -> [String] {
return self.carDetails
}
}

In ReceivingVC you can call the delegate method in viewWillAppear

class ReceivingVC: UIViewController {

var incomingCarDetails = [String]()
var senderDelegate: SenderVCDelegate?

override func viewWillAppear(animated: bool) {
super.viewWillAppear(animated)

if let incomingDetails = self.senderDelegate?.passArrayData() {
self.incomingCarDetails = incomingDetails
}
}
}

As you can see this is a whole lot more work and no benefit. A delegation pattern is typically used where you want to send data back and where the function call will happen at an unpredictable time, such as in response to a network operation completing or a user interaction.

Not able to get array using delegate

Here is how you can proceed:

  1. Conform VC1 to ImageAssetsProtocol and implement its sendImageAssets(myData:) method.

    class VC1: UIViewController, ImageAssetsProtocol {
    func openVC2() {
    let vc2 = self.storyboard?.instantiateViewController(withIdentifier: "VC3") as! VC3
    vc2.delegate = self
    }

    func sendImageAssets(myData: [MyImageAsset]) {
    print(myData)
    }
    }
  2. In VC2 create a delegate of type ImageAssetsProtocol? and set vc2.delegate = self while presenting VC2 from VC1

    class VC2: UIViewController {
    var delegate: ImageAssetsProtocol?

    func openVC3() {
    let vc3 = self.storyboard?.instantiateViewController(withIdentifier: "VC3") as! VC3
    vc3.delegate = self.delegate
    }
    }
  3. In VC3 create a delegate of type ImageAssetsProtocol? and set vc3.delegate = self while presenting VC3 from VC2. Call the delegate method sendImageAssets(myData:) in viewWillDisappear(_:).

    class VC3: UIViewController {
    weak var delegate: ImageAssetsProtocol?

    override func viewWillDisappear(_ animated: Bool) {
    self.delegate?.sendImageAssets(myData: selectedAssets)
    }
    }

Swift : Create a multi-function multicast delegate

You need to change the signature of invokeDelegates to take a closure of type (MyProtocol) -> (), and then you need to pass each delegate to the closure.

protocol MyProtocol {
func method1()
func method2()
func method3()
}

class TestClass {
var delegates = [MyProtocol]()

func invokeDelegates(delegateMethod: (MyProtocol) -> ()) {
for delegate in delegates {
delegateMethod(delegate)
}
}
}

The closure should just invoke the appropriate delegate method on its argument. Swift can infer the argument and return types of the closure, and you can use the shorthand $0 to refer to the argument, so the closure can be quite short:

let tester = TestClass()
tester.invokeDelegates(delegateMethod: { $0.method1() })

On the other hand, you could just use Collection.forEach directly on the delegates array (if it's accessible) and skip the invokeDelegates method:

tester.delegates.forEach { $0.method1() }


Related Topics



Leave a reply



Submit