Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

Swift Concurrency: Calling @MainActor Function from Protocol Implementation in Swift 6

I have a Settings class that conform to the TestProtocol. From the function of the protocol I need to call the setString function and this function needs to be on the MainActor. Is there a way of make this work in Swift6, without making the protocol functions running on @MainActor

The calls are as follows:

class Settings: TestProtocol{

    var value:String = ""
    
    @MainActor func setString( _ string:String ){
        value = string
    }
    
    func passString(string: String) {
        Task{
            await setString(string)
        }
    }
}


protocol TestProtocol{
    func passString( string:String )
}
Answered by DTS Engineer in 839927022

For those reading along at home, the code that reza___f posted fails to compile with an isolation error. Here’s a slightly edited version that makes the reason for that clearer:

class Settings: TestProtocol {
    …

    func passString(string: String) {
        let s = self
        Task {
          // ^ Passing closure as a 'sending' parameter risks causing data races
          // between code in the current task and concurrent execution of the
          // closure
            await s.setString(string)
        }
    }
}

This fails because:

  1. The task initialiser expects a sendable closure.

  2. But the task captures s.

  3. s is not sendable.

There are various ways around this but probably the easiest is to declare the class as @MainActor:

@MainActor
class Settings: TestProtocol {
    …

    nonisolated func passString(string: String) {
        let s = self
        Task {
            await s.setString(string)
        }
    }
}

Or to return to your simpler form:

@MainActor
class Settings: TestProtocol {
    …

    nonisolated func passString(string: String) {
        Task {
            await setString(string)
        }
    }
}

This works because main-actor objects are a bit like actual actors, that is, they are sendable. That’s because the compiler knows that everything in the object is either only accessible on the main actor or non-isolated, and hence safe to call from other contexts.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

For those reading along at home, the code that reza___f posted fails to compile with an isolation error. Here’s a slightly edited version that makes the reason for that clearer:

class Settings: TestProtocol {
    …

    func passString(string: String) {
        let s = self
        Task {
          // ^ Passing closure as a 'sending' parameter risks causing data races
          // between code in the current task and concurrent execution of the
          // closure
            await s.setString(string)
        }
    }
}

This fails because:

  1. The task initialiser expects a sendable closure.

  2. But the task captures s.

  3. s is not sendable.

There are various ways around this but probably the easiest is to declare the class as @MainActor:

@MainActor
class Settings: TestProtocol {
    …

    nonisolated func passString(string: String) {
        let s = self
        Task {
            await s.setString(string)
        }
    }
}

Or to return to your simpler form:

@MainActor
class Settings: TestProtocol {
    …

    nonisolated func passString(string: String) {
        Task {
            await setString(string)
        }
    }
}

This works because main-actor objects are a bit like actual actors, that is, they are sendable. That’s because the compiler knows that everything in the object is either only accessible on the main actor or non-isolated, and hence safe to call from other contexts.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Swift Concurrency: Calling @MainActor Function from Protocol Implementation in Swift 6
 
 
Q