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
让您摆脱处理这些边缘情况的杂草,在这些情况下您需要单数而不是复数或英语以外的语言。
我无法通过标准 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
让您摆脱处理这些边缘情况的杂草,在这些情况下您需要单数而不是复数或英语以外的语言。