Jake Walker Jake Walker - 3 months ago 12
Swift Question

How do I append to a section of an array? [[]]

I have an array of arrays,

theMealIngredients = [[]]


I'm creating a
newMeal
from a current meal and I'm basically copying all checkmarked ingredients into it from the right section and row from a tableview. However, when I use the
append
, it obviously doesn't know which section to go in as the array is multidimensional. It keeps telling me to cast it as an
NSArray
but that isn't what I want to do, I don't think.

The current line I'm using is:

newMeal.theMealIngredients.append((selectedMeal!.theMealIngredients[indexPath.section][indexPath.row])

Answer

You should re-model your data to match its meaning, then extract your tableview from that. That way you can work much more easily on the data without having to fuss with the special needs of displaying the data. From your description, you have a type, Meal that has [Ingredient]:

struct Meal {
    let name: String
    let ingredients: [Ingredient]
}

(None of this has been tested; but it should be pretty close to correct. This is all in Swift 3; Swift 2.2 is quite similar.)

Ingredient has a name and a kind (meat, carbs, etc):

struct Ingredient {
    enum Kind: String {
        case meat
        case carbs
        var name: String { return self.rawValue }
    }
    let kind: Kind
    let name: String
}

Now we can think about things in terms of Meals and Ingredients rather than sections and rows. But of course we need sections and rows for table views. No problem. Add them.

extension Meal {
    // For your headers (these are sorted by name so they have a consistent order)
    var ingredientSectionKinds: [Ingredient.Kind] {
        return ingredients.map { $0.kind }.sorted(by: {$0.name < $1.name})
    }

    // For your rows
    var ingredientSections: [[Ingredient]] {
        return ingredientSectionKinds.map { sectionKind in
            ingredients.filter { $0.kind == sectionKind }
        }
    }
}

Now we can easily grab an ingredient for any given index path, and we can implement your copying requirement based on index paths:

extension Meal {
    init(copyingIngredientsFrom meal: Meal, atIndexPaths indexPaths: [IndexPath]) {
        let sections = meal.ingredientSections
        self.init(ingredients: indexPaths.map { sections[$0.section][$0.row] })
    }
}

Now we can do everything in one line of calling code in the table view controller:

let newMeal = Meal(copyingIngredientsFrom: selectedMeal,
                   atIndexPaths: indexPathsForSelectedRows)

We don't have to worry about which section to put each ingredient into for the copy. We just throw all the ingredients into the Meal and let them be sorted out later.

Some of this is code is very inefficient (it recomputes some things many times). That would be a problem if ingredient lists could be long (but they probably aren't), and can be optimized if needed by caching the results or redesigning the internal implementation details of Meal. But starting with a clear data model keeps the code simple and straightforward rather than getting lost in nested arrays in the calling code.

Multi-dimentional arrays are very challenging to use well in Swift because they're not really multi-dimentional. They're just arrays of arrays. That means every row can have a different number of columns, which is a common source of crashing bugs when people run off the ends of a given row.