Reputation: 1465
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 Array
s to NSArray
s. 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.
Upvotes: 1
Views: 737
Reputation: 437442
We should generally follow the Open-Closed Principle, OCP, namely that classes are open for extension and closed for modification. This attempt to subclass NSMutableArray
, and replace a few key methods, violates OCP. It is likely to manifest all sorts of weird behaviors.
If you really want to subclass, you will want to override all of the primitive methods of both NSArray
and NSMutableArray
. Theoretically you could examine your Objective-C code and see what methods it is calling, but this is a very fragile approach. I would not advise this pattern.
Personally, if I was trying to integrate with some Objective-C code that required NSMutableArray
, I would stick with Swift arrays and create a NSMutableArray
before I called my Objective-C library, and then copy it back to an array (consistent with the value semantics of Swift arrays).
Consider:
@implementation Foo
- (void)bar:(NSMutableArray *)array {
[array addObject:@42];
}
@end
I would shim this for a Swift Array
:
extension Foo {
func bar(_ values: inout [Int]) {
let array = NSMutableArray(array: values)
bar(array)
values = array as! [Int]
}
}
Then I could do:
var values = [1, 2]
values.append(3)
let foo = Foo()
foo.bar(&values)
print(values) // 1, 2, 3, 42
But I would avoid avoid subclassing NSMutableArray
simply because I happened to be calling some Objective-C routine that used NSMutableArray
. I would only do that if benchmarking (or other considerations) deemed that absolutely necessary.
Let us assume for a second that you absolutely needed to stick with NSMutableArray
(e.g., you had millions of records and are calling this Objective-C routine constantly). In that case, I still would not advise subclassing NSMutableArray
, but rather just use NSMutableArray
directly in my Swift code. If I wanted to retrieve Int
values from my NSMutableArray
and didn't want to cast all over the place, I'd add an extension
to NSMutableArray
:
extension NSMutableArray {
func append(integer value: Int) {
add(value)
}
func intValue(at index: Int) -> Int {
return self[index] as! Int
}
}
Then you could do:
let values: NSMutableArray = [1, 2]
values.append(integer: 3)
let foo = Foo()
foo.bar(values)
print(values) // 1, 2, 3, 42
let value = values.intValue(at: 3)
print(value) // This is `Int` value of 42
Now, I know this is not the answer you were hoping for, namely that you did not want to “lose the benefits of Swift typed Array
s” while still using this Objective-C legacy code. But at least these two methods give you type-safe setting and retrieval of integer values.
Upvotes: 1
Reputation: 2661
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
.
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
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]
XCTestCase
and it did not crashsuper.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.
Upvotes: 1