7

I am trying to make an Array extension in Swift 3.1.1 that supports the addition of an object to a certain index in a 2D Array even if the array hasn't been populated yet. The extension should also provide the ability to get an object at certain indexPath. I have the code for this in Swift 2 but I don't seem to be able to migrate it to Swift 3. This is the Swift 2 code:

extension Array where Element: _ArrayProtocol, Element.Iterator.Element: Any {

    mutating func addObject(_ anObject : Element.Iterator.Element, toSubarrayAtIndex idx : Int) {
        while self.count <= idx {
            let newSubArray = Element()
            self.append(newSubArray) 
        }

        var subArray = self[idx]
        subArray.append(anObject)
    }

    func objectAtIndexPath(_ indexPath: IndexPath) -> Any {
        let subArray = self[indexPath.section]
        return subArray[indexPath.row] as Element.Iterator.Element
    }
}

The code is taken from this answer.

Hamish
  • 78,605
  • 19
  • 187
  • 280
Stefan Stefanov
  • 829
  • 7
  • 23
  • Well first of all there is no `_ArrayProtocol` anymore in Swift3. I guess I don't need a `where` clause anymore? Not sure .. – Stefan Stefanov May 26 '17 at 11:54
  • Related: https://swift.org/migration-guide/ (maybe you know this already but if not, it will be of great help) – Eric Aya May 26 '17 at 11:58
  • I am really not sure how to migrate the code to Swift 3. I tried by not using a `where` clause and instead of `Element.Iterator.Element` I simply use `Element` but then when I try to do `let newSubArray = Element()` I get `Element cannot be constructed because it has no accessible initiliaziers` I also tried making an extension of `Collection` instead of `Array` and use `Iterator.Element` instead of `Element` but with no success again. – Stefan Stefanov May 26 '17 at 11:59
  • I just can't get the syntax and everything right somehow .. – Stefan Stefanov May 26 '17 at 11:59
  • This seems important: https://stackoverflow.com/q/43338557/2227743 – Eric Aya May 26 '17 at 12:01
  • I've seen this already. However, that doesn't help me. As I said, when I get rid of the whole `where` clause there are other errors that I'm encountering. – Stefan Stefanov May 26 '17 at 12:16
  • Can you maybe just copy the code and try it on your machine? Then you would see what I mean, because it's hard for me to explain all problems that I encounter. – Stefan Stefanov May 26 '17 at 12:17
  • Can you post your best Swift 3 version (even if it doesn't compile yet)? – Lou Franco May 26 '17 at 12:30
  • Unrelated, but note that the constraint `Element.Iterator.Element: Any` is thoroughly pointless. – Hamish May 26 '17 at 12:44

2 Answers2

6

As Martin says in his answer here, _ArrayProtocol is no longer public in Swift 3.1, therefore meaning that you cannot use it as a constraint in your extension.

A simple alternative in your case is to instead constrain the Array's Element to being a RangeReplaceableCollection – which both defines an init() requirement meaning "empty collection", and an append(_:) method in order to add elements to the collection.

extension Array where Element : RangeReplaceableCollection {

    typealias InnerCollection = Element
    typealias InnerElement = InnerCollection.Iterator.Element

    mutating func fillingAppend(
        _ newElement: InnerElement,
        toSubCollectionAtIndex index: Index) {

        if index >= count {
            append(contentsOf: repeatElement(InnerCollection(), count: index + 1 - count))
        }

        self[index].append(newElement)
    }
}

Note also that we're doing the append as a single call (using append(contentsOf:), ensuring that we only have to resize the outer array at most once.

For your method to get an element from a given IndexPath, you can just constrain the inner element type to being a Collection with an Int Index:

// could also make this an extension on Collection where the outer Index is also an Int.
extension Array where Element : Collection, Element.Index == Int {

    subscript(indexPath indexPath: IndexPath) -> Element.Iterator.Element {
        return self[indexPath.section][indexPath.row]
    }
}

Note that I've made it a subscript rather than a method, as I feel it fits better with Array's API.

You can now simply use these extensions like so:

var arr = [[Int]]()

arr.fillingAppend(6, toSubCollectionAtIndex: 3)
print(arr) // [[], [], [], [6]]

let indexPath = IndexPath(row: 0, section: 3)
print(arr[indexPath: indexPath]) // 6

Although of course if you know the size of the outer array in advance, the fillingAppend(_:toSubCollectionAtIndex:) method is redundant, as you can just create your nested array by saying:

var arr = [[Int]](repeating: [], count: 5)

which will create an [[Int]] array containing 5 empty [Int] elements.

Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Thank you for the great answer! The code worked as a charm and was exactly what I was looking for! Well written and documented answer, learned a lot from it! – Stefan Stefanov May 26 '17 at 14:01
2

There's no need to limit all these ideas to the concrete Array type.

Here's my solution. This discussion was great in that I just learned about RangeReplaceableCollection. Merging (what I think is) the best of both worlds, I pushed all the operations down (up?) the Type hierarchy as far as possible.

Subscript works on much more than Array as @Hamish says. But also, there's no need to constrain the index type, so we have to get rid of IndexPath. We can always sugar this with typealias Index2d = ...

extension Collection where Self.Element: Collection {
    subscript(_ indexTuple: (row: Self.Index, column: Self.Element.Index)) -> Self.Element.Element {
        get {
            return self[indexTuple.row][indexTuple.column]
        }
    }
}

Why not have a mutable version at the most generic possible level (between Collection and RangeReplaceableCollection) (unfortunately I don't think the getter can be inherited when we redefine subscript):

extension MutableCollection where Self.Element: MutableCollection {
    subscript(_ indexTuple: (row: Self.Index, column: Self.Element.Index)) -> Self.Element.Element {
        get {
            return self[indexTuple.row][indexTuple.column]
        }
        set {
            self[indexTuple.row][indexTuple.column] = newValue
        }
    }
}

Then, if you want to initialize lazily, avoid using init:repeatedValue and revise set to have auto-initialization semantics. You can trap bounds overflow and add missing empty elements in both dimensions by integrating the accepted answer's fillingAppend idea.

And when creating a 2D initializer, why not extend the idea of repeating in the natural way:

extension RangeReplaceableCollection where Element: RangeReplaceableCollection {
    init(repeating repeatedVal: Element.Element, extents: (row: Int, column: Int)) {
        let repeatingColumn = Element(repeating: repeatedVal, count: extents.column)
        self.init(repeating: repeatingColumn, count: extents.row)
    }
}

Example Usage:

enum Player {
    case first
    case second
}

class Model {
    let playerGrid: Array<Array<Player>> = {
        var p = [[Player]](repeating: .first, extents: (row: 10, column: 10))
        p[(3, 4)] = .second
        print("Player at 3, 4 is: \(p[(row: 3, column: 4)])")
        return p
    }()
}
BaseZen
  • 8,650
  • 3
  • 35
  • 47