1
votes

I know I'm doing something wrong but I do not know what..

All I would like to do here is add some pet to my Array<Pet>

What am I doing wrong here?

The error is

Cannot use mutating member on immutable value: function call returns immutable value

import UIKit

struct Person {
    let id = UUID()
    let name: String
    var pets: [String]
}

final class City {
    let people = [Person]()
    
    func addPetFor(id: UUID, newPets: [String]) {
        self.people.findPerson(by: id).pets.append(contentsOf: newPets) //<-- error Cannot use mutating member on immutable value: function call returns immutable value
    }
}

extension Array where Element == Person {
    func findPerson(by id: UUID) -> Person {
        let foundPerson = self.first { person in
            person.id == id
        }
        return foundPerson!
    }
}

Picture of failed attempt to append an array to an array

2

2 Answers

2
votes

Structs have value semantics. When you pass them around/assign them to things, they get copied, and unless it is var, you can't mutate them.

Return values are not vars, you cannot change a return value. You can put it in a var variable first, and then append to pets

var person = self.people.findPerson(by: id)
person.pets.append(contentsOf: newPets)

This makes the compiler error go away, but person here is a copy of the return value, so your modification here doesn't actually change the things in the self.people array. self.people is declared a let constant in the first place, so you better change that too:

var people = [Person]()

One way is to have findPerson return the index of the person found:

extension Array where Element == Person {
    func findPerson(by id: UUID) -> Int {
        let foundPerson = self.firstIndex { person in
            person.id == id
        }
        return foundPerson!
    }
}

This way, you can do:

let index = self.people.findPerson(by: id)
self.people[index].pets.append(contentsOf: newPets)

Note how I am doing things directly on self.people, rather than through another variable. addPetFor also needs to be marked mutating.

Another way is to have findPerson take a closure, and you write in that closure what you want to do with the found person:

extension Array where Element == Person {
    mutating func findPerson(by id: UUID, andDo block: (inout Person) -> Void) -> Int {
        let index = self.firstIndex { person in
            person.id == id
        }
        block(&self[index!])
    }
}

Note how findPerson is now marked mutating, because it now mutates the array (self). self[index] is passed by inout, which is kind of like "by reference".

Then you can do:

self.people.findPerson(by: id) {
    $0.pets.append(contentsOf: newPets)
}
1
votes

As the error tells you, your findPerson method is returning an immutable struct, so you can't edit it.

There are a few solutions to this -- two are included in the code sample below:

final class City {
    var people = [Person]()
    
    func addPetFor(id: UUID, newPets: [String]) {
        guard let index = self.people.firstIndex(where: { $0.id == id }) else {
            assertionFailure("Not found")
            return
        }
        self.people[index].pets.append(contentsOf: newPets)
    }
    
    func addPetFor2(id: UUID, newPets: [String]) {
        self.people = self.people.map {
            if $0.id == id {
                var personCopy = $0
                personCopy.pets.append(contentsOf: newPets)
                return personCopy
            } else {
                return $0
            }
        }
    }
}

In both cases, the code has a reference to where the Person belongs in the people array, so it can modify it. Note that people also has to be var, not let, for the same reason -- it needs to be mutable.