Property mit Lady Schlüsselwort kann kein @Published Wrapper genutzt werden.

Diese Seite verwendet Cookies. Durch die Nutzung unserer Seite erklären Sie sich damit einverstanden, dass wir Cookies setzen. Weitere Informationen

  • Property mit Lady Schlüsselwort kann kein @Published Wrapper genutzt werden.

    Nabend zusammen,

    irgendwie komme ich einfach nicht weiter.
    Ich habe ein paar Propertys als lazy gesetzt, weil sie beim Start noch nicht zur Verfügung stehen.
    Wie bekomme ich es nun hin, dass diese auch aktualisiert werden, wenn sich Werte ändern.

    Ich vermute mal, dass ich den falschen Weg eingeschlagen habe, doch ich weiß nicht wo.

    Das sollte im beste Fall funktionieren:
    1. Anhand des aktuellen Datums und dem Zieldatum die Differenz in Tagen, Wochen und Monaten berechnen.
    2. Von der Anzahl, welche als Ziel deklariert werden, soll die aktuelle Anzahl abgezogen werden.
    3. Das ganze soll dann noch ausgerechnet werden wie viele dies pro Tag, pro Woche, pro Monat sind.

    Quellcode: StatistikView.swift

    1. struct StatistikView: View {
    2. @ObservedObject var statistik = Statistik()
    3. var body: some View {
    4. VStack{
    5. VStack{
    6. Text("Zieldatum: ")
    7. TextField("31.12.20222", text: $statistik.comparisonDateString)
    8. }
    9. VStack {
    10. Text("Aktuelles Ziel: ")
    11. TextField("", value: $statistik.goalValue, format: .number)
    12. .keyboardType(.decimalPad)
    13. }
    14. VStack{
    15. Text("Derzeitiger Stand: ")
    16. TextField("", value: $statistik.currentValue, format: .number)
    17. .keyboardType(.decimalPad)
    18. }
    19. VStack {
    20. Text("Pro Tag: \(statistik.perDays)" )
    21. Text("Pro Woche: \(statistik.perWeeks)")
    22. Text("Pro Monat: \(statistik.perMonths)")
    23. }
    24. }
    25. }
    26. }
    27. struct StatistikView_Previews: PreviewProvider {
    28. static var previews: some View {
    29. StatistikView()
    30. }
    31. }
    Alles anzeigen

    Quellcode: Statistik.swift

    1. import Foundation
    2. class Statistik : ObservableObject {
    3. var comparisonDateString = "31.12.2022"
    4. var goalValue = 3811
    5. var currentValue = 689
    6. let dateCalculator = DateCalculator()
    7. lazy var remainingDays = dateCalculator.calculatesDateDifference(today: Date(), comparisonDateString: comparisonDateString, dateComponent: .day) + 1
    8. lazy var remainingWeeks = remainingDays / 7
    9. lazy var remainingMonths = remainingDays / 30
    10. lazy var perDays = (goalValue - currentValue) / remainingDays
    11. lazy var perWeeks = (goalValue - currentValue) / remainingWeeks
    12. lazy var perMonths = (goalValue - currentValue) / remainingMonths
    13. }
    Alles anzeigen

    Quellcode: DateCalculator.swift

    1. import Foundation
    2. class DateCalculator{
    3. func calculatesDateDifference(today: Date, comparisonDateString : String, dateComponent : Calendar.Component) -> Int{
    4. let dateFormatter = DateFormatter()
    5. dateFormatter.dateFormat = "dd.MM.yyyy"
    6. let compareDate = dateFormatter.date(from: comparisonDateString)
    7. let difference = Calendar.current.dateComponents([dateComponent], from: today, to: compareDate!)
    8. return difference.day!
    9. }
    10. }
    Alles anzeigen
  • Hallo,

    wie auch bei deinem letzten Thread habe ich Schwierigkeiten zu verstehen, was genau das Problem ist. Zudem habe ich das Gefühl, dass du einige Konstrukte nutzt ohne zu wissen, welchen Zweck sie eigentlich dienen und dass dies zu Komplikationen führt. Im letzten Thread schriebst du z.B. "Über @ObservedObject gebe ich die Daten aus meinem Model an eine View weiter." - was meiner Meinung nach inhaltlich keinen Sinn ergibt. Jetzt schreibst du "Ich habe ein paar Propertys als lazy gesetzt, weil sie beim Start noch nicht zur Verfügung stehen.", was für mich ebenfalls nach einem Missverständnis klingt. Lazy properties nutzt du, wenn du properties hast, die erst dann initialisiert werden sollen, wenn sie aufgerufen / genutzt werden - das macht in deinem Fall aber wenig Sinn, da du sie ohnehin direkt nach Initialisierung in deiner View nutzt.

    Du wunderst dich, warum deine View sich nicht aktualisiert? Nun ja, wieso sollte sie auch? ;) Du hast in deiner Class ja überhaupt keine Logik, die beim Ändern der Werte ausgeführt wird und die fehlenden berechnet. Und selbst wenn du dies hättest, würde das die View nicht updaten da du keine @Published (sprich, die View erkennt nicht / wird nicht darüber informiert, ob sich Werte in deiner Class geändert haben).

    Ich würde dir folgendes empfehlen:
    - Lies ein paar Tutorials über SwiftUI. Normalerweise würde ich dir gerne mit Code-Beispielen etc. aushelfen, aber das, was du beschreibst, ist das simpelste SwiftUI-Pattern das es gibt. Versuch es doch erst einmal mit den vielen Tutorials, die es gibt, und frag noch mal nach, wenn du konkrete Fragen oder Probleme hast!
    - @ObservableObjects etc. verwirren am Anfang ein wenig. Ich würde daher vorschlagen, am Anfang einfach mal nur in der View selbst zu arbeiten. Erstelle keine neue Klasse, sondern bekomm dein Vorhaben erst einmal ausschließlich mit der "StatistikView" hin, also mit z.B. @State (auch deine DateCalculator-Klasse für eine einzige Funktion scheint eher überflüssig). Dann musst du dich zunächst nicht mit den @ObservableObject, @Published, oder sonstigem herumschlagen. Denn funktional ist das im Prinzip genau das Gleiche. Mit @ObservableObjects lagerst du das ganze lediglich aus und musst daher ein paar Fallstrippen beachten. Sobald du verstanden hast, wie du dein Vorhaben mit @States hinbekommst, ist es relativ simpel, das ganze in eine Model-Klasse auszulagern mittels @ObservableObject und @Published.

    Viele Grüße
  • Ich habe schon einige Tutorials durchgearbeitet und war der Meinung, dass ich es richtig verstanden und angewendet habe. Anscheinend habe ich doch ein paar Probleme.
    Deinem Rat folgend habe ich das ganze jetzt einmal in der StatistikView umgesetzt und da funktioniert es auch wie gehofft.

    Ist es jetzt richtig umgesetzt oder wo habe ich evtl. immer noch Verständnisprobleme?

    Quellcode: StatistikView.swift

    1. struct StatistikView: View {
    2. @State var comparingDateString = "31.12.2022"
    3. @State var goalValue = 3811
    4. @State var currentValue = 689
    5. func calculatesDateDifference(today: Date, comparisonDateString : String) -> Int{
    6. let dateFormatter = DateFormatter()
    7. dateFormatter.dateFormat = "dd.MM.yyyy"
    8. let compareDate = dateFormatter.date(from: comparisonDateString)
    9. let difference = Calendar.current.dateComponents([.day], from: today, to: compareDate!)
    10. return difference.day!
    11. }
    12. var body: some View {
    13. let targetValue = goalValue - currentValue
    14. let remainingDays = calculatesDateDifference(today: Date(), comparisonDateString: comparingDateString) + 1
    15. VStack{
    16. VStack{
    17. Text("Zieldatum: ")
    18. TextField("31.12.20222", text: $comparingDateString)
    19. }
    20. VStack {
    21. Text("Aktuelles Ziel: ")
    22. TextField("", value: $goalValue, format: .number)
    23. .keyboardType(.decimalPad)
    24. }
    25. VStack{
    26. Text("Derzeitiger Stand: ")
    27. TextField("", value: $currentValue, format: .number)
    28. .keyboardType(.decimalPad)
    29. }
    30. VStack {
    31. Text("Pro Tag: \(targetValue / remainingDays)" )
    32. Text("Pro Woche: \(targetValue / (remainingDays / 4))")
    33. Text("Pro Monat: \(targetValue / (remainingDays / 30))")
    34. }
    35. }
    36. }
    37. }
    38. struct StatistikView_Previews: PreviewProvider {
    39. static var previews: some View {
    40. StatistikView()
    41. }
    42. }
    Alles anzeigen
  • Zunächst einmal schön, dass es zumindest in dieser Variante funktioniert! Das sieht grundsätzlich ja ganz gut aus, ein paar kleine Anmerkungen, die sicher auch zum Teil Geschmacksache sind:
    • Den body versuche ich immer clean zu halten. Dort landet bei mir nur in Ausnahmefällen imperativer Code wie bei dir das let targetValue = ... und let remainingDays = .... Diese zwei Werte würde ich über Computed Properties generieren. Das ist meiner Meinung nach übersichtlicher.
    • Hat es einen spezifischen Grund, weshalb du für die Datumseingabe ein Textfeld nutzt und kein DatePicker? Vorteil hier ist, dass du direkt ein Date erhältst und du dir das mühsame Umkonvertieren sparen kannst.
    • Wenn du es aber via Textfeld machst, würde ich bei der Konvertierung sehr vorsichtig sein - konkret heißt das, es sollte in der Funktion calculatesDateDifference kein Force Unwrapping (also ein !) auftauchen. Denn bei falscher User-Eingabe ist es durchaus möglich, dass das compareDate nil ist (in diesem Fall würde deine App sofort crashen). Stattdessen verwende ein guard let oder if let Statement für ein sicheres Unwrapping.
    • Die Funktion würde ich allerdings ohnehin als Computed Property implementieren (à la var remainingDays: Int { ... return x }. Da sie von comparingDateString, abhängt, was ein State ist, würde die View sich automatisch aktualisieren, hier keine Sorge.
    • Ich persönlich setzte die States immer auf private (da der State nur View-intern genutzt werden sollte). Das finde ich architektonisch sauberer, aber wie gesagt, sicherlich auch Geschmacksache.
    Nun zu deinem Ursprungsgedanken - das ganze in eine Klasse auszulagern: Bei dieser Größenordnung scheint das vielleicht nicht unbedingt notwendig. Aber nur mal aus Prinzip würdest du dann in etwa wie folgt vorgehen:
    • Erstelle deine Klasse (ich erstelle diese übrigens oft gerne innerhalb einer Extension der View, um den Scope sauber zu halten. Von Außerhalb kannst du z.B. über let viewModel = StatistikView.ViewModel() zugreifen) und mach sie ObservableObject-conformable, so wie du es in deinem Eingangsposting bereits gemacht hast.
    • Jedes Variable, die du in deiner View als @State markiert hast, wandert nun in die neue Klasse und wird als @Published markiert, um den gleichen Effekt zu behalten. Du kannst dir das so vorstellen: Die Klasse ist ja ein Referenz-Typ, das heißt die View kennt jetzt ja erst einmal lediglich die Adresse der Instanz, aber kriegt nicht mit, wenn sich innerhalb dieser etwas ändert. Genau hier hilft das @Published, dieses signalisiert bei jeder Änderung der Variable, dass die Klasseninstanz sich geändert hat. Das kriegt die View nun dank des @ObservedObject keywords mit und kann dementsprechend die View updaten. Also eigentlich genau wie mit States, nur mit einem Zwischenschritt.
    • Jetzt musst du natürlich eine Variable für dein ViewModel (ich nenne es ViewModel, in deinem Beispiel natürlich die Klasse Statisik) in deiner View deklarieren. Aber Vorsicht, Fallstrippe! Folgendes: Üblich ist es, dass du lediglich @ObservedObject var statistik: Statisik schreibst, ohne einen Wert zuzuweisen. Das macht man nämlich dann, wenn man deine View aus einer anderen View heraus instanziiert, also z.B. dann StatistikView(statistik: Statistik()) schreibt. Warum? Naja, du willst ja in der Regel außerhalb deiner StatistikView auf das Model (= die Statistik Instanz) zugreifen, und deshalb musst du es auch in der Parent-View instanziieren und dann der View übergeben und nicht in der View selbst erstellen, wo du anschließend von außerhalb keinen (bzw. unschönen) Zugriff darauf hast. Fallstrippe habe ich aus dem folgenden Grund geschrieben. Wenn man, so wie du in deinem Eingangsposting, @ObservedObject var statistik = Statistik() schreibt, dann hat das zur Folge, dass deine Statistik-Instanz bei jedem Neu-Rendern der View auch neu instanziiert wird - sprich all deine Daten in statistik sind weg bzw. wieder beim Ursrpungszustand. Genau hierfür gibt es *trommelwirbel* @StateObject. Das ist im Prinzip genau das Gleiche wie @ObservedObject mit dem entscheidenden Unterschied, dass es beim Neurendern der View nicht verworfen wird. Da du scheinbar deine statistik Instanz von außerhalb der View nicht benötigst, wäre das evtl. für deinen Fall passend (wobei ich persönlich den Approach mittels @ObservedObject und Instanziierung in der Parent-View tatsächlich ordentlicher und fehler-unanfälliger finde).


    So, genug geschrieben, ich hoffe ich konnte ein paar Gedanken anregen. VG
  • Osxer schrieb:


    • Fallstrippe habe ich aus dem folgenden Grund geschrieben. Wenn man, so wie du in deinem Eingangsposting, @ObservedObject var statistik = Statistik() schreibt, dann hat das zur Folge, dass deine Statistik-Instanz bei jedem Neu-Rendern der View auch neu instanziiert wird - sprich all deine Daten in statistik sind weg bzw. wieder beim Ursrpungszustand. Genau hierfür gibt es *trommelwirbel* @StateObject. Das ist im Prinzip genau das Gleiche wie @ObservedObject mit dem entscheidenden Unterschied, dass es beim Neurendern der View nicht verworfen wird. Da du scheinbar deine statistik Instanz von außerhalb der View nicht benötigst, wäre das evtl. für deinen Fall passend (wobei ich persönlich den Approach mittels @ObservedObject und Instanziierung in der Parent-View tatsächlich ordentlicher und fehler-unanfälliger finde).

    Schöner Post. Nur das obige ist Falsch. Der Unterschied zwischen den alten und den neuen Property Wrapper ist, dass der alte ein paar Bugs enthält, welche nur sehr schwer nachvollziehbar sind. Daher wird bei der instanzierung eines Objektes in der View der Neue @StateObject empfohlen. Der alte bleibt nach wie vor, bei der Deklaration erhalten.
  • #Wolf Das ist falsch. Da sind keine Bugs in der Implementierung. Die Frage ist schlicht, wie verhaelt sich das Object, wenn der View neugezeichnet wird.

    Die Initialisierung des Objects kann! von aussen in den View hineingereicht werden ...
    und hält eventuell nicht! den state, wenn der View neugezeichnet wird

    Quellcode

    1. @ObservedObject

    Die Initialisierung des Objects wird immer! in dem View vorgenommen ...
    und hält den state, auch wenn der View neugezeichnet wird

    Quellcode

    1. @StateObject
    Osxer war da schon auf dem richtigen Weg :)
  • Sehe ich nicht so. Unter SwiftUI (I), gab es keinen Property Wrapper "@StateObject". Hat auch gut funktioniert. Es gab aber ein paar erratische Fehler, welche kaum nachzuvollziehen
    waren. Dies hat dann zur Entwicklung von @StateObject unter SwiftUI (II) geführt. Beide hatten den selben Anwendungsbereich. Beide gibt es noch. Es gab nur eine Empfehlung bei neuen Code @StateObject anstatt @ObservedObject zu verwenden. Beide werden in einer View auch nur einmalig erstellt. Daher die obige Aussage auch falsch. Was nicht bedeutet, dass @StateObject für neu zu instantizierende Objekte nicht die bessere Wahl ist.

    Um zum Thema Statistik etwas zu sagen, würde ich dieses Objekt entweder als @EnvironmentObject oder als Singleton implementieren.