简体   繁体   中英

Swift didSet called but not updating UILabel - iOS Property observer

Any idea why my label.text is only updating when the count finishes?

didSet is called. But the label.text = String(counter) appears to do nothing.

Swift 5

import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!

    var counter:Int = 0 {
        didSet {
            print("old value \(oldValue) and new value: \(counter)")
            label.text = String(counter)
            sleep(1).  // just added to show the label.text is not updating
        }
    }

    @IBAction func start_btn(_ sender: Any) {
        for _ in 1...3 {
            counter += 1
        }
    }
}

didSet code is called from the Main Thread. It is all wired correctly with Storyboards ( not SwiftUI ).

You can see the didSet code is called.

old value 0 and new value: 1. Main thread: true
old value 1 and new value: 2. Main thread: true
old value 2 and new value: 3. Main thread: true

It looks like you're trying to make some kind of counter which starts at 0 and stops at 3. If that is the case you should not call sleep (which blocks the main thread).

edit: apparently the sleep call was added for demonstration purposes? In any case the reason why your label seems like it is only updating when the count finishes is because the for loop runs too quickly for the UI to update on each counter increment.

Rather use Timer :

counter = 0
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
    self.counter += 1

    if self.counter >= 3 {
        timer.invalidate()
    }
}

This is based on my rough understanding on what you're aiming to achieve.

You could also DispatchQueue.main.asyncAfter :

func countUp() {
    guard counter < 3 else { return }

    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.counter += 1
        fire()
    }
}

For short time intervals, the difference between the two approaches is going to be pretty insignificant. For really accurate time counting, one shouldn't rely on either though, but rather use Date with a Timer that fires say every tenth of a second, and updates counter by rounding to the nearest second (for example).

You can achieve it like following

@IBAction func start_btn(_ sender: Any) {
    updateCounter()
}

func updateCounter() {
    if counter == 3 {
        return
    } else {
        counter += 1
        DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
            self.updateCounter()
        })

    }
}

Never, ever call sleep in an iOS app. That will block the main thread, which means your app will be frozen for a whole second with sleep(1) .

This means that the main thread will be blocked while the loop in start_btn finishes and hence the UI can only be updated after the loop has already finished.

If you want to make the text change every second, modify the button action to

@IBAction func start_btn(_ sender: Any) {
    for i in 1...3 {
        DispatchQueue.main.asyncAfter(deadline: .now() + Double(i), execute: {
            self.counter += 1
        })
    }
}

and remove sleep(1) from didSet .

To avoid UI blocking, dispatch the whole routine to a global queue, and dispatch the UI part to the main queue.

import UIKit
class ViewController: UIViewController {

    @IBOutlet weak var label: UILabel!

    var counter:Int = 0 {
        didSet {
            print("old value \(oldValue) and new value: \(counter)")
             DispatchQueue.main.async {
                self.label.text = String(self.counter)
            }
            sleep(1)
        }
    }

    @IBAction func start_btn(_ sender: Any) {
       DispatchQueue.global().async {
        for _ in 1...3 {
            self.counter += 1
          }
        }
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM