How to update an array with WritableKeyPath in Swift 4.0

In Swift 4.0, I have an array of structs. Is there a way to use keyPaths to update all items in the array without using manually iterating like map or forEach? Something similar to objc [people makeObjectsPerformSelector: @selector(setName:) withObject: @"updated"];

struct Person {
    var name: String? = "Empty"
}
var people = [Person(), Person()]

//This only updates one person:
people[keyPath: \[Person].[0].name] = "single update"

//I'm looking to accomplish something like this without a map
let updatedPeople = people.map { (person: Person) -> Person in
    var copy = person
    copy[keyPath: \Person.name] = "updated"
    return copy
}

something like people[keyPath: \[People].all.name] = "update all without manually iterating"

2 answers

  • answered 2018-02-13 01:09 Alexander

    Mutating into a member of an array requires an l-value. Swift's mechanism for l-values is the subscript, so we can use that:

    for i in people.indices {
        people[i][keyPath: \Person.name] = updated
        // or more simply, just:
        // people[i].name = "updated"
    
        // This even works too, but I can't see any reason why it would be desirable
        // over the other 2 approaches:
        // people[keyPath: \[Person].[i].name] = "update"
    }
    

    You could also use forEach, but I generally only recommend that over for in cases where you have an existing closure/function to pass in which has type (Index) -> Void:

    // meh
    people.indices.forEach {
        people[$0][keyPath: \Person.name] = "updated"
    }
    

  • answered 2018-02-13 01:09 matt

    EDIT Responding to your now edited question asking where is the Swift equivalent of [people makeObjectsPerformSelector: @selector(setName:) withObject: @"updated"], the simple answer is that map, which you for some reason reject in your question, is that equivalent. Of course, to do what Objective-C does, we have to use the Objective-C style of object type, namely a class:

    class Person {
        var name: String? = "Empty"
    }
    var people = [Person(), Person()]
    people = people.map {$0.name = "updated"; return $0} // *
    

    The starred line is how you make the objects in the array perform the "selector".

    A struct is a value type, so as you rightly said in your question, we have to insert a temp variable with a var reference:

    struct Person {
        var name: String? = "Empty"
    }
    var people = [Person(), Person()]
    people = people.map {var p = $0; p.name = "updated"; return p}
    

    [Original answer:]

    The use of key paths in your question seems to be a red herring. You're just asking how to set a property of all the structs in an array.

    map is just a way of cycling through the array. You cannot magically do this without cycling through the array; if you don't do it explicitly, you have to do it implicitly.

    Here's an explicit way of doing it:

    struct Person {
        var name: String? = "Empty"
    }
    var people = [Person(), Person()]
    let kp = \Person.name
    for i in 0..<people.count {
        people[i][keyPath:kp] = "updated"
    }
    

    That's not actually any more efficient than using map, though, as far as I know; structs are not mutable in place, so we are still filling the array with entirely new Person objects, exactly as we would have done if using map.