4
votes

Passer des données entre deux vues

Je voulais créer une application simple et silencieuse sur watchOS 6, mais après qu'Apple ait changé l'ObjectBindig dans Xcode 11 beta 5, mon application ne fonctionne plus. Je veux simplement synchroniser les données entre deux vues.

J'ai donc réécrit mon application avec le nouveau @Published, mais je ne peux pas vraiment la configurer:

class UserInput: ObservableObject {

    @Published var score: Int = 0
}

struct ContentView: View {
    @ObservedObject var input = UserInput()
    var body: some View {
        VStack {
            Text("Hello World\(self.input.score)")
            Button(action: {self.input.score += 1})
                {
                    Text("Adder")
                }
            NavigationLink(destination: secondScreen()) {
                Text("Next View")
            }

        }

    }
}

struct secondScreen: View {
    @ObservedObject var input = UserInput()
    var body: some View {
        VStack {
            Text("Button has been pushed \(input.score)")
            Button(action: {self.input.score += 1
            }) {
                Text("Adder")
            }
        }

    }
}


6 commentaires

Je sais que les sites de notes de publication qui objectWillChange seront automatiquement émis, mais ce n'est que lorsque je l'ai explicitement codé que mes trucs ont fonctionné. Avez-vous essayé cela?


Une duplication possible de SwiftUI onTapGesture ne fonctionne pas avec ObservedObject dans l'application Mac


Vous n'avez pas besoin d'un objet observable car un @State Int fonctionnera également.


J'ai modifié ma réponse avec plus de détails.


Utilisez ce que superpuccio a dit ou appelez ContentView().environmentObject(UserInput()) et déclarez l'entrée comme @EnvironmentObject var input: User Input , auquel cas l'environnement .environmentObject distribue l'objet créé à tous les enfants.


stackoverflow.com/questions/62635914/...


4 Réponses :


7
votes

Votre code comporte quelques erreurs:

1) Vous n'avez pas placé votre ContentView dans un NavigationView , donc la navigation entre les deux vues n'a jamais eu lieu.

2) Vous avez mal utilisé la liaison de données. Si vous avez besoin que la deuxième vue repose sur un état appartenant à la première vue, vous devez transmettre une liaison à cet état à la deuxième vue. À la fois dans votre première vue et dans votre deuxième vue, vous @ObservedObject créé un @ObservedObject ligne:

import SwiftUI

class UserInput: ObservableObject {
    @Published var score: Int = 0
}

struct ContentView: View {
    @ObservedObject var input = UserInput() //please, note the difference between this...
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello World\(self.input.score)")
                Button(action: {self.input.score += 1})
                    {
                        Text("Adder")
                    }
                NavigationLink(destination: secondScreen(input: self.input)) {
                    Text("Next View")
                }

            }
        }

    }
}

struct secondScreen: View {
    @ObservedObject var input: UserInput //... and this!
    var body: some View {
        VStack {
            Text("Button has been pushed \(input.score)")
            Button(action: {self.input.score += 1
            }) {
                Text("Adder")
            }
        }

    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

ainsi, la première vue et la seconde ont fonctionné avec deux objets totalement différents. Au lieu de cela, vous souhaitez partager le score entre les vues. Laissez la première vue posséder l'objet UserInput et transmettez simplement une liaison à l'entier de score à la deuxième vue. De cette façon, les deux vues fonctionneront sur la même valeur (vous pouvez copier-coller le code ci-dessous et essayer vous-même).

import SwiftUI

class UserInput: ObservableObject {
    @Published var score: Int = 0
}

struct ContentView: View {
    @ObservedObject var input = UserInput()
    var body: some View {
        NavigationView {
            VStack {
                Text("Hello World\(self.input.score)")
                Button(action: {self.input.score += 1})
                    {
                        Text("Adder")
                    }
                NavigationLink(destination: secondScreen(score: self.$input.score)) {
                    Text("Next View")
                }

            }
        }

    }
}

struct secondScreen: View {
    @Binding var score:  Int
    var body: some View {
        VStack {
            Text("Button has been pushed \(score)")
            Button(action: {self.score += 1
            }) {
                Text("Adder")
            }
        }

    }
}

#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
#endif

Si vous en avez vraiment besoin, vous pouvez même passer tout l'objet UserInput à la deuxième vue:

@ObservedObject var input = UserInput()


1 commentaires

hey, ça vous dérangerait d'expliquer comment ça marche? le premier exemple que le score a été passé à la deuxième vue. le score est le type de valeur. la valeur doit être copiée dans la deuxième vue. pourquoi cela affecterait-il la première vue?



0
votes

Ma question est toujours liée à la façon de passer des données entre deux vues, mais j'ai un ensemble de données JSON plus compliqué et je rencontre des problèmes à la fois avec le passage des données et avec leur initialisation. J'ai quelque chose qui fonctionne mais je suis sûr que ce n'est pas correct. Voici le code. Aidez-moi!!!!

/ File: simpleContentView.swift
import SwiftUI
// Following is the more complicated @ObservedObject (Buddy and class Buddies)
struct Buddy : Codable, Identifiable, Hashable {
    var id = UUID()
    var TheirNames: TheirNames
    var dob: String = ""
    var school: String = ""
    enum CodingKeys1: String, CodingKey {
        case id = "id"
        case Names = "Names"
        case dob = "dob"
        case school = "school"
    }
}
struct TheirNames : Codable, Identifiable, Hashable {
    var id = UUID()
    var first: String = ""
    var middle: String = ""
    var last: String = ""

    enum CodingKeys2: String, CodingKey {
        case id = "id"
        case first = "first"
        case last = "last"
    }
}

class Buddies: ObservableObject {
    @Published var items: [Buddy] {
        didSet {
            let encoder = JSONEncoder()
            if let encoded = try? encoder.encode(items) {UserDefaults.standard.set(encoded, forKey: "Items")}
        }
    }
    @Published var buddy: Buddy
    init() {
        if let items = UserDefaults.standard.data(forKey: "Items") {
            let decoder = JSONDecoder()
            if let decoded = try? decoder.decode([Buddy].self, from: items) {
                self.items = decoded
                // ??? How to initialize here
                self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
                return
            }
        }
        // ??? How to initialize here
        self.buddy = Buddy(TheirNames: TheirNames(first: "c", middle: "r", last: "c"), dob: "1/1/1900", school: "hard nocks")
        self.items = []
    }
}

struct simpleContentView: View {
    @Environment(\.presentationMode) var presentationMode
    @State private var showingSheet = true
    @ObservedObject var buddies = Buddies()
    var body: some View {
        VStack {
            Text("Simple View")
            Button(action: {self.showingSheet.toggle()}) {Image(systemName: "triangle")
            }.sheet(isPresented: $showingSheet) {
                simpleDetailView(buddies: self.buddies, item: self.buddies.buddy)}
        }
    }
}

struct simpleContentView_Previews: PreviewProvider {
    static var previews: some View {
        simpleContentView()
    }
}
// End of File: simpleContentView.swift
// This is in a separate file: simpleDetailView.swift
import SwiftUI

struct simpleDetailView: View {
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var buddies = Buddies()
    var item: Buddy
    var body: some View {
        VStack {
            Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
            Text("First Name = \(item.TheirNames.first)")
            Button(action: {self.presentationMode.wrappedValue.dismiss()}){ Text("return"); Image(systemName: "gobackward")}
        }
    }
}
// ??? Correct way to make preview call
struct simpleDetailView_Previews: PreviewProvider {
    static var previews: some View {
        // ??? Correct way to call here
        simpleDetailView(item: Buddy(TheirNames: TheirNames(first: "", middle: "", last: ""), dob: "", school: "") )
    }
}
// end of: simpleDetailView.swift


1 commentaires

comme vous devez analyser et gérer une structure complexe, ce qui ne sera donc pas possible avec les propriétés State, vous devrez éventuellement utiliser Published pour obtenir le comportement souhaité. J'ai publié ma réponse ci-dessous, essayez cela et voyez si cela fonctionne pour vous ou non. Je l'ai modifié pour mon propre usage avec (c'est-à-dire pour les structures codables.)



0
votes

L'utilisation directe de la variable @State vous aidera à y parvenir, mais si vous souhaitez synchroniser cette variable pour les deux écrans en utilisant le modèle de vue ou @Published, c'est ce que vous pouvez faire. Comme @State ne sera pas lié à la propriété @Published. Pour ce faire, procédez comme suit.

Étape 1: - Créez un délégué pour lier la valeur sur pop ou disparaître.

final class ViewModel : ObservableObject {

@Published var score : Int

   init(_ value : Int) {
       self.score = value
   }
}

struct secondScreen: View {

@Binding var score:  Int
@Binding var navIndex : Int?

@ObservedObject private var vm : ViewModel

var delegate  : BindingDelegate?

init(score : Binding<Int>, del : BindingDelegate, navIndex : Binding<Int?>) {
    self._score = score
    self._navIndex = navIndex
    self.delegate = del
    self.vm = ViewModel(score.wrappedValue)
}

private var btnBack : some View { Button(action: {
    self.delegate?.updateOnPop(value: self.vm.score)
    self.navIndex = nil
}) {
    HStack {
        Text("Back")
    }
    }
}

var body: some View {
    VStack {
        Text("Button has been pushed \(vm.score)")
        Button(action: {
            self.vm.score += 1
        }) {
            Text("Adder")
        }
    }
    .navigationBarBackButtonHidden(true)
    .navigationBarItems(leading: btnBack)

   }
}

Étape 2: - Suivez la base de code pour l'affichage du contenu

 class UserInput: ObservableObject {
      @Published var score: Int = 0
 }

 struct ContentView: View , BindingDelegate {

   @ObservedObject var input = UserInput()

   @State var navIndex : Int? = nil

   var body: some View {
       NavigationView {
          VStack {
              Text("Hello World\(self.input.score)")
              Button(action: {self.input.score += 1}) {
                    Text("Adder")
                }

            ZStack {
                NavigationLink(destination: secondScreen(score: self.$input.score,
                                                         del: self, navIndex: $navIndex),
                                                         tag: 1, selection: $navIndex) {
                    EmptyView()
                }

                Button(action: {
                    self.navIndex = 1
                }) {
                    Text("Next View")

                }
            }
        }
    }
}

   func updateOnPop(value: Int) {
       self.input.score = value
   }
}

Étape 3: Suivez ces étapes pour secondScreen

 protocol BindingDelegate {
     func updateOnPop(value : Int)
 }


0 commentaires

0
votes

J'ai essayé de nombreuses approches différentes sur la façon de transmettre des données d'une vue à une autre et j'ai trouvé une solution qui convient aux modèles de vues / vues simples et complexes.

Version

class SubviewModel: ObservableObject {
    
    @Published var subViewText: String?
    
    func updateText(text: String?) {
        self.subViewText = text
    }
}

Cette solution fonctionne avec iOS 14.0 vers le haut, car vous avez besoin du modificateur de vue .onChange() . L'exemple est écrit dans Swift Playgrounds. Si vous avez besoin d' un onChange comme modificateur pour les versions inférieures, vous devez écrire votre propre modificateur.

Vue principale

La vue principale a un @StateObject viewModel gérant toute la logique des vues, comme le @StateObject viewModel sur le bouton et les "données" (testingID: String) -> Vérifiez le ViewModel

.onAppear(perform: { self.viewModel.updateText(text: test) })

Modèle de vue principale (ViewModel)

Le viewModel publie un testID: String? . Ce testID peut être n'importe quel type d'objet (par exemple, un objet de configuration, vous le nommez), pour cet exemple, il s'agit simplement d'une chaîne également nécessaire dans la vue secondaire.

.onChange(of: self.test) { (text) in
                self.viewModel.updateText(text: text)
            }

Ainsi, en appuyant sur le bouton, notre ViewModel mettra à jour le testID . Nous voulons également ce testID dans notre SubView et s'il change, nous voulons également que notre SubView reconnaisse et gère ces changements. Grâce au ViewModel @Published var testingID nous sommes en mesure de publier des modifications dans notre vue. Jetons maintenant un œil à notre SubView et SubViewModel .

Sous-vue

Ainsi, le SubView a son propre @StateObject pour gérer sa propre logique. Il est complètement séparé des autres vues et ViewModels. Dans cet exemple, le SubView présente uniquement le testID de sa MainView . Mais rappelez-vous, il peut s'agir de tout type d'objet comme des préréglages et des configurations pour une requête de base de données.

struct SubView: View {
    
    @StateObject var viewModel: SubviewModel = .init()
    
    @Binding var test: String?
    init(text: Binding<String?>) {
        self._test = text
    }
    
    var body: some View {
        Text(self.viewModel.subViewText ?? "no text")
            .onChange(of: self.test) { (text) in
                self.viewModel.updateText(text: text)
            }
            .onAppear(perform: { self.viewModel.updateText(text: test) })
    }
}

Pour "connecter" notre testingID publié par notre MainViewModel nous initialisons notre SubView avec un @Binding . Nous avons donc maintenant le même testingID dans notre SubView . Mais nous ne voulons pas l'utiliser directement dans la vue, nous devons plutôt passer les données dans notre SubViewModel , rappelez-vous que notre SubViewModel est un @StateObject pour gérer toute la logique. Et nous ne pouvons pas passer la valeur dans notre @StateObject lors de l'initialisation de la vue. De plus, si les données ( testingID: String ) changent dans notre MainViewModel , notre SubViewModel doit reconnaître et gérer ces changements.

Par conséquent, nous utilisons deux ViewModifiers .

sur le changement

final class ViewModel: ObservableObject {
    
    @Published var testingID: String?
    
    func didTapButton() {
        self.testingID = UUID().uuidString
    }
    
}

Le modificateur onChange est abonnée à des changements dans notre @Binding propriété. Donc, si cela change , ces changements sont transmis à notre SubViewModel . Notez que votre propriété doit être équatable . Si vous passez un objet plus complexe, comme un Struct , assurez-vous d' implémenter ce protocole dans votre Struct .

onAppear

Nous avons besoin de onAppear pour gérer les "premières données initiales" car onChange ne se déclenche pas la première fois que votre vue est initialisée. Ce n'est que pour les changements .

struct TestMainView: View {
    
    @StateObject var viewModel: ViewModel = .init()
    
    var body: some View {
        VStack {
            Button(action: { self.viewModel.didTapButton() }) {
                Text("TAP")
            }
            Spacer()
            SubView(text: $viewModel.testingID)
        }.frame(width: 300, height: 400)
    }
    
}

Ok et voici le SubViewModel , rien de plus à expliquer à celui-ci je suppose.

Apple Swift version 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)

Maintenant , vos données sont synchronisées entre votre MainViewModel et SubViewModel et cette approche fonctionne pour les grandes vues avec de nombreux sous - vues et subviews de ces sous - vues et ainsi de suite. Il conserve également vos vues et les viewModels correspondants inclus avec une réutilisabilité élevée.

Exemple de travail

Playground sur GitHub: https://github.com/luca251117/PassingDataBetweenViewModels

Notes complémentaires

Pourquoi j'utilise onAppear et onChange au lieu de seulement onReceive : Il semble que le remplacement de ces deux modificateurs avec onReceive conduit à un flux continu de données mise à feu des SubViewModel updateText plusieurs fois. Si vous avez besoin de diffuser des données pour la présentation, cela peut être bien, mais si vous souhaitez gérer des appels réseau par exemple, cela peut entraîner des problèmes. C'est pourquoi je préfère "l'approche à deux modificateurs".

Note personnelle: veuillez ne pas modifier le StateObject en dehors de la portée de la vue correspondante. Même si c'est possible d'une manière ou d'une autre, ce n'est pas ce à quoi cela sert.


0 commentaires