GameplayKit usage with Swift 6: Call to main actor-isolated instance method 'run' in a synchronous nonisolated context

Hi there,

With a couple of other developers we have been busy with migrating our SpriteKit games and frameworks to Swift 6.

There is one issue we are unable to resolve, and this involves the interaction between SpriteKit and GameplayKit.

There is a very small demo repo created that clearly demonstrates the issue. It can be found here:

https://github.com/AchrafKassioui/GameplayKitExplorer/blob/main/GameplayKitExplorer/Basic.swift

The relevant code also pasted here:

import SwiftUI
import SpriteKit

struct BasicView: View {
    var body: some View {
        SpriteView(scene: BasicScene())
            .ignoresSafeArea()
    }
}

#Preview {
    BasicView()
}

class BasicScene: SKScene {
    override func didMove(to view: SKView) {
        size = view.bounds.size
        anchorPoint = CGPoint(x: 0.5, y: 0.5)
        backgroundColor = .gray
        view.isMultipleTouchEnabled = true
        
        let entity = BasicEntity(color: .systemYellow, size: CGSize(width: 100, height: 100))
        if let renderComponent = entity.component(ofType: BasicRenderComponent.self) {
            addChild(renderComponent.sprite)
        }
    }
}

@MainActor
class BasicEntity: GKEntity {
    init(color: SKColor, size: CGSize) {
        super.init()
        
        let renderComponent = BasicRenderComponent(color: color, size: size)
        addComponent(renderComponent)
        
        let animationComponent = BasicAnimationComponent()
        addComponent(animationComponent)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

@MainActor
class BasicRenderComponent: GKComponent {
    let sprite: SKSpriteNode
    
    init(color: SKColor, size: CGSize) {
        self.sprite = SKSpriteNode(texture: nil, color: color, size: size)
        super.init()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class BasicAnimationComponent: GKComponent {
    let action1 = SKAction.scale(to: 1.3, duration: 0.07)
    let action2 = SKAction.scale(to: 1, duration: 0.15)
    
    override init() {
        super.init()
    }

    override func didAddToEntity() {
        if let renderComponent = entity?.component(ofType: BasicRenderComponent.self) {
            renderComponent.sprite.run(SKAction.repeatForever(SKAction.sequence([action1, action2])))
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

As SKNode is designed to run on the MainActor, the BasicRenderComponent is attributed with MainActor as well. This is needed as this GKComponent is dedicated to encapsulate the node that is rendered to the scene.

There is also a BasicAnimationComponent, this GKComponent is responsible for animating the rendered node.

Obviously, this is just an example, but when using GameplayKit in combination with SpriteKit it is very common that a GKComponent instance manipulates an SKNode referenced from another GKComponent instance, often done via open func update(deltaTime seconds: TimeInterval) or as in this example, inside didAddToEntity.

Now, the problem is that in the above example (but the same goes foupdate(deltaTime seconds: TimeInterval) the methoddidAddToEntity is not isolated to the MainActor, as GKComponent is not either.

This leads to the error Call to main actor-isolated instance method 'run' in a synchronous nonisolated context, as indeed the compiler can not infer that didAddToEntity is isolated to the MainActor.

Marking BasicAnimationComponent as @MainActor does not help, as this isolation is not propogated back to the superclass inherited methods.

In fact, we tried a plethora of other options, but none resolved this issue.

How should we proceed with this? As of now, this is really holding us back migrating to Swift 6. Hope someone is able to help out here!

Answered by DTS Engineer in 811987022

Hello @sanderfrenken,

Thank you for raising this issue!

Unfortunately, I don't have a good solution for you. Anything that you could do to get this integration working in Swift 6 would rely on very fragile assumptions that I won't recommend you make. It is better for you to file an enhancement request using Feedback Assistant, to request changes to SpriteKit and GameplayKit that would better facilitate their integration in a Swift 6 app.

For now, I recommend that you stick with the Swift 5 language mode while you wait for resolution on your bug report.

Best regards,

Greg

Accepted Answer

Hello @sanderfrenken,

Thank you for raising this issue!

Unfortunately, I don't have a good solution for you. Anything that you could do to get this integration working in Swift 6 would rely on very fragile assumptions that I won't recommend you make. It is better for you to file an enhancement request using Feedback Assistant, to request changes to SpriteKit and GameplayKit that would better facilitate their integration in a Swift 6 app.

For now, I recommend that you stick with the Swift 5 language mode while you wait for resolution on your bug report.

Best regards,

Greg

Hi @DTS Engineer ,

Thanks a lot for your reply. That is unfortunate, but maybe an enhancement request will provide a solution.

I did also post the question on StackOverflow, and there is a way to make it work indeed.

https://stackoverflow.com/questions/79128004/call-to-main-actor-isolated-instance-method-run-in-a-synchronous-non-isolated

I think that is the same solution as you refer to when you talk about:

very fragile assumptions that I won't recommend you make

In practice, I do see this could work if you really have no other option, but sticking with Swift 5 seems more reliable indeed.

I will create an enhancement request as you propose, thanks again for your help.

Sander

GameplayKit usage with Swift 6: Call to main actor-isolated instance method 'run' in a synchronous nonisolated context
 
 
Q