SwiftUI Timer 在更新 ObservedObjects 时不断重置
SwiftUI Timer keep resetting when updating the ObservedObjects
我刚刚编写了一个简单的计时器代码,并试图找出一种方法,在其他观察到的对象状态发生变化时不重置计时器。
当我使用 init 函数启动计时器时,只要其他观察到的对象的状态发生变化,它就会重置。
当我用 onAppear 启动计时器时,一旦其他观察到的对象的状态发生变化,它就会发生变化,并且再也不会启动。
我想要完成的是,计时器启动一次,并且在其他观察到的对象从其他 View 传递出去并且 tiemr 本身必须是 Subview 期间其他观察到的对象发生变化时不会重置。
有什么建议吗?
import SwiftUI
import Combine
import Foundation
struct ContentView: View {
@ObservedObject var apptCardVM: ApptCardViewModel
@ObservedObject var timerData = TimerDataViewModel()
var body: some View {
VStack {
CurrentDateView(timerData: timerData) // << here !!
Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
ForEach(0..<apptCardVM.typeChoice.count) {
Text(self.apptCardVM.typeChoice[[=10=]])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct CurrentDateView: View {
@State private var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@ObservedObject var timerData: TimerDataViewModel
var body: some View {
Text("\(Int(timerData.hoursElapsed), specifier: "%02d"):\(Int(timerData.minutesElapsed), specifier: "%02d"):\(Int(timerData.secondsElapsed), specifier: "%02d")")
.fontWeight(.bold)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear(){
timerData.start()
}
}
}
class ApptCardViewModel: ObservableObject, Identifiable {
@Published var typeChoice = ["Quick", "Long", "FullService"]
@Published var typeIndex: Int = 0
private var cancellables = Set<AnyCancellable>()
}
class TimerDataViewModel: ObservableObject{
@Published var timer = Timer()
@Published var startTime : Double = 0.0
@Published var secondsOriginal = 0.0
@Published var secondsElapsed = 0.0
@Published var secondsElapsed_ = 0.0
@Published var minutesElapsed = 0.0
@Published var hoursElapsed = 0.0
enum stopWatchMode {
case running
case stopped
case paused
}
init(){
// start()
print("initialized")
}
func start(){
self.secondsOriginal = self.startTime
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
self.secondsOriginal += 1
self.secondsElapsed_ = Double(Int(self.secondsOriginal))
self.secondsElapsed = Double(Int(self.secondsOriginal)%60)
self.minutesElapsed = Double(Int(self.secondsOriginal)/60 % 60)
self.hoursElapsed = Double(Int(self.secondsOriginal)/3600 % 24)
}
}
}
import SwiftUI
import Combine
struct SeizureView: View {
@ObservedObject var apptCardVM: ApptCardViewModel
//This will solve your issue.
@StateObject var timerData = TimerDataViewModel()
@State private var currentDate = Date()
var body: some View {
VStack {
CurrentDateView(currentDate: $currentDate, timerData: timerData)
//But something to consider
//This View is reusable and does not need a timer. Just a Date object
TimerView(date: currentDate, showSubseconds: false)
Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
ForEach(0..<apptCardVM.typeChoice.count) {
Text(self.apptCardVM.typeChoice[[=10=]])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct CurrentDateView: View {
@Binding var currentDate : Date
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@ObservedObject var timerData: TimerDataViewModel
var body: some View {
Text("\(Int(timerData.hoursElapsed), specifier: "%02d"):\(Int(timerData.minutesElapsed), specifier: "%02d"):\(Int(timerData.secondsElapsed), specifier: "%02d")")
.fontWeight(.bold)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear(){
timerData.start()
}
}
}
class ApptCardViewModel: ObservableObject, Identifiable {
@Published var typeChoice = ["Quick", "Long", "FullService"]
@Published var typeIndex: Int = 0
private var cancellables = Set<AnyCancellable>()
}
class TimerDataViewModel: ObservableObject{
@Published var timer = Timer()
@Published var startTime : Double = 0.0
@Published var secondsOriginal = 0.0
@Published var secondsElapsed = 0.0
@Published var secondsElapsed_ = 0.0
@Published var minutesElapsed = 0.0
@Published var hoursElapsed = 0.0
enum stopWatchMode {
case running
case stopped
case paused
}
init(){
// start()
print("initialized")
}
func start(){
self.secondsOriginal = self.startTime
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
self.secondsOriginal += 1
self.secondsElapsed_ = Double(Int(self.secondsOriginal))
self.secondsElapsed = Double(Int(self.secondsOriginal)%60)
self.minutesElapsed = Double(Int(self.secondsOriginal)/60 % 60)
self.hoursElapsed = Double(Int(self.secondsOriginal)/3600 % 24)
}
}
}
struct TimerView: View {
var date: Date
var showSubseconds: Bool
var fontWeight: Font.Weight = .bold
var body: some View {
if #available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0, *) {
//The code from here is mostly from https://developer.apple.com/wwdc21/10009
TimelineView(MetricsTimelineSchedule(from: date)) { context in
ElapsedTimeView(elapsedTime: -date.timeIntervalSinceNow, showSubseconds: showSubseconds)
}
} else {
Text(date,style: .timer)
.fontWeight(fontWeight)
.clipped()
}
}
}
@available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0,*)
private struct MetricsTimelineSchedule: TimelineSchedule {
var startDate: Date
init(from startDate: Date) {
self.startDate = startDate
}
func entries(from startDate: Date, mode: TimelineScheduleMode) -> PeriodicTimelineSchedule.Entries {
PeriodicTimelineSchedule(from: self.startDate, by: (mode == .lowFrequency ? 1.0 : 1.0 / 30.0))
.entries(from: startDate, mode: mode)
}
}
struct ElapsedTimeView: View {
var elapsedTime: TimeInterval = 0
var showSubseconds: Bool = false
var fontWeight: Font.Weight = .bold
@State private var timeFormatter = ElapsedTimeFormatter(showSubseconds: false)
var body: some View {
Text(NSNumber(value: elapsedTime), formatter: timeFormatter)
.fontWeight(fontWeight)
.onChange(of: showSubseconds) {
timeFormatter.showSubseconds = [=10=]
}
.onAppear(perform: {
timeFormatter = ElapsedTimeFormatter(showSubseconds: showSubseconds)
})
}
}
class ElapsedTimeFormatter: Formatter {
let componentsFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second, .hour]
formatter.zeroFormattingBehavior = .pad
return formatter
}()
var showSubseconds: Bool
init(showSubseconds: Bool) {
self.showSubseconds = showSubseconds
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func string(for value: Any?) -> String? {
guard let time = value as? TimeInterval else {
return nil
}
guard let formattedString = componentsFormatter.string(from: time) else {
return nil
}
if showSubseconds {
let hundredths = Int((time.truncatingRemainder(dividingBy: 1)) * 100)
let decimalSeparator = Locale.current.decimalSeparator ?? "."
return String(format: "%@%@%0.2d", formattedString, decimalSeparator, hundredths)
}
return formattedString
}
}
struct SeizureView_Previews: PreviewProvider {
static var previews: some View {
SeizureView(apptCardVM: ApptCardViewModel())
}
}
我刚刚编写了一个简单的计时器代码,并试图找出一种方法,在其他观察到的对象状态发生变化时不重置计时器。 当我使用 init 函数启动计时器时,只要其他观察到的对象的状态发生变化,它就会重置。 当我用 onAppear 启动计时器时,一旦其他观察到的对象的状态发生变化,它就会发生变化,并且再也不会启动。 我想要完成的是,计时器启动一次,并且在其他观察到的对象从其他 View 传递出去并且 tiemr 本身必须是 Subview 期间其他观察到的对象发生变化时不会重置。 有什么建议吗?
import SwiftUI
import Combine
import Foundation
struct ContentView: View {
@ObservedObject var apptCardVM: ApptCardViewModel
@ObservedObject var timerData = TimerDataViewModel()
var body: some View {
VStack {
CurrentDateView(timerData: timerData) // << here !!
Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
ForEach(0..<apptCardVM.typeChoice.count) {
Text(self.apptCardVM.typeChoice[[=10=]])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct CurrentDateView: View {
@State private var currentDate = Date()
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@ObservedObject var timerData: TimerDataViewModel
var body: some View {
Text("\(Int(timerData.hoursElapsed), specifier: "%02d"):\(Int(timerData.minutesElapsed), specifier: "%02d"):\(Int(timerData.secondsElapsed), specifier: "%02d")")
.fontWeight(.bold)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear(){
timerData.start()
}
}
}
class ApptCardViewModel: ObservableObject, Identifiable {
@Published var typeChoice = ["Quick", "Long", "FullService"]
@Published var typeIndex: Int = 0
private var cancellables = Set<AnyCancellable>()
}
class TimerDataViewModel: ObservableObject{
@Published var timer = Timer()
@Published var startTime : Double = 0.0
@Published var secondsOriginal = 0.0
@Published var secondsElapsed = 0.0
@Published var secondsElapsed_ = 0.0
@Published var minutesElapsed = 0.0
@Published var hoursElapsed = 0.0
enum stopWatchMode {
case running
case stopped
case paused
}
init(){
// start()
print("initialized")
}
func start(){
self.secondsOriginal = self.startTime
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
self.secondsOriginal += 1
self.secondsElapsed_ = Double(Int(self.secondsOriginal))
self.secondsElapsed = Double(Int(self.secondsOriginal)%60)
self.minutesElapsed = Double(Int(self.secondsOriginal)/60 % 60)
self.hoursElapsed = Double(Int(self.secondsOriginal)/3600 % 24)
}
}
}
import SwiftUI
import Combine
struct SeizureView: View {
@ObservedObject var apptCardVM: ApptCardViewModel
//This will solve your issue.
@StateObject var timerData = TimerDataViewModel()
@State private var currentDate = Date()
var body: some View {
VStack {
CurrentDateView(currentDate: $currentDate, timerData: timerData)
//But something to consider
//This View is reusable and does not need a timer. Just a Date object
TimerView(date: currentDate, showSubseconds: false)
Picker("Seizure Type", selection: $apptCardVM.typeIndex) {
ForEach(0..<apptCardVM.typeChoice.count) {
Text(self.apptCardVM.typeChoice[[=10=]])
}
}.pickerStyle(SegmentedPickerStyle())
}
}
}
struct CurrentDateView: View {
@Binding var currentDate : Date
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@ObservedObject var timerData: TimerDataViewModel
var body: some View {
Text("\(Int(timerData.hoursElapsed), specifier: "%02d"):\(Int(timerData.minutesElapsed), specifier: "%02d"):\(Int(timerData.secondsElapsed), specifier: "%02d")")
.fontWeight(.bold)
.textFieldStyle(RoundedBorderTextFieldStyle())
.onAppear(){
timerData.start()
}
}
}
class ApptCardViewModel: ObservableObject, Identifiable {
@Published var typeChoice = ["Quick", "Long", "FullService"]
@Published var typeIndex: Int = 0
private var cancellables = Set<AnyCancellable>()
}
class TimerDataViewModel: ObservableObject{
@Published var timer = Timer()
@Published var startTime : Double = 0.0
@Published var secondsOriginal = 0.0
@Published var secondsElapsed = 0.0
@Published var secondsElapsed_ = 0.0
@Published var minutesElapsed = 0.0
@Published var hoursElapsed = 0.0
enum stopWatchMode {
case running
case stopped
case paused
}
init(){
// start()
print("initialized")
}
func start(){
self.secondsOriginal = self.startTime
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true){ timer in
self.secondsOriginal += 1
self.secondsElapsed_ = Double(Int(self.secondsOriginal))
self.secondsElapsed = Double(Int(self.secondsOriginal)%60)
self.minutesElapsed = Double(Int(self.secondsOriginal)/60 % 60)
self.hoursElapsed = Double(Int(self.secondsOriginal)/3600 % 24)
}
}
}
struct TimerView: View {
var date: Date
var showSubseconds: Bool
var fontWeight: Font.Weight = .bold
var body: some View {
if #available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0, *) {
//The code from here is mostly from https://developer.apple.com/wwdc21/10009
TimelineView(MetricsTimelineSchedule(from: date)) { context in
ElapsedTimeView(elapsedTime: -date.timeIntervalSinceNow, showSubseconds: showSubseconds)
}
} else {
Text(date,style: .timer)
.fontWeight(fontWeight)
.clipped()
}
}
}
@available(watchOSApplicationExtension 8.0, watchOS 8.0, iOS 15.0,*)
private struct MetricsTimelineSchedule: TimelineSchedule {
var startDate: Date
init(from startDate: Date) {
self.startDate = startDate
}
func entries(from startDate: Date, mode: TimelineScheduleMode) -> PeriodicTimelineSchedule.Entries {
PeriodicTimelineSchedule(from: self.startDate, by: (mode == .lowFrequency ? 1.0 : 1.0 / 30.0))
.entries(from: startDate, mode: mode)
}
}
struct ElapsedTimeView: View {
var elapsedTime: TimeInterval = 0
var showSubseconds: Bool = false
var fontWeight: Font.Weight = .bold
@State private var timeFormatter = ElapsedTimeFormatter(showSubseconds: false)
var body: some View {
Text(NSNumber(value: elapsedTime), formatter: timeFormatter)
.fontWeight(fontWeight)
.onChange(of: showSubseconds) {
timeFormatter.showSubseconds = [=10=]
}
.onAppear(perform: {
timeFormatter = ElapsedTimeFormatter(showSubseconds: showSubseconds)
})
}
}
class ElapsedTimeFormatter: Formatter {
let componentsFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute, .second, .hour]
formatter.zeroFormattingBehavior = .pad
return formatter
}()
var showSubseconds: Bool
init(showSubseconds: Bool) {
self.showSubseconds = showSubseconds
super.init()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func string(for value: Any?) -> String? {
guard let time = value as? TimeInterval else {
return nil
}
guard let formattedString = componentsFormatter.string(from: time) else {
return nil
}
if showSubseconds {
let hundredths = Int((time.truncatingRemainder(dividingBy: 1)) * 100)
let decimalSeparator = Locale.current.decimalSeparator ?? "."
return String(format: "%@%@%0.2d", formattedString, decimalSeparator, hundredths)
}
return formattedString
}
}
struct SeizureView_Previews: PreviewProvider {
static var previews: some View {
SeizureView(apptCardVM: ApptCardViewModel())
}
}