7
votes

Propriété conditionnelle dans SwiftUI

Comment puis-je ajouter une propriété supplémentaire en fonction d'une condition?

Avec mon code ci-dessous, j'ai l'erreur Cannot assign value of type 'some View' (result of 'Self.overlay(_:alignment:)') to type 'some View' (result of 'Self.onTapGesture(count:perform:)')

import SwiftUI

struct ConditionalProperty: View {
    @State var overlay: Bool
    var body: some View {
        var view = Image(systemName: "photo")
            .resizable()
            .onTapGesture(count: 2, perform: self.tap)
        if self.overlay {
            view = view.overlay(Circle().foregroundColor(Color.red))
        }
        return view
    }

    func tap() {
        // ...
    }
}


0 commentaires

6 Réponses :


8
votes

Dans la terminologie SwiftUI, vous n'ajoutez pas de propriété. Vous ajoutez un modificateur.

Le problème ici est que, dans SwiftUI, chaque modificateur renvoie un type qui dépend de ce qu'il modifie.

var body: some View {
    let view = Image(systemName: "photo")
        .resizable()
        .onTapGesture(count: 2, perform: self.tap)
    if overlay {
        return AnyView(view.overlay(Circle().foregroundColor(.red)))
    } else {
        return AnyView(view)
    }
}

Puisque newView a un type différent de view , vous ne pouvez pas simplement attribuer view = newView .

Une façon de résoudre ce problème est de toujours utiliser le modificateur de overlay , mais de rendre la superposition transparente lorsque vous ne voulez pas qu'elle soit visible:

var body: some View {
    return Image(systemName: "photo")
        .resizable()
        .onTapGesture(count: 2, perform: self.tap)
        .overlay(Circle().foregroundColor(overlay ? .red : .clear))
}

Une autre façon de gérer cela consiste à utiliser la gomme de type AnyView :

var view = Image(systemName: "photo")
    .resizable()
    .onTapGesture(count: 2, perform: self.tap)

// view has some deduced type like GestureModifier<SizeModifier<Image>>

if self.overlay {
    let newView = view.overlay(Circle().foregroundColor(Color.red))
    // newView has a different type than view, something like
    // OverlayModifier<GestureModifier<SizeModifier<Image>>>
}

Les recommandations que j'ai vues des ingénieurs Apple sont d'éviter d'utiliser AnyView car il est moins efficace que d'utiliser des vues entièrement typées. Par exemple, ce tweet et ce tweet .


1 commentaires

Les tests de performances réels d' AnyView ne révèlent aucune implication négative lors de son utilisation: medium.com/swlh / ...



10
votes

Rob Mayoff a déjà assez bien expliqué pourquoi ce comportement apparaît et propose deux solutions (superpositions transparentes ou utilisant AnyView ). Une troisième manière plus élégante consiste à utiliser _ConditionalContent .

Un moyen simple de créer _ConditionalContent consiste à écrire des instructions if else dans un groupe ou une pile. Voici un exemple utilisant un groupe:

import SwiftUI

struct ConditionalProperty: View {
    @State var overlay: Bool
    var body: some View {
        Group {
            if overlay {
                base.overlay(Circle().foregroundColor(Color.red))
            } else {
                base
            }
        }
    }
    
    var base: some View {
        Image("photo")
            .resizable()
            .onTapGesture(count: 2, perform: self.tap)
    }

    func tap() {
        // ...
    }
}


1 commentaires

Notez que par défaut, SwiftUI traite le type statique d'une vue comme son identifiant pour les animations ( expliqué par Joe Groff ici ). Dans votre solution, les deux sous-vues (avec / sans superposition) sont donc traitées comme des vues non liées. Si l'on applique une animation, elle peut s'appliquer différemment dans votre solution que dans la mienne. Ce n'est pas nécessairement faux, juste quelque chose dont il faut être conscient. Il peut être contourné en utilisant le modificateur .id .



0
votes

Je pense que la méthode préférée pour implémenter les Conditional Properties est ci-dessous. Cela fonctionne bien si vous avez appliqué des animations.
J'ai essayé plusieurs autres méthodes, mais cela affecte l'animation que j'ai appliquée.

Vous pouvez ajouter votre condition à la place de false J'ai gardé .conditionalView ( false ) .

Ci-dessous, vous pouvez passer à true pour vérifier le "Hello, World!" sera affiché en gras .

struct ContentView: View {
    var body: some View {
        Text("Hello, World!")
        .conditionalView(false)
        // or use:
        // .modifier(ConditionalModifier(isBold: false))

    }
}

extension View {
    func conditionalView(_ value: Bool) -> some View {
        self.modifier(ConditionalModifier(isBold: value))
  }
}

struct ConditionalModifier: ViewModifier {
    var isBold: Bool
    func body(content: Content) -> some View {
    Group {
        if self.isBold {
            content.font(.custom("HelveticaNeue-Bold", size: 14))
        }else{
            content.font(.custom("HelveticaNeue", size: 14))
        }
    }
  }
}


2 commentaires

Il n'est pas nécessaire d'utiliser l'extension conditionalView pour View . Initialisez la structure ConditionalModifier directement à l'aide de .modifier(ConditionalModifier(isBold : viewIsBold))


@PeterSchorn Je pense que j'ai compris votre point, je vous suggère d'ajouter votre réponse aussi.



2
votes

Voici une réponse similaire à ce que Kiran Jasvanee a publié, mais simplifiée:

struct ContentView: View {

    @State private var makeBold = false

    var body: some View {

        Button(action: {
            self.makeBold.toggle()
        }, label: {
            Text("Tap me to Toggle Bold")
                .modifier(ConditionalModifier(isBold: makeBold))
        })

    }
}

struct ConditionalModifier: ViewModifier {

    var isBold: Bool

    func body(content: Content) -> some View {
        Group {
            if self.isBold {
                content.font(.custom("HelveticaNeue-Bold", size: 14))
            }
            else{
                content.font(.custom("HelveticaNeue", size: 14))
            }
        }
    }
}

La différence est qu'il n'est pas nécessaire d'ajouter cette extension:

struct ConditionalModifier: ViewModifier {

    var isBold: Bool

    @ViewBuilder
    func body(content: Content) -> some View {
        // Group {
            if self.isBold {
                content.font(.custom("HelveticaNeue-Bold", size: 14))
            }
            else{
                content.font(.custom("HelveticaNeue", size: 14))
            }
        // }
    }
}

Dans la structure ConditionalModifier , vous pouvez également éliminer la vue Groupe et utiliser à la @ViewBuilder décorateur @ViewBuilder , comme ceci:

extension View {
    func conditionalView(_ value: Bool) -> some View {
        self.modifier(ConditionalModifier(isBold: value))
  }
}

Vous devez utiliser une vue de groupe (ou une autre vue parente) ou le décorateur @ViewBuilder si vous souhaitez afficher des vues de manière conditionnelle.

Voici un autre exemple dans lequel le texte d'un bouton peut être basculé entre gras et non gras:

struct ContentView: View {

    var body: some View {
        Text("Hello, World!")
            .modifier(ConditionalModifier(isBold: true))
    }
}

struct ConditionalModifier: ViewModifier {

    var isBold: Bool

    func body(content: Content) -> some View {
        Group {
            if self.isBold {
                content.font(.custom("HelveticaNeue-Bold", size: 14))
            }
            else{
                content.font(.custom("HelveticaNeue", size: 14))
            }
        }
    }
}


0 commentaires

6
votes

Grâce à ce post, j'ai trouvé une option avec une interface très simple:

extension View {
    @ViewBuilder
    func `if`<Transform: View>(_ condition: Bool, transform: (Self) -> Transform) -> some View {
        if condition { transform(self) }
        else { self }
    }
}

Activé par cette extension:

Text("Hello")
    .if(shouldBeRed) { $0.foregroundColor(.red) }

Voici un excellent article de blog avec des conseils supplémentaires pour les modificateurs conditionnels: https://fivestars.blog/swiftui/conditional-modifiers.html


1 commentaires

C'est la meilleure solution.



0
votes

Aussi simple que ça

import SwiftUI

struct SomeView: View {
  let value: Double
  let maxValue: Double
  
  private let lineWidth: CGFloat = 1
  
  var body: some View {
    Circle()
      .strokeBorder(Color.yellow, lineWidth: lineWidth)
      .overlay(optionalOverlay)
  }
  
  private var progressEnd: CGFloat {
    CGFloat(value) / CGFloat(maxValue)
  }
  
  private var showProgress: Bool {
    value != maxValue
  }
  
  @ViewBuilder private var optionalOverlay: some View {
    if showProgress {
      Circle()
        .inset(by: lineWidth / 2)
        .trim(from: 0, to: progressEnd)
        .stroke(Color.blue, lineWidth: lineWidth)
        .rotationEffect(.init(degrees: 90))
    }
  }
}

struct SomeView_Previews: PreviewProvider {
  static var previews: some View {
    Group {
      SomeView(value: 40, maxValue: 100)
      
      SomeView(value: 100, maxValue: 100)
    }
      .previewLayout(.fixed(width: 100, height: 100))
  }
}


0 commentaires