94

I want to enumerate through an array in Swift, and remove certain items. I'm wondering if this is safe to do, and if not, how I'm supposed to achieve this.

Currently, I'd be doing this:

for (index, aString: String) in enumerate(array) {
    //Some of the strings...
    array.removeAtIndex(index)
}
Wain
  • 118,658
  • 15
  • 128
  • 151
Andrew
  • 7,693
  • 11
  • 43
  • 81

9 Answers9

78

In Swift 2 this is quite easy using enumerate and reverse.

var a = [1,2,3,4,5,6]
for (i,num) in a.enumerate().reverse() {
    a.removeAtIndex(i)
}
print(a)
EarlGrey
  • 2,514
  • 4
  • 34
  • 63
Johnston
  • 20,196
  • 18
  • 72
  • 121
  • 2
    Works but filter is really the way to go –  Mar 02 '16 at 16:45
  • 15
    @Mayerz False. "I want to **enumerate** through an array in Swift, and remove certain items." `filter` returns a new array. You are not removing anything from the array. I wouldn't even call `filter` an enumeration. There is always more than one way to skin a cat. – Johnston Mar 02 '16 at 17:36
  • 11
    rigth, my bad! Pla dont skin any cats –  Mar 03 '16 at 07:56
59

You might consider filter way:

var theStrings = ["foo", "bar", "zxy"]

// Filter only strings that begins with "b"
theStrings = theStrings.filter { $0.hasPrefix("b") }

The parameter of filter is just a closure that takes an array type instance (in this case String) and returns a Bool. When the result is true it keeps the element, otherwise the element is filtered out.

Tapani
  • 3,191
  • 1
  • 25
  • 41
Matteo Piombo
  • 6,688
  • 2
  • 25
  • 25
  • 18
    I'd make explicit that `filter` doesn't update the array, it just returns a new one – Antonio Feb 04 '15 at 14:49
  • Parentheses should be deleted; that's a trailing closure. –  Feb 04 '15 at 15:24
  • @Antonio you're right. Indeed that's why I posted it as a safer solution. A different solution could be considered for huge arrays. – Matteo Piombo Feb 04 '15 at 17:21
  • Hm, as you say this returns a new array. Is it possible to make the `filter` method into a `mutating` one then (as I've read the `mutating` keyword enables functions like this to alter `self` instead)? – Gerald Eersteling Jul 06 '15 at 15:23
  • @Gee.E certainly you can add an _in place_ filter as an extension on `Array` marking it as `mutating` and similar to the question's code. Anyway consider that this might not always be an advantage. Anyway every time you remove an object, your array might be _reorganised_ in memory. Thus it could be more efficient allocating a new array and then make an atomic substitution with the result of the filter function. The compiler could do even more optimisations, depending on your code. – Matteo Piombo Jul 06 '15 at 15:56
  • True, up till now there was no problem creating a new one every time. But it's good to know what would happen with a mutating 'in place' filter. Thanks! – Gerald Eersteling Jul 06 '15 at 16:11
  • @Gee.E There's a school of thought which believes that mutable state is the root cause which eventually turns a codebase of sufficient size into a heap of fragile, undebuggable spaghetti. To combat this, functional programming is used where possible, and immutable value types are preferred over mutable reference types. So you'll see a lot of "foo = foo.mutatedCopy()" instead of "foo.mutateInPlace()". See also http://2014.funswiftconf.com/speakers/justin.html – Jason Pepas Jan 19 '16 at 20:18
  • @JasonPepas This is true, got no comment there. I've come too known that with Swift, like in other more scripting languages, explicit is better then implicit too. – Gerald Eersteling Jan 19 '16 at 20:21
46

In Swift 3 and 4, this would be:

With numbers, according to Johnston's answer:

var a = [1,2,3,4,5,6]
for (i,num) in a.enumerated().reversed() {
   a.remove(at: i)
}
print(a)

With strings as the OP's question:

var b = ["a", "b", "c", "d", "e", "f"]

for (i,str) in b.enumerated().reversed()
{
    if str == "c"
    {
        b.remove(at: i)
    }
}
print(b)

However, now in Swift 4.2 or later, there is even a better, faster way that was recommended by Apple in WWDC2018:

var c = ["a", "b", "c", "d", "e", "f"]
c.removeAll(where: {$0 == "c"})
print(c)

This new way has several advantages:

  1. It is faster than implementations with filter.
  2. It does away with the need of reversing arrays.
  3. It removes items in-place, and thus it updates the original array instead of allocating and returning a new array.
jvarela
  • 3,744
  • 1
  • 22
  • 43
14

When an element at a certain index is removed from an array, all subsequent elements will have their position (and index) changed, because they shift back by one position.

So the best way is to navigate the array in reverse order - and in this case I suggest using a traditional for loop:

for var index = array.count - 1; index >= 0; --index {
    if condition {
        array.removeAtIndex(index)
    }
}

However in my opinion the best approach is by using the filter method, as described by @perlfly in his answer.

Antonio
  • 71,651
  • 11
  • 148
  • 165
5

No it's not safe to mutate arrays during enumaration, your code will crash.

If you want to delete only a few objects you can use the filter function.

Starscream
  • 1,128
  • 1
  • 9
  • 22
  • 3
    This is incorrect for Swift. Arrays are **value** types, so they are "copied" when they are passed to functions, assigned to variables, or used in enumeration. (Swift implements copy-on-write functionality for value types, so actual copying is kept to a minimum.) Try the following to verify: var x = [1, 2, 3, 4, 5]; print(x); var i = 0; for v in x { if (v % 2 == 0) { x.remove(at: i) } else { i += 1 } }; print(x) – 404compilernotfound Apr 01 '17 at 20:55
  • Yes you are right, provided that you know exactly what you're doing. Maybe I didn't express my response clearly. I should have said *It's possible but it's not safe*. It's not safe because you're mutating the container size and if you make a mistake in your code, your app will crash. Swift's all about writing safe code that won't unexpectedly crash at runtime. That's why using functionnal programming functions such as `filter` is _safer_. Here's my a dumb example: `var y = [1, 2, 3, 4, 5]; print(y); for (index, value) in y.enumerated() { y.remove(at: index) } print(y)` – Starscream Apr 04 '17 at 07:56
  • 1
    I just wanted to distinguish that it's possible to modify the collection being enumerated in Swift, as opposed to the exception-throwing behavior of doing so when iterating through NSArray with fast enumeration or even C#'s collection types. It's not the modification that would throw an exception here, but the possibility to mismanage indexes and go out of bounds (because they had decreased the size). But I definitely agree with you that it's usually safer and clearer to use the functional programming type of methods to manipulate collections. Especially in Swift. – 404compilernotfound Apr 04 '17 at 17:31
3

Either create a mutable array to store the items to be deleted and then, after the enumeration, remove those items from the original. Or, create a copy of the array (immutable), enumerate that and remove the objects (not by index) from the original while enumerating.

Wain
  • 118,658
  • 15
  • 128
  • 151
2

The traditional for loop could be replaced with a simple while loop, useful if you also need to perform some other operations on each element prior to removal.

var index = array.count-1
while index >= 0 {

     let element = array[index]
     //any operations on element
     array.remove(at: index)

     index -= 1
}
Locutus
  • 444
  • 4
  • 12
1

I recommend to set elements to nil during enumeration, and after completing remove all empty elements using arrays filter() method.

freele
  • 142
  • 7
0

Just to add, if you have multiple arrays and each element in index N of array A is related to the index N of array B, then you can still use the method reversing the enumerated array (like the past answers). But remember that when accessing and deleting the elements of the other arrays, no need to reverse them.

Like so, (one can copy and paste this on Playground)

var a = ["a", "b", "c", "d"]
var b = [1, 2, 3, 4]
var c = ["!", "@", "#", "$"]

// remove c, 3, #

for (index, ch) in a.enumerated().reversed() {
    print("CH: \(ch). INDEX: \(index) | b: \(b[index]) | c: \(c[index])")
    if ch == "c" {
        a.remove(at: index)
        b.remove(at: index)
        c.remove(at: index)
    }
}

print("-----")
print(a) // ["a", "b", "d"]
print(b) // [1, 2, 4]
print(c) // ["!", "@", "$"]
Glenn Posadas
  • 12,555
  • 6
  • 54
  • 95