tlaminator
tlaminator

Reputation: 1006

Swift error in SwiftUI Button action closure: "Cannot use mutating member on immutable value: 'self' is immutable"

Following is the shortened version of the ContentView in my Swift app. The error Cannot use mutating member on immutable value: 'self' is immutable shows up on line self.classifyImage(self.image) inside my Button action closure. How do I set image to be mutable? Or is there a better way to do what I'm trying to accomplish? Essentially I want to pass the UIImage var in my ContentView to be processed by my Vision CoreML model via the classifyImage function I have here.

struct ContentView: View {
    @State private var image = UIImage()

    private lazy var classificationRequest: VNCoreMLRequest = {
        do {
          let model = try VNCoreMLModel(for: SqueezeNet().model)
        
          let request = VNCoreMLRequest(model: model) { request, _ in
              if let classifications =
                request.results as? [VNClassificationObservation] {
                print("Classification results: \(classifications)")
              }
          }
          request.imageCropAndScaleOption = .centerCrop
          return request
        } catch {
          fatalError("Failed to load Vision ML model: \(error)")
        }
    }()

    private mutating func classifyImage(_ image: UIImage) {
        guard let orientation = CGImagePropertyOrientation(
          rawValue: UInt32(image.imageOrientation.rawValue)) else {
          return
        }

        guard let ciImage = CIImage(image: image) else {
          fatalError("Unable to create \(CIImage.self) from \(image).")
        }

        DispatchQueue.global(qos: .userInitiated).async {
          let handler =
            VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
          do {
              try handler.perform([self.classificationRequest])
          } catch {
            print("Failed to perform classification.\n\(error.localizedDescription)")
          }
        }
    }

    var body: some View {
        Button(action: {
            self.classifyImage(self.image).   // <-- error
        }) {
            // Button text here
        }
        // blah blah
    }
}

Upvotes: 2

Views: 1923

Answers (2)

Asperi
Asperi

Reputation: 257709

You cannot mutate View from within itself as struct (so no lazy creations, no mutating func, etc). If you need to change image somewhere, then assign to it directly as to state.

Here is fixed (compilable) part of code. Tested with Xcode 12.

struct ContentView: View {
    @State private var image = UIImage()

    private let classificationRequest: VNCoreMLRequest = {
        do {
          let model = try VNCoreMLModel(for: SqueezeNet().model)

          let request = VNCoreMLRequest(model: model) { request, _ in
              if let classifications =
                request.results as? [VNClassificationObservation] {
                print("Classification results: \(classifications)")
              }
          }
          request.imageCropAndScaleOption = .centerCrop
          return request
        } catch {
          fatalError("Failed to load Vision ML model: \(error)")
        }
    }()

    private func classifyImage(_ image: UIImage) {
        guard let orientation = CGImagePropertyOrientation(
          rawValue: UInt32(image.imageOrientation.rawValue)) else {
          return
        }

        guard let ciImage = CIImage(image: image) else {
          fatalError("Unable to create \(CIImage.self) from \(image).")
        }

        DispatchQueue.global(qos: .userInitiated).async {
          let handler =
            VNImageRequestHandler(ciImage: ciImage, orientation: orientation)
          do {
              try handler.perform([self.classificationRequest])
          } catch {
            print("Failed to perform classification.\n\(error.localizedDescription)")
          }
        }
    }

    var body: some View {
        Button(action: {
            self.classifyImage(self.image)   // <-- error
        }) {
            // Button text here
        }
        // blah blah
    }
}

Upvotes: 1

Erwin Schens
Erwin Schens

Reputation: 434

The problem is that you are dealing with a struct. Changing a value inside a struct is semantically the same as assigning a new value to it. So the function would work when your ContentView struct is defined via var contentView = ContentView(). If you use let contentView = ContentView() than you get your error. The difference is that by assigning a new value to your image with the mutating function swift automatically creates a new Struct and assigns it to var contentView = ....

Another better approach would be if you would use a ViewModel like this:

import Combine
import UIKit
....
class ContentViewModel: ObservableObject {
    var image: uiImage
    @Published var classification: String

    init(_ image: UIImage) {
       self.image = image
    }

    func classifyImage() {
       // classify your image and assign the result to the published var classification. This way your view will be automatically updated on a change
    }
}

Then you could use the model in your View like this:

struct ContentView: View {
    @ObservedObject private var viewModel = ContentViewModel(UIImage())

    var body: some View {
        Button(action: {
            self.viewModel.classifyImage()
        }) {
            // Button text here
        }
        // blah blah
    }
}

This is a much cleaner approach due to the fact that you are encapsulate your logic into a viewmodel and do not pollute the view with processing code.

Upvotes: 0

Related Questions