Search code examples
swiftuilabelgrand-central-dispatchscenekit

iOS - Swift 5 - Label Text Not Updating within Loop


I am using Scenekit and I am trying to update a label text property on the UI from within a loop when I tap on the screen. I have verified the label is connected in the StoryBoard and I can update the label from ViewDidLoad. I have also verified the text value within the loop exists as I can see it printing in the console. The full code of the gesture recogniser is below and you can see at the bottom I have already tried GCD with no luck. Any help appreciated.

enter image description here

@objc func sceneTapped(recognizer: UITapGestureRecognizer) {
    
    let KTStoKMperSec = 0.0005144444444444
    let aircraftSpeed = 480.0 * KTStoKMperSec
     
    let quatSun =  simd_quatf(angle: Float(-1.0 / aircraftSpeed * 2 * .pi / 86400.0),
                              axis: simd_float3(x: 0,
                                                y: 1,
                                                z: 0))
    
    for i in 0...geodesic.count-1 {
        
        SCNTransaction.begin()

        let quatPitch =  simd_quatf(angle: Float(-1.0/6371.0),
                                    axis: simd_float3(x: 1,
                                                      y: 0,
                                                      z: 0))

        aircraft.position = geodesic[i].unit * 1.05
        aircraft.localRotate(by: SCNQuaternion(quatPitch.vector.x, quatPitch.vector.y, quatPitch.vector.z, quatPitch.vector.w))

        let SCNQuat = SCNQuaternion(quatSun.vector.x, quatSun.vector.y, quatSun.vector.z, quatSun.vector.w)
        termPathnode.rotate(by: SCNQuat, aroundTarget: SCNVector3(x:0, y:1, z:0))

        lightNode.rotate(by: SCNQuat, aroundTarget: SCNVector3(x:0, y:1, z:0))

        cameraNode.position = aircraft.position.unit*3

        SCNTransaction.commit()


        //print(String("\(aircraft.position.x)"))
        DispatchQueue.main.async {
            //self.dataLabel.setNeedsDisplay()
            self.dataLabel.text =  String("\(aircraft.position.x)")
        }
    }
}

Solution

  • You didn't explain what the problem is, but I'm going to guess that nothing happens and then the label gets update (finally) to the last value.

    That's because sceneTapped is a gesture recognizer, which mean it's running on the main thread. (All UI code and event handling runs on the main thread.)

    So sceneTapped is spinning in a loop, on the main thread, queuing up blocks to run on the main thread. But the main thread is busy running sceneTapped so none of those blocks execute.

    Finally, sceneTapped returns, and all of those queued blocks suddenly execute, one after the other.

    If this for loop is a long running loop, it needs to be running in its own thread. As a rule, you never want to have any code that takes a significant amount to time to execute to be running on the main thread. Any code that blocks the main thread freezes your UI and all user interaction.

    So basically you should have something like this:

    @objc func sceneTapped(recognizer: UITapGestureRecognizer) {
        // ...
        DispatchQueue.global().async {
            // execute long running code in a background thread
            for i in 0...geodesic.count-1 {
                // ...
    
                //print(String("\(aircraft.position.x)"))
                DispatchQueue.main.async {
                    //self.dataLabel.setNeedsDisplay()
                    self.dataLabel.text =  String("\(aircraft.position.x)")
                }
            }
        }
    }
    

    You want the bulk of the action to run in a background thread, doing its thing, calculating stuff, blocking, waiting, whatever.

    When it has something to do with the UI, it can queue a block to run on the main thread, which is still (independently) running, handling events, updating views, recalculating layouts, and generally keeping your app responsive.

    Finally, you have to be sure that everything in that background thread is thread safe.