Home > Mobile >  Swift class throwing immutable self
Swift class throwing immutable self

Time:08-12

I have a method in a protocol extension that plays music files. Since the protocol doesn't know if its conformer will be a class or struct and the methods in it change an ivar, it requires them to be marked as mutating.

When I conform a class to that protocol and try to call the method I'm getting the below error even though a class should always be muteable...

Cannot use mutating member on immutable value: 'self' is immutable

Here's the protocol...

import AVFoundation

/// Conformers are required to implement `player` property to gain access to `playMusic:fileLloops`
/// method.
protocol CanPlayMusic {
    
    /// This instance variable stores the audio player for `playMusic:file:loops` method.
    var player: AVAudioPlayer! { get set }
}

extension CanPlayMusic {
    
    /// This method creates a new audio player from url and then plays for a number of given.
    ///
    /// - Parameter file:  The url where sound file is stored.
    /// - Parameter loops: The number of loops to play (-1 is infinite).
    mutating func playMusic(file url: URL, loops: Int = -1) {
        player = try! AVAudioPlayer(contentsOf: url)
        player.numberOfLoops = loops
        player.play()
    }
    
    /// This method starts playing intro music.
    mutating func playIntroMusic() {
        let file = Assets.Music.chargeDeBurkel
        let ext = Assets.Music.ext
        guard let url = Bundle.main.url(forResource: file,
                                        withExtension: ext) else { return }
        
        playMusic(file: url)
    }
    
    /// This method starts playing game over music based on win/loss condition.
    mutating func playGameOverMusic(isWinning: Bool) {
        guard let lose = Bundle.main.url(forResource: Assets.Music.taps,
                                         withExtension: Assets.Music.ext),
              let win  = Bundle.main.url(forResource: Assets.Music.reveille,
                                         withExtension: Assets.Music.ext)
        else { return }
        
        playMusic(file: isWinning ? win : lose, loops: 1)
    }
}

And here's how I call it in a class...

import UIKit
import AVFoundation

class EntranceViewController: UIViewController, CanGetCurrency, CanPlayMusic {
    
...

    // MARK: - Properties: CanPlayMusic
    
    var player: AVAudioPlayer!

...

    // MARK: - Functions: UIViewController
    
    override func viewDidLoad() {
        playIntroMusic()                          // <-- Error thrown here 
        startButton.setTitle("", for: .normal)
        getCurrency()
    }

UPDATE

I use these methods in multiple places, both in UIKit scenes and SwiftUI views; moving it into the protocol extension was an attempt to reduce duplication of code.

For now, I'm using a class wrapper that I can call in both contexts; but I still haven't seen an explanation for the error triggering on a class (since they're pass by ref and all of their functions are considered mutating by default).

To clarify, my question is "Why is this class having issues with a mutating function?"

CodePudding user response:

The error is a bit misleading, but I believe the reason of it is that you call a method marked with mutating (defined in your protocol extension) from a class – it's illegal.

Consider this simplified example:

protocol SomeProtocol {
    var state: Int { get set }
}

extension SomeProtocol {
    mutating func doSomething() {
        state  = 1
    }
}

struct SomeValueType: SomeProtocol {
    var state = 0
    init() {
        doSomething()
    }
}

final class SomeReferenceType: SomeProtocol {
    var state = 0
    init() {
        doSomething() // Cannot use mutating member on immutable value: 'self' is immutable
    }
}

One way to get rid of the error is not using the same implementation for both structs and classes and defining their own implementations:

protocol SomeProtocol {
    var state: Int { get set }
    mutating func doSomething()
}

struct SomeValueType: SomeProtocol {
    var state = 0
    init() {
        doSomething()
    }
    mutating func doSomething() {
        state  = 1
    }
}

final class SomeReferenceType: SomeProtocol {
    var state = 0
    init() {
        doSomething()
    }
    func doSomething() {
        state  = 1
    }
}

Another way is to, at least, defining an own implementation for classes, which will shade the default implementation from the protocol extension:

protocol SomeProtocol {
    var state: Int { get set }
}

extension SomeProtocol {
    mutating func doSomething() {
        state  = 1
    }
}

struct SomeValueType: SomeProtocol {
    var state = 0
    init() {
        doSomething()
    }
}

final class SomeReferenceType: SomeProtocol {
    var state = 0
    init() {
        doSomething()
    }
    func doSomething() {
        state  = 1
    }
}

CodePudding user response:

You can declare your protocol to be used for classes only:

protocol CanPlayMusic: AnyObject {
    // ...
}

and remove all mutating keywords. Then it should compile.

  • Related