Dans ma vue SwiftUI, je dois déclencher une action lorsqu'un Toggle () change d'état. La bascule elle-même ne prend qu'une liaison. J'ai donc essayé de déclencher l'action dans le didSet de la variable @State. Mais le didSet n'est jamais appelé.
Existe-t-il un (autre) moyen de déclencher une action? Ou un moyen d'observer le changement de valeur d'une variable @State?
Mon code ressemble à ceci:
struct PWSDetailView : View { @ObjectBinding var station: PWS @State var isDisplayed: Bool = false { didSet { if isDisplayed != station.isDisplayed { PWSStore.shared.toggleIsDisplayed(station) } } } var body: some View { VStack { ZStack(alignment: .leading) { Rectangle() .frame(width: UIScreen.main.bounds.width, height: 50) .foregroundColor(Color.lokalZeroBlue) Text(station.displayName) .font(.title) .foregroundColor(Color.white) .padding(.leading) } MapView(latitude: station.latitude, longitude: station.longitude, span: 0.05) .frame(height: UIScreen.main.bounds.height / 3) .padding(.top, -8) Form { Toggle(isOn: $isDisplayed) { Text("Wetterstation anzeigen") } } Spacer() }.colorScheme(.dark) } }
Le comportement souhaité serait que l'action «PWSStore.shared.toggleIsDisplayed (station)» soit déclenchée lorsque le Toggle () change son état.
14 Réponses :
Tout d'abord, savez-vous réellement que les notifications KVO supplémentaires pour station.isDisplayed
posent un problème? Rencontrez-vous des problèmes de performances? Sinon, ne vous inquiétez pas.
Si vous rencontrez des problèmes de performance et vous avez établi qu'ils sont dus à trop station.isDisplayed
notifications KVO, la prochaine chose à faire est d' éliminer les notifications KVO inutiles. Pour ce faire, passez aux notifications KVO manuelles.
Ajoutez cette méthode à la définition de classe de la station
:
@objc dynamic var isDisplayed = false { willSet { if isDisplayed != newValue { willChangeValue(for: \.isDisplayed) } } didSet { if isDisplayed != oldValue { didChangeValue(for: \.isDisplayed) } } }
Et utilisez les observateurs willSet
et didSet
de Swift pour notifier manuellement les observateurs KVO, mais uniquement si la valeur change:
@objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool { return false }
Merci, Rob! Votre première ligne de code a déjà fait le travail. @objc class var automaticallyNotifiesObserversOfIsDisplayed: Bool { return false }
Je ne comprends pas complètement les mécanismes en arrière-plan (et la documentation Apple n'a pas beaucoup aidé), mais il semble que cette ligne ne fait que faire taire certaines notifications. Lorsqu'une instance de la classe PWS est créée ou lorsqu'une valeur pour isDisplayed
est définie (mais pas modifiée), aucune notification n'est envoyée. Mais lorsqu'une vue SwiftUI change réellement la valeur de isDisplayed
, il y a toujours une notification. Pour mon application, c'est exactement le comportement dont j'ai besoin.
Vous pouvez essayer ceci (c'est une solution de contournement):
func toggleAction(state: String, index: Int) -> String { print("The switch no. \(index) is \(state)") return "" }
Et en dessous, créez une fonction comme celle-ci:
@State var isChecked: Bool = true @State var index: Int = 0 Toggle(isOn: self.$isChecked) { Text("This is a Switch") if (self.isChecked) { Text("\(self.toggleAction(state: "Checked", index: index))") } else { CustomAlertView() Text("\(self.toggleAction(state: "Unchecked", index: index))") } }
Je pense que ça va
struct ToggleModel { var isWifiOpen: Bool = true { willSet { print("wifi status will change") } } } struct ToggleDemo: View { @State var model = ToggleModel() var body: some View { Toggle(isOn: $model.isWifiOpen) { HStack { Image(systemName: "wifi") Text("wifi") } }.accentColor(.pink) .padding() } }
class PWSStore : ObservableObject { ... var station: PWS @Published var isDisplayed = true { willSet { PWSStore.shared.toggleIsDisplayed(self.station) } } } struct PWSDetailView : View { @ObservedObject var station = PWSStore.shared ... var body: some View { ... Toggle(isOn: $isDisplayed) { Text("Wetterstation anzeigen") } ... } } Demo here https://youtu.be/N8pL7uTjEFM
J'ai trouvé une solution plus simple, utilisez simplement onTapGesture: D
Toggle(isOn: $stateChange) { Text("...") } .onTapGesture { // Any actions here. }
Il se déclenchera également, même lorsque le texte est tapé. Je pense que ce n'est pas une bonne solution.
Voici mon approche. J'étais confronté au même problème, mais j'ai plutôt décidé d'envelopper UISwitch d'UIKit dans une nouvelle classe conforme à UIViewRepresentable.
struct PWSDetailView : View { @State var isDisplayed: Bool = false @ObservedObject var station: PWS ... var body: some View { ... UIToggle(isOn: $isDisplayed) { isOn in //Do something here with the bool if you want //or use "_ in" instead, e.g. if isOn != station.isDisplayed { PWSStore.shared.toggleIsDisplayed(station) } } ... } }
Et puis il est utilisé comme ceci:
import SwiftUI final class UIToggle: UIViewRepresentable { @Binding var isOn: Bool var changedAction: (Bool) -> Void init(isOn: Binding<Bool>, changedAction: @escaping (Bool) -> Void) { self._isOn = isOn self.changedAction = changedAction } func makeUIView(context: Context) -> UISwitch { let uiSwitch = UISwitch() return uiSwitch } func updateUIView(_ uiView: UISwitch, context: Context) { uiView.isOn = isOn uiView.addTarget(self, action: #selector(switchHasChanged(_:)), for: .valueChanged) } @objc func switchHasChanged(_ sender: UISwitch) { self.isOn = sender.isOn changedAction(sender.isOn) } }
Pour les approches @Philipp Serflings: Joindre un TapGestureRecognizer n'était pas une option pour moi, car il ne se déclenche pas lorsque vous effectuez un "balayage" pour basculer le Toggle. Et je préférerais ne pas perdre sur la fonctionnalité de l'UISwitch. Et utiliser une liaison comme proxy fait l'affaire, mais je ne pense pas que ce soit une façon SwiftUI de le faire, mais cela pourrait être une question de goût. Je préfère les fermetures dans la déclaration View elle-même
Basé sur la réponse de @Legolas Wang.
Lorsque vous masquez l'étiquette d'origine de la bascule, vous pouvez attacher le tapGesture uniquement à la bascule elle-même
HStack { Text("...") Spacer() Toggle("", isOn: $stateChange) .labelsHidden() .onTapGesture { // Any actions here. } }
La meilleure solution ici! FYI - onTap est appelé avant que l'état isOn ne change réellement, donc j'ai également dû ajouter un délai de 0,1 seconde à l'action onTap pour que l'état isOn ait le temps de basculer avant que l'action ne soit appelée. Merci!
L'approche la plus propre à mon avis est d'utiliser une liaison personnalisée. Avec cela, vous avez un contrôle total sur le moment où la bascule doit basculer
import SwiftUI struct ToggleDemo: View { @State private var isToggled = false var body: some View { let binding = Binding( get: { self.isToggled }, set: { potentialAsyncFunction($0) } ) func potentialAsyncFunction(_ newState: Bool) { //something async self.isToggled = newState } return Toggle("My state", isOn: binding) } }
Beaucoup d'erreurs si j'ai déjà un ZStacks et VStacks ... essayé de mettre à l'intérieur / à l'extérieur - seulement des erreurs
C'est la bonne solution à ce problème. Il n'y a aucune raison de fouiller avec les notifications KVO.
Voici une version sans utiliser tapGesture.
@State private var isDisplayed = false Toggle("", isOn: $isDisplayed) .onReceive([self.isDisplayed].publisher.first()) { (value) in print("New value is: \(value)") }
Cela a l'air génial. Aucune liaison supplémentaire requise.
C'est sympa! Vous pouvez également faire .onReceive(Just(isDisplayed)) { value in … }
Je me demande pourquoi vous mettez self.isDisplayed entre crochets carrés et ajoutez .publisher.first (). Dans le cas d'un ObservedObject au lieu de State, vous écrirez nameOfObject. $ IsDisplayed à la place. Au moins, cela semble fonctionner dans mon cas.
Je crois que ce code est déclenché chaque fois que la variable d'état est modifiée pour une raison quelconque?
Juste au cas où vous ne voudriez pas utiliser de fonctions supplémentaires, gâtez la structure - utilisez des états et utilisez-les où vous le souhaitez. Je sais que ce n'est pas une réponse à 100% pour le déclencheur d'événement, cependant, l'état sera enregistré et utilisé de la manière la plus simple.
struct PWSDetailView : View { @State private var isToggle1 = false @State private var isToggle2 = false var body: some View { ZStack{ List { Button(action: { print("\(self.isToggle1)") print("\(self.isToggle2)") }){ Text("Settings") .padding(10) } HStack { Toggle(isOn: $isToggle1){ Text("Music") } } HStack { Toggle(isOn: $isToggle1){ Text("Music") } } } } } }
Disponible pour XCode 12
import SwiftUI struct ToggleView: View { @State var isActive: Bool = false var body: some View { Toggle(isOn: $isActive) { Text(isActive ? "Active" : "InActive") } .padding() .toggleStyle(SwitchToggleStyle(tint: .accentColor)) } }
Voici comment je code:
Toggle("Enabled", isOn: $isDisplayed.didSet { val in //Action here })
Code mis à jour (Xcode 12, iOS14):
Toggle("Title", isOn: $isDisplayed) .onReceive([self.isDisplayed].publisher.first()) { (value) in //Action code here }
C'est super propre et concis. Cela devrait être la bonne réponse à mon humble avis
Je n'ai pas pu faire fonctionner votre deuxième version, mais la première version a certainement résolu mon problème après de nombreuses recherches. La deuxième version ne compilerait pas pour moi. Merci
Merci! @Manngo Je l'ai testé tout à l'heure. cela fonctionne sur mon Xcode12 et iOS14. Quelle est votre version de Xcode? y a-t-il un message d'erreur de compilation? je crois que le second est meilleur :)
@ z33 Je suis sur SCode 12, mais je cible MacOS 10.15 Catalina. Je ne reçois pas de message d'erreur directement. Le compilateur prend une éternité pour décider qu'il ne peut pas continuer.
Voici une approche plus générique que vous pouvez appliquer à n'importe quelle Binding
pour presque toutes les View
intégrées telles que Pickers, Textfields, Toggle.
@State var isOn: Bool = false Toggle("Toggle Title", isOn: $isOn.didSet { (state) in print(state) })
Et l'utilisation est simplement;
extension Binding { func didSet(execute: @escaping (Value) -> Void) -> Binding { return Binding( get: { return self.wrappedValue }, set: { self.wrappedValue = $0 execute($0) } ) } }
C'est la mise en œuvre la plus propre. Pour moi, onReceive s'est déclenché chaque fois qu'une des autres variables d'état de la vue changeait. Avec cette solution, l'action ne s'exécute que lorsque la variable d'état attachée change.
Si vous utilisez SwiftUI 2 / iOS 14, vous pouvez utiliser onChange
:
struct ContentView: View { @State private var isDisplayed = false var body: some View { Toggle("", isOn: $isDisplayed) .onChange(of: isDisplayed) { value in // action... print(value) } } }
Puisque je ne sais pas tout ce qui se passe dans les coulisses de votre application, ce n'est peut-être pas une solution, mais comme la
station
est unBindableObject
, ne pouvez-vous pas simplement remplacerToggle(isOn: $isDisplayed)
parToggle(isOn: $station.isDisplayed)
puis mettez à jourPWSStore.shared
dans ledidSet
surisDisplayed
dans votre classePWS
?@graycampbell Cela fonctionne théoriquement (et c'est ce que j'ai essayé plus tôt). Malheureusement, la fonction didChangeValue (forKey :) de ma classe PWS (qui est une entité Core Date) est appelée assez souvent. Dans certains cas (comme en appuyant sur la bascule), la valeur de 'isDisplayed' a vraiment changé (-> l'action devrait être déclenchée). Dans d'autres cas, la valeur de 'isDisplayed' obtient "update" avec l'ancienne valeur (-> l'action ne doit pas être déclenchée). Je n'ai pas trouvé le moyen de faire la distinction entre ces deux cas. Par conséquent, ma tentative de déclencher l'action directement dans la vue.