25
votes

Comment puis-je déclencher une action lorsqu'une bascule swiftUI () est basculée?

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.


2 commentaires

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 un BindableObject , ne pouvez-vous pas simplement remplacer Toggle(isOn: $isDisplayed) par Toggle(isOn: $station.isDisplayed) puis mettez à jour PWSStore.shared dans le didSet sur isDisplayed dans votre classe PWS ?


@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.


14 Réponses :


1
votes

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 }


1 commentaires

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.



2
votes

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


0 commentaires

10
votes

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()
   }
}


0 commentaires

3
votes
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

0 commentaires

6
votes

J'ai trouvé une solution plus simple, utilisez simplement onTapGesture: D

Toggle(isOn: $stateChange) {
  Text("...")
}
.onTapGesture {
  // Any actions here.
}


1 commentaires

Il se déclenchera également, même lorsque le texte est tapé. Je pense que ce n'est pas une bonne solution.



1
votes

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


1 commentaires

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



4
votes

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


1 commentaires

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!



11
votes

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


2 commentaires

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.



25
votes

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


4 commentaires

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?



0
votes

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


0 commentaires

0
votes

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


0 commentaires

4
votes

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
}


4 commentaires

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.



18
votes

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


1 commentaires

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.



3
votes

SwiftUI 2

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


0 commentaires