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 struct
s and class
es 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 class
es, 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.