Xcode 11 Playground 中的 SwiftUI 状态更新

SwiftUI State updates in Xcode 11 Playground

我无法通过标准 Google 搜索找到任何内容,但是 ContentView 没有通过 ObservableObject 更新有什么原因吗?感觉好像错过了什么,但我不太确定是什么。

import SwiftUI
import PlaygroundSupport

let start = Date()
let seconds = 10.0 * 60.0

func timeRemaining(minutes: Int, seconds: Int) -> String {
    return "\(minutes) minutes \(seconds) seconds"
}

class ViewData : ObservableObject {
    @Published var timeRemaining: String = "Loading..."
}

// View

struct ContentView: View {
    @ObservedObject var viewData: ViewData = ViewData()

    var body: some View {
        VStack {
            Text(viewData.timeRemaining)
        }
    }
}


let contentView = ContentView()
let viewData = contentView.viewData
let hosting = UIHostingController(rootView: contentView)


// Timer

let timer = DispatchSource.makeTimerSource()
timer.schedule(deadline: .now(), repeating: .seconds(1))
timer.setEventHandler {
    let diff = -start.timeIntervalSinceNow
    let remaining = seconds - diff
    let mins = Int(remaining / 60.0)
    let secs = Int(remaining) % 60
    let timeRemaning = timeRemaining(minutes: mins, seconds: secs)
    viewData.timeRemaining = timeRemaning
    print(timeRemaning)
}
timer.resume()

DispatchQueue.main.asyncAfter(deadline: .now() + seconds) {
    timer.cancel()
    PlaygroundPage.current.finishExecution()
}

PlaygroundPage.current.setLiveView(contentView)
PlaygroundPage.current.needsIndefiniteExecution = true

原因是基于 GCD 的计时器在自己的队列上工作,所以这里是修复 - 必须在主视图模型上更新视图模型,UI,队列如下

DispatchQueue.main.async {
    viewData.timeRemaining = timeRemaning
}

GCD 计时器相对于标准计时器的主要用途是它们可以 运行 在后台队列中。正如 Asperi 所说,如果您的 GCD 计时器不使用主队列本身,您可以将更新分派到主队列。

但是,您最好从一开始就将 GCD 计时器安排在主队列上,这样您就完全不必手动调度到主队列了:

let timer = DispatchSource.makeTimerSource(queue: .main)

但是,如果你要在主线程上 运行 这个,你可以只使用 Timer,或者,在 SwiftUI 项目中,你可能更喜欢 Combine 的 TimerPublisher :

import Combine

...

var timer: AnyCancellable? = nil
timer = Timer.publish(every: 1, on: .main, in: .common)
    .autoconnect()
    .sink { _ in
        let remaining = start.addingTimeInterval(seconds).timeIntervalSince(Date())

        guard remaining >= 0 else {
            viewData.timeRemaining = "done!"
            timer?.cancel()
            return
        }

        let mins = Int(remaining / 60.0)
        let secs = Int(remaining) % 60

        viewData.timeRemaining = timeRemaining(minutes: mins, seconds: secs)
}

当您将计时器合并到您的 SwiftUI 代码中(而不是像这里这样的全局代码)时,最好留在 Combine Publisher 范例中。

我还认为当定时器在定时器处理程序中到期时取消定时器可能更干净,而不是单独执行 asyncAfter


无关,但您可以考虑在 timeRemaining 函数中使用 DateComponentsFormatter,例如:

let formatter: DateComponentsFormatter = {
    let formatter = DateComponentsFormatter()
    formatter.allowedUnits = [.minute, .second]
    formatter.unitsStyle = .full
    return formatter
}()

func timeRemaining(_ timeInterval: TimeInterval) -> String {
    formatter.string(from: timeInterval) ?? "Error"
}

然后,

  • 它让您摆脱自己计算分秒的工作;
  • 字符串将被本地化;和
  • 它将确保语法正确的措辞;例如当还剩 61 秒时,现有例程将报告语法错误的“1 分钟,1 秒”。

DateComponentsFormatter 让您摆脱处理这些边缘情况的杂草,在这些情况下您需要单数而不是复数或英语以外的语言。