Search code examples
arraysswiftnsmutablearray

Swift: subclass NSMutableArray


I'm trying to subclass NSMutableArray in Swift to provide bridge functionality for a Swift array to some old Objective-C codebase. However, I can't find a functional example and all my attempts have failed so far. This is the closest I got to a functional example:

import UIKit

class MyArray: NSMutableArray {

    var array: [Int]?

// Adding an initializer triggers the error: 'required' initializer 'init(arrayLiteral:)' must be provided by subclass of 'NSArray'
//    override init() {
//        self.array = []
//        super.init()
//    }

    static func makeArray() -> MyArray {
        let array = MyArray()
        array.array = []
        return array
    }

    override var count: Int {
        get {
            if let count = array?.count {
                return count
            }

            return 0
        }
    }

    override func insert(_ anObject: Any, at index: Int) {
        print("insert called")
    }

    override func removeObject(at index: Int) {
        print("removeObject called")
    }

    override func add(_ anObject: Any) {
        print("Trying to add \(anObject)")
        if let newInt = anObject as? Int {
            array?.append(newInt)
        }
    }

    override func removeLastObject() {
        print("removeLastObject called")
    }

    override func replaceObject(at index: Int, with anObject: Any) {
        print("replaceObject called")
    }
}

func append42(array: NSMutableArray) {
    array.add(42)
}

let myArray = MyArray.makeArray()

print("Before: \(myArray.array)")
myArray.array?.append(42)
// The following two lines generate a SIGABRT
//myArray.add(42)
//append42(array: myArray)
print("After: \(myArray.array)")

However, using the add method always triggers a SIGABRT. I'm using XCode 12.4 on Catalina.

EDIT (for clarifications): I know that Swift bridges Arrays to NSArrays. However, I need to pass a reference to a Swift array to some Objective-C code accepting an NSMutableArray and possibly making updates to the array.

Essentially I'm adding some Swift code to an existing project, and I'd like to use Swift typed arrays in the new code, but I still to pass these arrays to the old code. I know that I can make an NSMutableArray copy of the Swift array, pass this copy to the old code, then copy it back to the Swift array but it's rather convoluted and hardly maintainable. What I'm tried to do is encapsulating a reference to a Swift array into an 'NSMutableArray' subclass so I can transparently use this subclass with the legacy code.


Solution

  • Are you aware that:

    When importing the Foundation framework, the Swift overlay provides value types for many bridged reference types. Many other types are renamed or nested to clarify relationships.

    And specifically that :

    Class clusters that include immutable and mutable subclasses, like NSArray and NSMutableArray, are bridged to a single value type.

    So you can use Array in place of NSArray and if you want a NSMutableArray is also a Swift Array (but a var). Same thing applies for Dictionary and NSDictionary.

    Disclaimer

    I would probably not use that in production code and although sublassing NSArray and NSMutableArray is not forbidden, documentation says :

    There is typically little reason to subclass NSMutableArray.

    And this should be reason enough to consider another solution IMHO

    Edit

    After reading this answer

    I decided to check NSMutableArray documentation:

    Methods to Override

    NSMutableArray defines five primitive methods:

    insert(_:at:)

    removeObject(at:)

    add(_:)

    removeLastObject()

    replaceObject(at:with:)

    In a subclass, you must override all these methods. You must also override the primitive methods of the NSArray class.

    So I checked NSArray documentation too:

    Any subclass of NSArray must override the primitive instance methods count and object(at:). These methods must operate on the backing store that you provide for the elements of the collection. For this backing store you can use a static array, a standard NSArray object, or some other data type or mechanism. You may also choose to override, partially or fully, any other NSArray method for which you want to provide an alternative implementation.

    and now, with:

    @objc class MyArray: NSMutableArray {
    
        var array: [Int] = []
        static func makeArray() -> MyArray {
            let array = MyArray()
            array.array = []
            return array
        }
    
        override var count: Int {
            array.count
        }
        
        override func object(at index: Int) -> Any {
            array[index]
        }
    
        override func insert(_ anObject: Any, at index: Int) {
            print("insert called")
            if let newInt = anObject as? Int {
                array.insert(newInt, at: index)
            }
        }
    
        override func removeObject(at index: Int) {
            array.remove(at: index)
            print("removeObject called")
        }
    
        override func add(_ anObject: Any) {
            print("Trying to add \(anObject)")
            if let newInt = anObject as? Int {
                array.append(newInt)
            }
        }
    }
    
    func append42(array: NSMutableArray) {
        array.add(42)
    }
    

    I have :

    let myArray = MyArray.makeArray()
    
    print("Before: \(myArray.array)")
    myArray.array.append(42) // [42]
    // The following two lines generate a SIGABRT
    myArray.add(42) // [42, 42]
    append42(array: myArray)
    print("After: \(myArray.array)")
    
    // Before: []
    // Trying to add 42
    // Trying to add 42
    // After: [42, 42, 42]
    
    

    Fun facts

    • I tried your code in a playground and reproduced the crash. I tried in a XCTestCase and it did not crash
    • calling super.add(_:) in add(_:) will trigger insert(_:at:)
    • NSMutableArray conformance to ExpressibleByArrayLiteral is probably written in an extension (which makes perfect sense since NSMutableArray is an Objective-C class) and so overriding init() forces you to override init(arrayLitteral:) and the compiler says Overriding declarations in extensions is not supported. :

    Automatic Initializer Inheritance

    Rule 1

    If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.

    Rule 2

    If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers.