【问题标题】:SwiftUI State updates in Xcode 11 PlaygroundXcode 11 Playground 中的 SwiftUI 状态更新
【发布时间】:2020-01-21 19:35:51
【问题描述】:

我无法通过标准的 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

【问题讨论】:

    标签: ios swiftui grand-central-dispatch swift-playground xcode11


    【解决方案1】:

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

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

    【讨论】:

    • 我知道有些事情是显而易见的。谢谢
    【解决方案2】:

    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 让您摆脱处理这类边缘情况的麻烦,在这种情况下,您需要单数而不是复数或英语以外的其他语言。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-06-12
      • 1970-01-01
      相关资源
      最近更新 更多