Search code examples
iosswiftavfoundationimmutability

Swift class throwing immutable self


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?"


Solution

  • 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
        }
    }