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") } } } }
4 Réponses :
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()
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?
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
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.)
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) }
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.
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/...