2 votes

Mise à jour de l'état de SwiftUI dans le terrain de jeu de Xcode 11

Je n'ai pas réussi à trouver quoi que ce soit par le biais d'une recherche standard sur Google, mais y a-t-il une raison pour laquelle le ContentView n'est pas mis à jour par l'intermédiaire du ObservableObject ? J'ai l'impression qu'il me manque quelque chose, mais je ne sais pas exactement quoi.

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

2voto

Asperi Points 123157

La raison est que la minuterie basée sur le GCD fonctionne sur sa propre file d'attente, voici donc la solution - le modèle de vue doit être mis à jour sur la file d'attente principale, l'interface utilisateur et la file d'attente comme suit

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

0voto

Rob Points 70987

La principale utilité des minuteries GCD par rapport aux minuteries standard est qu'elles peuvent fonctionner sur une file d'attente en arrière-plan. Comme l'a dit Asperi, vous pouvez envoyer les mises à jour à la file d'attente principale si votre minuterie GCD n'utilise pas elle-même la file d'attente principale.

Mais vous pourriez tout aussi bien programmer votre minuteur GCD sur la file d'attente principale dès le départ, et vous n'auriez alors plus à dispatcher manuellement vers la file d'attente principale :

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

Mais si vous voulez exécuter cette opération sur le thread principal, vous pouvez simplement utiliser une fonction Timer ou, dans les projets SwiftUI, vous pouvez préférer Combine's 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)
}

Lorsque vous incorporez votre timer dans votre code SwiftUI (plutôt que dans un code global comme ici), il est agréable de rester dans le cadre de Combine Publisher paradigme.

Je pense également qu'il est probablement plus propre d'annuler la minuterie lorsqu'elle expire dans le gestionnaire de la minuterie, plutôt que de faire une opération séparée de asyncAfter .


Sans rapport avec ce qui précède, vous pourriez envisager d'utiliser DateComponentsFormatter dans votre timeRemaining par exemple :

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"
}

Ensuite,

  • Il vous dispense de calculer vous-même les minutes et les secondes ;
  • La chaîne sera localisée ; et
  • Il garantira une formulation grammaticalement correcte ; par exemple, lorsqu'il reste 61 secondes et que la routine existante signale un "1 minute, 1 seconde" grammaticalement incorrect.

DateComponentsFormatter vous permet de ne pas avoir à gérer ce genre de cas particuliers, où vous voulez du singulier au lieu du pluriel ou des langues autres que l'anglais.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X