67
votes

SwiftUI rejeter le modal

Puisque SwiftUI est déclaratif, il n'y a pas de méthode de dismiss . Comment puis- DetailView ajouter un bouton de rejet / fermeture à DetailView ?

struct DetailView: View {
  var body: some View {
  Text("Detail")
  }
}

struct ContentView : View {
  var body: some View {
  PresentationButton(Text("Click to show"), destination: DetailView())
  }
}


4 commentaires

Aucun des exemples que j'ai vus n'a de méthode pour rejeter une vue présentée, donc je ne pense pas qu'il y en ait encore une.


Je suis presque sûr qu'ils l'introduiront avec la prochaine version bêta. La méthode Pop est également absente.


Je pense qu'il est important de se rappeler que SwiftUI est un changement de paradigme. Nous devons penser davantage en termes d '"état" et moins en termes d'écriture d'instructions conditionnelles, etc. Donc, comme d'autres l'ont écrit, il s'agit plus d'écouter l'état via @Environment ou @State ou d'autres "Property Wrappers". Il s'agit d'un passage au modèle d'observateur dans un cadre déclaratif, pour ceux qui aiment les phrases compliquées :-)


Il existe maintenant un moyen très propre de le faire dans la bêta 5. Voir ma réponse ci-dessous. BTW, la même méthode fonctionne pour faire apparaître une vue de navigation.


13 Réponses :


20
votes

Voici un moyen de supprimer la vue présentée.

struct DetailView: View {
    @Binding
    var dismissFlag: Bool

    var body: some View {
        Group {
            Text("Detail")
            Button(action: {
                self.dismissFlag.toggle()
            }) {
                Text("Dismiss")
            }
        }

    }
}

struct ContentView : View {
    @State var dismissFlag = false

    var body: some View {
        Button(action: {
            self.dismissFlag.toggle()
        })
        { Text("Show") }
            .presentation(!dismissFlag ? nil :
                Modal(DetailView(dismissFlag: $dismissFlag)) {
                print("dismissed")
            })
    }
}

entrez la description de l'image ici


3 commentaires

Merci, mais si l'utilisateur fait glisser pour ignorer, la bascule doit appuyer deux fois. Peut être contourné en changeant l'état self.dismissFlag = true; self.dismissFlag = false; . Solution de contournement, pas de solution. Cherche également un moyen de désactiver le glissement pour ignorer.


Je pense que si vous implémentiez onDismiss dans le constructeur Modal , vous seriez en mesure de maintenir la synchronisation de dismissFlag . Je n'ai pas essayé pour être sûr.


Pour vérifier cela, je viens de tester ce qui se passe avec self.dismissFlag lors du rejet de la vue en utilisant le mouvement de glissement. Ajoutez onDismiss: { print(self.dismissFlag) } à votre .sheet pour vous tester. Il semble qu'il bascule automatiquement la variable lors du glissement. Notez que la fonction onDismiss ne semble être appelée que lors du déplacement de la vue modale. Si vous fermez le modal en basculant le self.dismissFlag vous le onDismiss n'est pas appelé. (Je suis sur iOS 13 Beta 8)



3
votes

Depuis PresentationButton est facile à utiliser mais cachant l'état qui mine le caractère prédictif de SwiftUI je l'ai implémenté avec une Binding accessible.

struct DetailView: View {
    @Binding var showModal: Bool

    var body: some View {
        Group {
            Text("Detail")
            Button(action: {
                self.showModal = false
            }) {
                Text("Dismiss")
            }
        }
    }
}

struct ContentView: View {
    @State var showModal = false

    var body: some View {
        BindedPresentationButton(
            showModal: $showModal,
            label: Text("Show"),
            destination: DetailView(showModal: $showModal)
        ) {
            print("dismissed")
        }
    }
}

Voici comment il est utilisé:

public struct BindedPresentationButton<Label, Destination>: View where Label: View, Destination: View {
    /// The state of the modal presentation, either `visibile` or `off`.
    private var showModal: Binding<Bool>

    /// A `View` to use as the label of the button.
    public var label: Label

    /// A `View` to present.
    public var destination: Destination

    /// A closure to be invoked when the button is tapped.
    public var onTrigger: (() -> Void)?

    public init(
        showModal: Binding<Bool>,
        label: Label,
        destination: Destination,
        onTrigger: (() -> Void)? = nil
    ) {
        self.showModal = showModal
        self.label = label
        self.destination = destination
        self.onTrigger = onTrigger
    }

    public var body: some View {
        Button(action: toggleModal) {
            label
        }
        .presentation(
            !showModal.value ? nil :
                Modal(
                    destination, onDismiss: {
                        self.toggleModal()
                    }
                )
        )
    }

    private func toggleModal() {
        showModal.value.toggle()
        onTrigger?()
    }
}


1 commentaires

Ne fonctionne pas pour SwiftUI 2 - Modal est obsolète



7
votes

Vous pouvez l'implémenter.

struct view: View {
    @Environment(\.isPresented) private var isPresented

    private func dismiss() {
        isPresented?.value = false
    }
}


2 commentaires

Merci pour l'indice avec l'environnement. Comment accéder à isPresented pour l'extérieur comme dans mon exemple?


C'est faux. Vous devriez passer une variable d'état qui est également utilisée pour isPresented, plutôt que de jouer avec le presentationMode.



92
votes

Utilisation du wrapper de propriété @State (recommandé)
struct ContentView: View {

  @State private var showModal = false

  // If you are getting the "can only present once" issue, add this here.
  // Fixes the problem, but not sure why; feel free to edit/explain below.
  @Environment(\.presentationMode) var presentationMode


  var body: some View {
    Button(action: {
        self.showModal = true
    }) {
        Text("Show modal")
    }.sheet(isPresented: self.$showModal) {
        ModalView()
    }
  }
}


struct ModalView: View {

  @Environment(\.presentationMode) private var presentationMode

  var body: some View {
    Group {
      Text("Modal view")
      Button(action: {
         self.presentationMode.wrappedValue.dismiss()
      }) {
        Text("Dismiss")
      }
    }
  }
}

Utilisation de presentationMode

Vous pouvez utiliser la variable d'environnement presentationMode dans votre vue modale et appeler self.presentaionMode.wrappedValue.dismiss() pour self.presentaionMode.wrappedValue.dismiss() le modal:

struct ContentView: View {
    @State private var showModal = false
    
    var body: some View {
       Button("Show Modal") {
          self.showModal.toggle()
       }.sheet(isPresented: $showModal) {
            ModalView(showModal: self.$showModal)
        }
    }
}

struct ModalView: View {
    @Binding var showModal: Bool
    
    var body: some View {
        Text("Modal view")
        Button("Dismiss") {
            self.showModal.toggle()
        }
    }
}

entrez la description de l'image ici


11 commentaires

Merci pour l'indice avec l'environnement. Comment accéder à isPresented pour l'extérieur comme dans mon exemple?


C'est une nouvelle pour moi. Merci!


Bonne trouvaille! Cependant, pour moi (en utilisant Xcode 11 Beta 3), cela ne fonctionne qu'une seule fois pour moi lorsque PresentationLink est utilisé dans un List ou un navigationBarItems. Je peux présenter et ignorer la vue une fois à partir de chaque bouton.


J'ai également expérimenté la bêta 3 "présente une seule fois" si vous utilisez un problème de liste. Cependant, la bêta 4 semble avoir interrompu la capacité du Modal à se rejeter avec la variable d'environnement isPresented dans certains cas. L'exemple ci-dessus fonctionne toujours, mais mon exemple ne fonctionne pas. J'essaie toujours d'isoler le problème.


À quoi ressemble la vue qui présente ModalView?


Je remarque dans Xcode Version 11.0 (11A419c) que lors de l' utilisation self.presentationMode.wrappedValue.dismiss() s'appelle que la onDismiss fonction sur .sheet( pas appelé. Quand je rejette le point de vue modal en tirant vers le bas le rappel est appelé.


Vous pouvez également simplement utiliser @Environment(\.presentationMode) var presentationMode puisque Swift déduira le type via le chemin de clé spécifié.


C'est faux. Vous devriez passer une variable d'état qui est également utilisée pour isPresented, plutôt que de jouer avec le presentationMode.


Je suis d'accord avec @ stardust4891. Vous devez passer une variable d'état. Utilisez la réponse ci-dessous. Cela pourrait causer des problèmes à un stade ultérieur. Par exemple en utilisant avec un TabView.


Je pense qu'il vaut mieux passer une variable @State plutôt que d'utiliser PresentationMode . PresentationMode ne rejettera pas toujours le modal. Par exemple, si vous avez un NavigationView dans votre modal comme dans cette réponse , alors l'appel de la fonction dismiss() n'apparaîtra à la vue précédente que si vous avez navigué vers un autre écran.


J'ai utilisé l'approche de @Environment(\.presentationMode) et la fermeture des modaux se comporte maintenant mal d'une manière imprévisible. Revenir au passage de la variable d'état à modal.



5
votes

Il existe maintenant un moyen assez simple de le faire dans la bêta 5.

import SwiftUI

struct ModalView : View {
    // In Xcode 11 beta 5, 'isPresented' is deprecated use 'presentationMode' instead
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        Group {
            Text("Modal view")
            Button(action: { self.presentationMode.wrappedValue.dismiss() }) { Text("Dismiss") }
        }
    }
}

struct ContentView : View {
    @State var showModal: Bool = false
    var body: some View {
        Group {
            Button(action: { self.showModal = true }) { Text("Show modal via .sheet modifier") }
                .sheet(isPresented: $showModal, onDismiss: { print("In DetailView onDismiss.") }) { ModalView() }
        }
    }
}


4 commentaires

C'est faux. Vous devriez passer une variable d'état qui est également utilisée pour isPresented, plutôt que de jouer avec le presentationMode.


Faux dans quel contexte? Votre problème est-il une question d'exactitude ou une préférence de style? Il existe plusieurs autres façons d'accomplir la même tâche qui fonctionnent également tout aussi bien. Les propres notes de publication iOS 13 d'Apple documentent cela comme une méthode pour rejeter les modaux et cela fonctionne. Merci.


C'est un aperçu intéressant, mais cela peut ou non être un réel problème. Il a été expliqué au début était que rejeter () avait été ajouté pour plus de commodité, de sorte qu'une liaison à la var isPresented n'aurait pas à être transmise à la vue modale en plus du modificateur de feuille. Tout ce qu'il fait est de définir la variable isPresented sur false si elle est vraie, sinon (selon le fichier d'en-tête SwiftUI), elle ne fait rien.


Je pense qu'il vaut mieux passer une variable @State plutôt que d'utiliser PresentationMode . PresentationMode ne rejettera pas toujours le modal. Par exemple, si vous avez un NavigationView dans votre modal comme dans cette réponse , alors l'appel de la fonction dismiss() n'apparaîtra à la vue précédente que si vous avez navigué vers un autre écran.



24
votes

Dans Xcode Beta 5, une autre façon de faire est d'utiliser @State dans la vue qui lance le modal et d'ajouter une liaison dans la vue modale pour contrôler la visibilité du modal. Cela ne vous oblige pas à accéder à la variable @Environment presentationMode.

struct MyView : View {
    @State var modalIsPresented = false

    var body: some View {
        Button(action: {self.modalIsPresented = true})  {
            Text("Launch modal view")
        }
        .sheet(isPresented: $modalIsPresented, content: {
            MyModalView(isPresented: self.$modalIsPresented)
        })
    }
}


struct MyModalView : View {
    @Binding var isPresented: Bool

    var body: some View {
        Button(action: {self.isPresented = false})  {
            Text("Close modal view")
        }
    }
}


8 commentaires

Félicitations pour avoir adhéré aux principes de SwiftUI avec l'approche déclarative et la source unique de vérité


Cela ne fonctionne que la première fois, si je ferme et réessaye d'ouvrir la fenêtre, cela ne fonctionne plus.


Cela semble fonctionner correctement pour moi, peut-être que vous changez la valeur isPresented ailleurs? Par exemple, si vous rejetez le modal en tirant vers le bas, swiftUI bascule automatiquement la valeur. Au lieu de définir la valeur explicitement sur true / false, essayez d'utiliser isPresented.toggle () à la place


Si je crée un projet avec seulement ces deux vues, cela fonctionne bien. Mais dans mon application réelle, cela ne fonctionne pas. J'ai le même comportement que Mario voit. Je ne place pas isPresented nulle part ailleurs.


@ stardust4891 Pourquoi est-ce la bonne réponse? Quel est le problème avec les autres?


Je suis d'accord avec @ stardust4891, c'est dommage que le mode de présentation ait reçu plus de votes positifs pour sa réponse. Lorsque vous consultez la documentation officielle sur wrappedValue, voici ce qu'Apple a écrit: "Cette propriété fournit un accès principal aux données de la valeur. Cependant, vous n'accédez pas directement à wrappedValue. Au lieu de cela, vous utilisez la variable de propriété créée avec \ @Binding attribut." Cela stimule l'utilisation de liaisons comme indiqué dans cette réponse. Pour une seule source de vérité.


Ecrire votre propre @Binding var isPresented: Bool est l'option la plus flexible. Il peut même être déclaré dans votre VM en tant que @Published , au lieu de @State comme ci-dessus. Je présentais un modal avec un NavigationView, que je voulais être en mesure de rejeter le modal entier sur n'importe quel "bouton Done" de NavigationLink. L'utilisation de presentationMode m'a obligé à suivre plus d'état que nécessaire. Mais le simple fait de me lier à ma vm m'a permis de isPresented facilement le modal de n'importe quel bouton Terminé, en basculant simplement isPresented sur false.


Si quelqu'un se demande comment faire fonctionner un aperçu pour la vue modale ci-dessus: utilisez .constant pour créer une liaison avec une valeur immuable, comme celle-ci MyModalView(isPresented: .constant(true))



2
votes

Dans Xcode 11.0 beta 7, la valeur est maintenant encapsulée, la fonction suivante fonctionne pour moi:

func dismiss() {
    self.presentationMode.wrappedValue.dismiss()
}


0 commentaires

8
votes

Il semble que pour Xcode 11 Beta 7 (c'est sur la build 11M392r de Xcode), c'est légèrement différent.

@Environment(\.presentationMode) var presentation


Button(action: { self.presentation.wrappedValue.dismiss() }) { Text("Dismiss") }


1 commentaires

C'est faux. Vous devriez passer une variable d'état qui est également utilisée pour isPresented, plutôt que de jouer avec le presentationMode.



1
votes

Les vues modales dans SwiftUI semblent simples jusqu'à ce que vous commenciez à les utiliser dans une vue List ou Form . J'ai créé une petite bibliothèque qui englobe tous les cas de bord et rend l'utilisation des vues modales identique à la paire NavigationView - NavigationLink .

La bibliothèque est open-source ici: https://github.com/diniska/modal-view . Vous pouvez l'inclure dans le projet à l'aide de Swift Package Manager, ou simplement en copiant le fichier unique inclus dans la bibliothèque.

La solution pour votre code serait:

struct DetailView: View {
    var dismiss: () -> ()
    var body: some View {
        Text("Detail")
        Button(action: dismiss) {
            Text("Click to dismiss")
        }
    }
}

struct ContentView : View {
    var body: some View {
        ModalPresenter {
            ModalLink(destination: DetailView.init(dismiss:)) {
                Text("Click to show")
            }
        }
    }
}

De plus, il existe un article avec une description complète et des exemples: Comment présenter la vue modale dans SwiftUI


0 commentaires

0
votes

Utilisez la variable d'environnement dans PresentationMode. Ce lien GitHub vous aidera peut-être à résoudre le problème https://github.com/MannaICT13/Sheet-in-SwiftUI

C'est une solution simple:

struct ContentView2 : View {

    @Environment (\.presentationMode) var presentationMode

    var body : some View {
        VStack {
            Text("This is ContentView2")
            Button(action: {
                self.presentationMode.wrappedValue.dismiss()
            }, label: {
                Text("Back")    
            })    
        }
    }
}


struct ContentView: View {

    @State var isShowingSheet : Bool = false

    var body: some View {
        Button(action: {
            self.isShowingSheet.toggle()
        }, label: {
            Text("Click Here")
        }).sheet(isPresented: $isShowingSheet, content: {  
            ContentView2()
        })
    }
}


0 commentaires

0
votes

Une façon de procéder peut être de déclarer votre propre modificateur pour la présentation modale et le rejet.

struct ContentView: View {

  @State var showModal = false

  var body: some View {

    Text("Show").foregroundColor(.blue).onTapGesture {
      withAnimation(.easeIn(duration: 0.75)) {
        self.showModal = true
      }
    }.showModal($showModal, {

      Text("Dismiss").foregroundColor(.blue).onTapGesture {
        withAnimation(.easeIn(duration: 0.75)) {
          self.showModal = false
        }
      }

    })


  }
}    

Ensuite, vous pouvez utiliser le modificateur sur n'importe quelle vue pour laquelle vous souhaitez indiquer comment afficher une vue et fermer cette vue. Tout comme un popover ou un modificateur de feuille.

extension View {

  func showModal<T>(_ binding: Binding<Bool>, _ view: @escaping () -> T) -> some View where T: View {

    let windowHeightOffset = (UIApplication.shared.windows.first?.frame.height ?? 600) * -1

    return ZStack {

      self

      view().frame(maxWidth: .infinity, maxHeight: .infinity).edgesIgnoringSafeArea(.all).offset(x: 0, y: binding.wrappedValue ? 0 : windowHeightOffset)

    }

  }
}

La présentation est en plein écran à partir du haut, si vous souhaitez qu'elle vienne du côté, changez la transition à l'intérieur du modificateur en début ou en fin. D'autres transitions fonctionneraient également, comme l'opacité ou l'échelle.

entrez la description de l'image ici


2 commentaires

Oui, mon ancienne application est tombée en panne, je mettrai à jour quand je trouverai une solution. Désolé Andrew


% câlins%% câlins%% câlins%



0
votes

Exemple de code SwiftUI 2 (fonctionne également avec les mobiles)

(l'exemple de code ne fonctionne pas avec swift 1, mais vous pouvez toujours l'essayer sans le bloc @main )

Exemple d'application complet pour l'utilisation de feuilles:

struct SheetLink<Content> : View where Content: View {
    @State var text: String
    @State var displaySheet = false
    @State var content: Content


    var body: some View {
        HStack {
            Button(text, action: { self.displaySheet = true } ).buttonStyle(PlainButtonStyle()).foregroundColor(.blue)
        }
        .sheet(isPresented: $displaySheet) {
            SheetTemplateView(isPresented: self.$displaySheet, content: content)
        }
    }
}

struct SheetTemplateView<Content> : View where Content: View {
    @Binding var isPresented: Bool
    @State var content: Content
    
    var body: some View {
        VStack{
            HStack{
                Button("Back!", action: { isPresented.toggle() } ).buttonStyle(PlainButtonStyle()).foregroundColor(.blue)
                Spacer()
            }
            Spacer()
            content
            Spacer()
        }
        .padding()
    }
}

entrez la description de l'image ici

et lorsque la sous-vue est plus grande que la vue principale:

entrez la description de l'image ici

Et le code derrière ceci:

@main
struct TestAppApp: App {
    var body: some Scene {
        WindowGroup {
            SheetLink(text: "click me!", content: ChildView() )
                .padding(.all, 100)
        }
    }
}

struct ChildView: View {
    var body: some View {
        Text("this is subView!")
    }
}


0 commentaires

1
votes

Pop automatiquement si dans la Navigation ou ignorer si Modal

Il suffit de prendre le presentationMode de l'environnement dans la vue destination et dismiss le wrappedValue de celui - ci:

struct DestinationView: View {
    @Environment(\.presentationMode) private var presentationMode

    var body: some View {
        Button("Dismiss") {
            self.presentationMode.wrappedValue.dismiss()
        }
    }
}


Démo (pop / ignorer)

Pop Rejeter


1 commentaires

Merci d'avoir posté ceci. C'est pourquoi PresentationMode n'est probablement pas la meilleure solution pour ignorer un modal, car il peut à la place apparaître dans la vue précédente si vous avez un NavigationView . Si vous voulez vous assurer de rejeter un modal, vous devez passer une variable @State .