iosswiftstructswift4mutating-function

Swift struct mutating a variable not working?


I am not able to modify my model class variable even using mutating func keyword in a method?

So basically I have wrapped my problem in a very easy way I have a class Car that has 3 variable id, start, and modelNo

After that initialize an empty Car model array and then I want to show 10 cars, creating a range for loop which iterates 1...10, initializing the Car model class and appending it to original cars array.

There is a check for first 5 cars id will be 0 and last 5 cars id will be 1

I want a filter in which same id's last car will start so I have created a method filtered and modifying the start variable but not able to modify it. Can you please help me what I am doing wrong?

Please see my complete code and copy paste it to the playground.

struct Car {
    var id = 0
    var start = false
    var modelNo: String

    init(start: Bool, id: Int, model: String) {
        self.start = start
        self.id = id
        self.modelNo = model
    }

    mutating func startCar(status: Bool) {
        start = status
    }
}

var arrCars:[Car] = [Car]()

for i in 1...10 {
    let md = Car(start: false, id: i <= 5 ? 0 : 1, model: "Model\(i)")
    arrCars.append(md)
}

private func filtered() {
    for item in arrCars {
        let filteredItems = arrCars.filter { $0.id == item.id }
        if filteredItems.count > 0 {
            if var lastItem = filteredItems.last {
                print("Will start \(lastItem.modelNo)")
                lastItem.startCar(status: true)
            }
        }
    }

    let cars = arrCars.filter { (car) -> Bool in
        car.start == true
    }

    print(cars.count)
}

filtered()

Any help will be appreciated.


Solution

  • Creating a mutating function on a struct doesn't change the value semantics of structs. As soon as a struct instance is mutated, a copy is made.

    You say:

    if var lastItem = filteredItems.last {
        print("Will start \(lastItem.modelNo)")
        lastItem.startCar(status: true)
    }
    

    So, lastItem contains an instance of a car you are going to start. Under the covers this is the same instance of the car that is in filteredItems, which is the same instance that is in arrCars. But, as soon as you mutate the Car in lastItem, a copy is made, so lastItem no longer has the same instance as the one in arrCars. filteredItems and arrCars still contain the original, unaltered Car instance.

    You can change Car to be a class rather than a struct so that it has reference semantics to get the behaviour you want.

    The other option is to modify the instance in place; something like arrCars[0].startCar(status: true). To do this you will need to get an array containing the indicies of the cars you want to start rather than the cars themselves:

    private func filtered() {
        for item in arrCars {
            let matchingIndices = arrCars.enumerated().compactMap { (index,car) in
                return car.id == item.id ? index:nil
            }
            if matchingIndices.count > 0 {
                if let lastIndex = matchingIndices.last {
                    print("Will start \(arrCars[lastIndex].modelNo)")
                    arrCars[lastIndex].startCar(status: true)
                }
            }
        }
    
        let cars = arrCars.filter { (car) -> Bool in
            car.start == true
        }
    
        print(cars.count)
    }
    

    However, where a model object requires mutability or is stateful, a struct is probably not suitable.