Представление таймера SwiftUI останавливает все другие представления на экране - PullRequest
0 голосов
/ 26 апреля 2020

Мне любопытно, есть ли способ заставить таймеры обновляться с помощью пользовательского интерфейса (как сейчас), даже когда кто-то прокручивает. Кроме того, я хочу убедиться, что при обновлении интерфейса каждую секунду экран не зависает. Этот вопрос был обновлен в ответ на полезные ответы, которые я получил ранее.

struct ContentView: View {
    var body: some View {
        NavigationView{
            NavigationLink(destination: ScrollTest()){
                HStack {
                     Text("Next Screen")
                }
            }
        }
    }
}
struct ScrollTest: View {
    @ObservedObject var timer = SectionTimer(duration: 60)
    var body: some View {
        HStack{
            List{
                Section(header: TimerNavigationView(timer: timer)){
                    ForEach((1...50).reversed(), id: \.self) {
                        Text("\($0)…").onAppear(){
                            self.timer.startTimer()
                        }

                    }
                }
            }.navigationBarItems(trailing: TimerNavigationView(timer: timer))
        }

    }
}
struct TimerNavigationView: View {
    @ObservedObject var timer: SectionTimer
    var body: some View{
        HStack {
            Text("\(timer.timeLeftFormatted) left")
            Spacer()
        }
    }
}
class SectionTimer:ObservableObject {
    private var endDate: Date
    private var timer: Timer?
    var timeRemaining: Double {
        didSet {
            self.setRemaining()
        }
    }
    @Published var timeLeftFormatted = ""

    init(duration: Int) {
        self.timeRemaining = Double(duration)
        self.endDate = Date().advanced(by: Double(duration))
        self.startTimer()

    }

    func startTimer() {
        guard self.timer == nil else {
            return
        }
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
            self.timeRemaining = self.endDate.timeIntervalSince(Date())
            if self.timeRemaining < 0 {
                timer.invalidate()
                self.timer = nil
            }
        })
    }

    private func setRemaining() {
        let min = max(floor(self.timeRemaining / 60),0)
        let sec = max(floor((self.timeRemaining - min*60).truncatingRemainder(dividingBy:60)),0)
        self.timeLeftFormatted = "\(Int(min)):\(Int(sec))"
    }

    func endTimer() {
        self.timer?.invalidate()
        self.timer = nil
    }
}

1 Ответ

1 голос
/ 26 апреля 2020

Несмотря на то, что в SwiftUI есть таймер, я не думаю, что его использование является правильным подходом в этом случае.

Ваша модель должна обрабатывать время для вас.

Также помогает, если ваше представление непосредственно наблюдает за своим объектом модели, вместо того, чтобы пытаться наблюдать элемент массива в свойстве вашей наблюдаемой.

Вы не показали свой SectionTimer, но это то, что я создал:

class SectionTimer:ObservableObject {
    private var endDate: Date
    private var timer: Timer?
    var timeRemaining: Double {
        didSet {
            self.setRemaining()
        }
    }

    @Published var timeLeftFormatted = ""

    init(duration: Int) {
        self.timeRemaining = Double(duration)
        self.endDate = Date().advanced(by: Double(duration))
        self.startTimer()

    }

    func startTimer() {
        guard self.timer == nil else {
            return
        }
        self.timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { (timer) in
            self.timeRemaining = self.endDate.timeIntervalSince(Date())
            if self.timeRemaining < 0 {
                timer.invalidate()
                self.timer = nil
            }
        })
    }

    private func setRemaining() {
        let min = max(floor(self.timeRemaining / 60),0)
        let sec = max(floor((self.timeRemaining - min*60).truncatingRemainder(dividingBy:60)),0)
        self.timeLeftFormatted = "\(Int(min)):\(Int(sec))"
    }

    func endTimer() {
        self.timer?.invalidate()
        self.timer = nil
    }
}

Он использует Date вместо вычитания из оставшегося счетчика; это более точно, поскольку таймер не тикает через определенные промежутки времени. Он обновляет опубликованное свойство timeLeftFormatted.

Чтобы использовать его, я внес следующие изменения в ваш TimerNavigationView -

struct TimerNavigationView: View {
    @ObservedObject var timer: SectionTimer
    var body: some View{
        HStack {
            Text("\(timer.timeLeftFormatted) left")
            Spacer()
        }
    }
}

Вы можете видеть, как значительно упрощается установка таймера в модели. ваш взгляд.

Вы будете использовать его через .navigationBarItems(trailing: TimerNavigationView(timer: self.test.timers[self.test.currentSection]))

Обновление

Обновленный код в вопросе помог продемонстрировать проблему, и я нашел решение в этом ответе

Когда режим прокрутки прокручивается, режим текущего RunLoop изменяется и таймер не запускается.

Решение заключается в планировании таймер в режиме common самостоятельно, а не в режиме default, который вы получаете с помощью scheduledTimer -

func startTimer() {
    guard self.timer == nil else {
        return
    }

    self.timer = Timer(timeInterval: 0.2, repeats: true) { (timer) in
        self.timeRemaining = self.endDate.timeIntervalSince(Date())
        if self.timeRemaining < 0 {
            timer.invalidate()
            self.timer = nil
        }
    }
    RunLoop.current.add(self.timer!, forMode: .common)
} 
...