Error when calling function to move an element: "Cannot use mutating member on immutable value: 'self' is immutable"

Hi,

The goal is the move an element based on an input matrix using timer to trigger changes in the position. Say we have 2 positions, an input matrix and the element that is to be moved defined:

import SwiftUI import Foundation

// Positions

struct PositionConstants {
    
    static let pos_1 = CGPoint(x: 155, y: 475)
    static let pos_2 = CGPoint(x: 135, y: 375)
    
    
}

// Input Matrix

struct Input {

    static let input_1: [[Int]] = [
        [    1,        1,       0,      0,      0,      0,     0,     0,     0   ],
        [    0,        0,       0,      0,      0,      0,     0,     0,     0   ],
        [    0,        0,       0,      0,      0,      0,     0,     0,     0   ],
        [    0,        0,       0,      0,      0,      0,     0,     0,     0   ],
        [    1,        0,       1,      0,      0,      0,     0,     0,     0   ], 
        [    0,        0,       0,      0,      0,      0,     0,     0,     0   ], 
        [    0,        0,       0,      0,      0,      0,     0,     0,     0   ], 
        [    0,        0,       0,      0,      0,      0,     0,     0,     0   ], 
    ]
}

// Element Class

class Element: ObservableObject {
    let imageName: String
    let name: String
    let size: CGSize
    @Published var position: CGPoint
    
    init(imageName: String, name: String, size: CGSize, position: CGPoint) {
        self.imageName = imageName
        self.name = name
        self.size = size
        self.position = position
    }
}

Moving the element without a function works perfectly fine:

struct Pos_View: View {
    @ObservedObject var element_1 = Element(imageName: "pic_1", name: "", size: CGSize(width: 100, height: 100), position: PositionConstants.pos_1)
    
    let Input_Matrix = Input.input_1 
    
    // Index to track current row in matrix
    @State private var currentIndex = 0
    
    // Timer to trigger updates
    private let timer = Timer.publish(every: 1.0, on: .main, in: .common).autoconnect()
    
    var body: some View {
        VStack {
            // Display element
            Image(element_1.imageName)
                .resizable()
                .frame(width: element_1.size.width, height: element_1.size.height)
                .position(element_1.position)
                .onReceive(timer) { _ in
                    // Update element position based on matrix
                    if currentIndex < Input_Matrix.count {
                        let isFirstElementOne = Input_Matrix[currentIndex][0] == 1
                        let newPosition = isFirstElementOne ? PositionConstants.pos_2 : PositionConstants.pos_1
                        withAnimation {
                            element_1.position = newPosition
                        }
                        currentIndex += 1
                    } else {
                        // Stop the timer when we reach the end of the matrix
                        timer.upstream.connect().cancel()
                    }
                }
        }
    }
}


struct ContentView: PreviewProvider {
    static var previews: some View {
        Pos_View()
    }
}

But when using a function to trigger the animation, I get the error "Cannot use mutating member on immutable value: 'self' is immutable" when calling the function using a button:

struct Pos_View: View {
    @ObservedObject var element_1 = Element(imageName: "pic_1", name: "", size: CGSize(width: 100, height: 100), position: PositionConstants.pos_1)
    
    let Input_Matrix = Input.input_1
    
    // Index to track current row in matrix
    @State var currentIndex = 0
    
    // Timer to trigger updates
    private var timer: Timer?
    
    var body: some View {
        VStack {
            // Display element
            Image(element_1.imageName)
                .resizable()
                .frame(width: element_1.size.width, height: element_1.size.height)
                .position(element_1.position)
            
            // Button to start animation
            Button("Start Animation") {
                startAnimation()
            }
        }
    }
    
    mutating func startAnimation() {
        currentIndex = 0 // Reset index before starting animation
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {timer in
            if self.currentIndex < self.Input_Matrix.count {
                let isFirstElementOne = self.Input_Matrix[self.currentIndex][0] == 1
                let newPosition = isFirstElementOne ? PositionConstants.pos_2 : PositionConstants.pos_1
                withAnimation {
                    self.element_1.position = newPosition
                }
                self.currentIndex += 1
            } else {
                // Stop the timer when we reach the end of matrix
                timer.invalidate()
            }
        }
    }
}

struct ContentView: PreviewProvider {
    static var previews: some View {
        Pos_View()
    }
}

The function is defined as mutating and the element as as @ObservedObject. Anyone has a clue?

Replies

Where exactly is the error ? On timer probably.

That's because you declare the function as mutating (as compiler asks for).

But you have then to make timer a State variable:

struct Pos_View: View { 
    @ObservedObject var element_1 = Element(imageName: "pic_1", name: "", size: CGSize(width: 100, height: 100), position: PositionConstants.pos_1) 
    
    let Input_Matrix = Input.input_1
    
    // Index to track current row in matrix
    @State var currentIndex = 0
    
    // Timer to trigger updates
    @State private var timer: Timer?    // <<-- State var
    
    var body: some View {
        VStack {
            // Display element
            Image(element_1.imageName)
                .resizable()
                .frame(width: element_1.size.width, height: element_1.size.height)
                .position(element_1.position)
            
            // Button to start animation
            Button("Start Animation") {
                startAnimation()
            }
        }
    }
    
    /*mutating*/ func startAnimation() {      // <<-- no mutating
        currentIndex = 0 // Reset index before starting animation
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) {timer in
            if self.currentIndex < self.Input_Matrix.count {
                let isFirstElementOne = self.Input_Matrix[self.currentIndex][0] == 1
                let newPosition = isFirstElementOne ? PositionConstants.pos_2 : PositionConstants.pos_1
                withAnimation {
                    self.element_1.position = newPosition
                }
                self.currentIndex += 1
            } else {
                // Stop the timer when we reach the end of matrix
                timer.invalidate()
            }
        }
    }
}

Ah. I See. Thank you very much! It's working now.

Thanks for the feedback. Don't forget to mark the correct answer to close the thread. Good continuation.