Verwenden Sie die Ansicht in SwiftUI bedingt


73

Ich versuche herauszufinden, wie ich eine Ansicht mit Swiftui bedingt einschließen kann. Ich konnte das ifdirekt in einer Ansicht nicht verwenden und musste dazu eine Stapelansicht verwenden.

Das funktioniert, aber es scheint, als gäbe es einen saubereren Weg.

var body: some View {
    HStack() {
        if keychain.get("api-key") != nil {
            TabView()
        } else {
            LoginView()
        }
    }
}

1
"Kann jemand erklären, wie man diese Erklärung liest?" Es ist ein Standard-Generikum. Was ist die Verwirrung?
Matt

ConditionalContentscheint mir eine entweder / oder Art von Struktur zu sein, die vom Compiler beim Interpretieren eines @ViewBuilderBlocks generiert wird . Ich denke, so sind unsere ifs/elsesinneren Gruppen. Stapel usw. werden übersetzt. Ich denke schon, weil es a ergibt View. In Ihrem Fall wird das in a if/elseübersetzt ConditionalContent<TabView, LoginView>.
Matteo Pacini

3
@MichaelStClair Wir sind alle Neulinge, wenn es darum SwiftUIgeht, eine zu definieren best practice. Code sieht gut aus, also machen Sie es! Eine Verbesserung, die Sie tun könnten: Haben Sie einen Status in der Ansicht, um zu entscheiden, ob das TabViewoder LoginViewangezeigt werden soll, und mutieren Sie diesen Status dann über ein Ansichtsmodell - über a Binding.
Matteo Pacini

3
Wenn das HStack { ... }nur verwendet wird, um eine "äußere Gruppe" bereitzustellen (damit das if-else kompiliert wird), können Sie Group { ... }stattdessen auch verwenden .
Martin R

3
Ich habe gerade überprüft, dass if/elsein einem @ViewBuilderBlock ein ConditionalStatementauf Compilerebene ergibt : i.imgur.com/VtI4yLg.png .
Matteo Pacini

Antworten:


126

Der einfachste Weg, die Verwendung eines zusätzlichen Containers wie zu vermeiden, HStackbesteht darin, Ihre bodyImmobilie wie folgt zu kommentieren @ViewBuilder:

@ViewBuilder
var body: some View {
    if user.isLoggedIn {
        MainView()
    } else {
        LoginView()
    }
}

2
Genau das habe ich gesucht! Vielen Dank
Michael St Clair

Auf diese Weise funktionierte meine Animation nicht mehr. Die if-Anweisung in meinem Fall ist ein Boolescher Wert, bei dem die andere Ansicht mit der Animation umschaltet, um die Ansicht in der if-Anweisung anzuzeigen / auszublenden, indem ein Übergangsmodifikator hinzugefügt wird.
EdiZ

@IanWarburton Dies könnte Ihnen helfen: Was ermöglicht SwiftUIs DSL?
pawello2222

34

Ich musste eine Ansicht bedingt in eine andere einbetten, sodass ich eine praktische ifFunktion erstellte:

extension View {
   @ViewBuilder
   func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
        if conditional {
            content(self)
        } else {
            self
        }
    }
}

Dies gibt zwar eine AnyView zurück, die nicht ideal ist, sich aber technisch korrekt anfühlt, da Sie das Ergebnis während der Kompilierungszeit nicht wirklich kennen.

In meinem Fall musste ich die Ansicht in eine ScrollView einbetten, damit es so aussieht:

var body: some View {
    VStack() {
        Text("Line 1")
        Text("Line 2")
    }
    .if(someCondition) { content in
        ScrollView(.vertical) { content }
    }
}

Sie können es aber auch verwenden, um Modifikatoren bedingt anzuwenden:

var body: some View {
    Text("Some text")
    .if(someCondition) { content in
        content.foregroundColor(.red)
    }
}

7
Der Backtick "bricht die SwiftUI-Vorschau", ich habe stattdessen "if" in "ifConditional" geändert und es funktioniert wunderbar.
Mike Lee

26

Sie haben es nicht in Ihre Frage aufgenommen, aber ich denke, der Fehler, den Sie erhalten, wenn Sie ohne den Stapel auskommen, ist der folgende?

Die Funktion deklariert einen undurchsichtigen Rückgabetyp, hat jedoch keine Rückgabeanweisungen in ihrem Hauptteil, aus denen auf einen zugrunde liegenden Typ geschlossen werden kann

Der Fehler gibt Ihnen einen guten Hinweis darauf, was vor sich geht, aber um ihn zu verstehen, müssen Sie das Konzept der undurchsichtigen Rückgabetypen verstehen . So rufen Sie die Typen auf, denen das someSchlüsselwort vorangestellt ist . Ich habe auf der WWDC keine Apple-Ingenieure gesehen, die sich eingehend mit diesem Thema befasst haben (vielleicht habe ich den jeweiligen Vortrag verpasst?). Deshalb habe ich selbst viel recherchiert und einen Artikel darüber geschrieben, wie diese Typen funktionieren und warum sie verwendet werden Rückgabetypen in SwiftUI .

🔗 Was ist das für ein "in SwiftUI"?

Es gibt auch eine ausführliche technische Erklärung in einer anderen

🔗 Stackoverflow-Post für undurchsichtige Ergebnistypen

Wenn Sie vollständig verstehen möchten, was los ist, empfehle ich, beide zu lesen.


Als kurze Erklärung hier:

Allgemeine Regel:

Funktionen oder Eigenschaften mit einem undurchsichtigen Ergebnistyp ( some Type)
müssen immer denselben konkreten Typ zurückgeben .

In Ihrem Beispiel gibt Ihre bodyEigenschaft je nach Bedingung einen anderen Typ zurück:

var body: some View {
    if someConditionIsTrue {
        TabView()
    } else {
        LoginView()
    }
}

Wenn someConditionIsTrue, würde es a zurückgeben TabView, sonst a LoginView. Dies verstößt gegen die Regel, weshalb sich der Compiler beschwert.

Wenn Sie Ihre Bedingung in eine Stapelansicht einschließen, enthält die Stapelansicht die konkreten Typen beider bedingter Zweige in einem eigenen generischen Typ:

HStack<ConditionalContent<TabView, LoginView>>

Unabhängig davon, welche Ansicht tatsächlich zurückgegeben wird, ist der Ergebnistyp des Stapels immer derselbe, und daher beschwert sich der Compiler nicht.


💡 Ergänzend:

Es gibt tatsächlich eine Ansichtskomponente, die SwiftUI speziell für diesen Anwendungsfall bereitstellt, und genau diese wird von Stacks intern verwendet, wie Sie im obigen Beispiel sehen können:

Bedingter Inhalt

Es hat den folgenden generischen Typ, wobei der generische Platzhalter automatisch aus Ihrer Implementierung abgeleitet wird:

ConditionalContent<TrueContent, FalseContent>

Ich empfehle, diesen Ansichtscontainer anstelle eines Stapels zu verwenden, da er anderen Entwicklern seinen Zweck semantisch klar macht.


Ich hatte versucht, bedingte Inhalte zu verwenden, habe aber einen Fehler erhalten. Wie genau würde ich das verwenden? Cannot convert value of type '() -> ()' to expected argument type 'ConditionalContent<_, _>.Storage'
Michael St Clair

var body: some View { ConditionalContent() { if userData.apiKey != nil { TabView() } else { LoginView() } } }
Michael St Clair

Ich weiß ehrlich gesagt nicht, warum das nicht funktioniert. Versuchte es selbst, stieß auf den gleichen Fehler. So wie ich es verstehe, ConditionalContent sollte es angesichts der Dokumentation genau das richtige Werkzeug sein: Zeigen Sie Inhalte an, die eines von zwei möglichen Kindern zeigen. Ich habe einige Beiträge auf Twitter gelesen, in denen einige Fehler erwähnt wurden, die in SwiftUI noch vorhanden sind. Vielleicht ist das einer von ihnen. Im Moment würde ich mich dann für Stapel oder Gruppen entscheiden oder hoffen, dass jemand anderes eine gute Antwort für die ConditionalContentrichtige Verwendung geben kann.
Mischa

1
Das ConditionalContentist in der Tat das richtige Werkzeug, aber wenn Sie genauer hinschauen, werden Sie feststellen, dass es keinen öffentlichen Initialisierer hat. Sie sollten es also nicht direkt verwenden, sondern ViewBuilderals ein paar Methoden, die tatsächlich a zurückgeben ConditionContent. Ich vermute, dass die Verwendung einer if Aussage der einzige Weg ist, dies zu erreichen.
Rraphael

3
Existiert ConditionalContentnoch? Ihr Link gibt einen 404.
speg

12

Wie auch immer, das Problem besteht immer noch. Wenn Sie mvvm-ähnlich denken, brechen alle Beispiele auf dieser Seite es. Logik der Benutzeroberfläche enthält in Ansicht. In allen Fällen ist es nicht möglich, einen Unit-Test zu schreiben, um die Logik abzudecken.

PS. Ich kann das immer noch nicht lösen.

AKTUALISIEREN

Ich bin mit der Lösung beendet,

Datei ansehen:

import SwiftUI


struct RootView: View {

    @ObservedObject var viewModel: RatesListViewModel

    var body: some View {
        viewModel.makeView()
    }
}


extension RatesListViewModel {

    func makeView() -> AnyView {
        if isShowingEmpty {
            return AnyView(EmptyListView().environmentObject(self))
        } else {
            return AnyView(RatesListView().environmentObject(self))
        }
    }
}

1
Habe so viele der anderen Lösungen ausprobiert, aber dies war die einzige, die für mich funktioniert hat. Umschließen der Ansichten innerhalb des if in einer AnyView.
Philip Aarseth

2
In MVVM, das ursprünglich für WPF entwickelt wurde, ist View Model eine Abstraktion von View, daher denke ich nicht, dass Ihr makeView()Modell, das eine bestimmte Ansicht erstellt, zu View Model gehören sollte. Die Ansicht sollte keine Domänenlogik enthalten, kann jedoch eine Präsentationslogik enthalten. Sie können nur setzen makeView()in RootView.
Manabu Nakazawa

@ManabuNakazawa Der einzige Grund, warum ich es hier eingefügt habe, ist, SwiftUI NICHT in das Ziel von Unit-Test aufzunehmen. 'Sie können es einfach sagen' - ja, dieses Beispiel war nur ein Beispiel und die endgültige Version enthält mehr Abstraktion für bestimmte Ansichten und VMs.
Mike Glukhov

6

Basierend auf den Kommentaren habe ich mich für diese Lösung entschieden, die die Ansicht neu generiert, wenn sich der API-Schlüssel mithilfe von @EnvironmentObject ändert.

UserData.swift

import SwiftUI
import Combine
import KeychainSwift

final class UserData: BindableObject  {
    let didChange = PassthroughSubject<UserData, Never>()
    let keychain = KeychainSwift()

    var apiKey : String? {
        get {
            keychain.get("api-key")
        }
        set {
            if let newApiKey : String = newValue {
                keychain.set(newApiKey, forKey: "api-key")
            } else {
                keychain.delete("api-key")
            }

            didChange.send(self)
        }
    }
}

ContentView.swift

import SwiftUI

struct ContentView : View {

    @EnvironmentObject var userData: UserData

    var body: some View {
        Group() {
            if userData.apiKey != nil {
                TabView()
            } else {
                LoginView()
            }
        }
    }
}

In Xcode 11 Beta 6 if leterhalte ich bei der Verwendung einen Kompilierungsfehler: Closure containing control flow statement cannot be used with function builder 'ViewBuilder'Dies könnte relevant sein: medium.com/q42-engineering/swiftui-optionals-ead04edd439f
Sajjon

4

Ein anderer Ansatz mit ViewBuilder (der auf dem genannten basiertConditionalContent )

buildEither + optional

import PlaygroundSupport
import SwiftUI

var isOn: Bool?

struct TurnedOnView: View {
    var body: some View {
        Image(systemName: "circle.fill")
    }
}

struct TurnedOffView: View {
    var body: some View {
        Image(systemName: "circle")
    }
}

struct ContentView: View {
    var body: some View {
        ViewBuilder.buildBlock(
            isOn == true ?
                ViewBuilder.buildEither(first: TurnedOnView()) :
                ViewBuilder.buildEither(second: TurnedOffView())
        )
    }
}

let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView

(Es gibt auch buildIf , aber ich konnte die Syntax noch nicht herausfinden. ¯\_(ツ)_/¯)


Man könnte wickelt auch das Ergebnis ViewinAnyView

import PlaygroundSupport
import SwiftUI

let isOn: Bool = false

struct TurnedOnView: View {
    var body: some View {
        Image(systemName: "circle.fill")
    }
}

struct TurnedOffView: View {
    var body: some View {
        Image(systemName: "circle")
    }
}

struct ContentView: View {
    var body: AnyView {
        isOn ?
            AnyView(TurnedOnView()) :
            AnyView(TurnedOffView())
    }
}

let liveView = UIHostingController(rootView: ContentView())
PlaygroundPage.current.liveView = liveView

Aber es fühlt sich irgendwie falsch an ...


Beide Beispiele führen zum gleichen Ergebnis:

Spielplatz


2

Frühere Antworten waren korrekt, ich möchte jedoch erwähnen, dass Sie optionale Ansichten in Ihren HStacks verwenden können. Nehmen wir an, Sie haben optionale Daten, z. die Benutzeradresse. Sie können den folgenden Code einfügen:

// works!!
userViewModel.user.address.map { Text($0) }

Anstelle des anderen Ansatzes:

// same logic, won't work
if let address = userViewModel.user.address {
    Text(address)
}

Da ein optionaler Text zurückgegeben wird, wird er vom Framework problemlos verarbeitet. Dies bedeutet auch, dass die Verwendung eines Ausdrucks anstelle der if-Anweisung ebenfalls in Ordnung ist, z.

// works!!!
keychain.get("api-key") != nil ? TabView() : LoginView()

In Ihrem Fall können beide kombiniert werden:

keychain.get("api-key").map { _ in TabView() } ?? LoginView()

Beta 4 verwenden


2

Ich habe mich entschieden, dieses Problem zu lösen, indem ich einen Modifikator erstellt habe, der eine Ansicht "sichtbar" oder "unsichtbar" macht. Die Implementierung sieht folgendermaßen aus:

import Foundation
import SwiftUI

public extension View {
    /**
     Returns a view that is visible or not visible based on `isVisible`.
     */
    func visible(_ isVisible: Bool) -> some View {
        modifier(VisibleModifier(isVisible: isVisible))
    }
}

fileprivate struct VisibleModifier: ViewModifier {
    let isVisible: Bool

    func body(content: Content) -> some View {
        Group {
            if isVisible {
                content
            } else {
                EmptyView()
            }
        }
    }
}

Um Ihr Beispiel zu lösen, kehren Sie einfach den hier gezeigten isVisibleWert um:

var body: some View {
    HStack() {
        TabView().visible(keychain.get("api-key") != nil)
        LoginView().visible(keychain.get("api-key") == nil)
    }
}

Ich habe darüber nachgedacht, dies in eine Art "Wenn" -Ansicht zu packen, die zwei Ansichten annehmen würde, eine, wenn die Bedingung wahr ist, und eine, wenn die Bedingung falsch ist, aber ich entschied, dass meine derzeitige Lösung sowohl allgemeiner als auch lesbarer ist.


Beachten Sie, dass ich diese Lösung jetzt zu meiner "KSSCore" -Bibliothek hinzugefügt habe, die der Öffentlichkeit auf GitHub unter github.com/klassen-software-solutions/KSSCore/blob/master/…
Steven W. Klassen

1

Wie wär es damit?

Ich habe eine bedingte Inhaltsansicht , die entweder ein Text oder ein Symbol ist . Ich habe das Problem so gelöst. Kommentare werden sehr geschätzt, da ich nicht weiß, ob dies wirklich "schnell" oder nur ein "Hack" ist, aber es funktioniert:

    private var contentView : some View {

    switch kind {
    case .text(let text):
        let textView = Text(text)
        .font(.body)
        .minimumScaleFactor(0.5)
        .padding(8)
        .frame(height: contentViewHeight)
        return AnyView(textView)
    case .icon(let iconName):
        let iconView = Image(systemName: iconName)
            .font(.title)
            .frame(height: contentViewHeight)
        return AnyView(iconView)
    }
}

1

Ich habe die Antwort von @ gabriellanata auf bis zu zwei Bedingungen erweitert. Sie können bei Bedarf weitere hinzufügen. Sie verwenden es so:

    Text("Hello")
        .if(0 == 1) { $0 + Text("World") }
        .elseIf(let: Int("!")?.description) { $0 + Text($1) }
        .else { $0.bold() }

Der Code:

extension View {
    func `if`<TrueContent>(_ condition: Bool, @ViewBuilder  transform: @escaping (Self) -> TrueContent)
        -> ConditionalWrapper1<Self, TrueContent> where TrueContent: View {
            ConditionalWrapper1<Self, TrueContent>(content: { self },
                                                   conditional: Conditional<Self, TrueContent>(condition: condition,
                                                                                               transform: transform))
    }

    func `if`<TrueContent: View, Item>(`let` item: Item?, @ViewBuilder transform: @escaping (Self, Item) -> TrueContent)
        -> ConditionalWrapper1<Self, TrueContent> {
            if let item = item {
                return self.if(true, transform: {
                    transform($0, item)
                })
            } else {
                return self.if(false, transform: {
                    transform($0, item!)
                })
            }
    }
}


struct Conditional<Content: View, Trans: View> {
    let condition: Bool
    let transform: (Content) -> Trans
}

struct ConditionalWrapper1<Content: View, Trans1: View>: View {
    var content: () -> Content
    var conditional: Conditional<Content, Trans1>

    func elseIf<Trans2: View>(_ condition: Bool, @ViewBuilder transform: @escaping (Content) -> Trans2)
        -> ConditionalWrapper2<Content, Trans1, Trans2> {
            ConditionalWrapper2(content: content,
                                conditionals: (conditional,
                                               Conditional(condition: condition,
                                                           transform: transform)))
    }

    func elseIf<Trans2: View, Item>(`let` item: Item?, @ViewBuilder transform: @escaping (Content, Item) -> Trans2)
        -> ConditionalWrapper2<Content, Trans1, Trans2> {
            let optionalConditional: Conditional<Content, Trans2>
            if let item = item {
                optionalConditional = Conditional(condition: true) {
                    transform($0, item)
                }
            } else {
                optionalConditional = Conditional(condition: false) {
                    transform($0, item!)
                }
            }
            return ConditionalWrapper2(content: content,
                                       conditionals: (conditional, optionalConditional))
    }

    func `else`<ElseContent: View>(@ViewBuilder elseTransform: @escaping (Content) -> ElseContent)
        -> ConditionalWrapper2<Content, Trans1, ElseContent> {
            ConditionalWrapper2(content: content,
                                conditionals: (conditional,
                                               Conditional(condition: !conditional.condition,
                                                           transform: elseTransform)))
    }

    var body: some View {
        Group {
            if conditional.condition {
                conditional.transform(content())
            } else {
                content()
            }
        }
    }
}

struct ConditionalWrapper2<Content: View, Trans1: View, Trans2: View>: View {
    var content: () -> Content
    var conditionals: (Conditional<Content, Trans1>, Conditional<Content, Trans2>)

    func `else`<ElseContent: View>(@ViewBuilder elseTransform: (Content) -> ElseContent) -> some View {
        Group {
            if conditionals.0.condition {
                conditionals.0.transform(content())
            } else if conditionals.1.condition {
                conditionals.1.transform(content())
            } else {
                elseTransform(content())
            }
        }
    }

    var body: some View {
        self.else { $0 }
    }
}

0

Wenn die Fehlermeldung lautet

Closure containing control flow statement cannot be used with function builder 'ViewBuilder'

Verstecken Sie einfach die Komplexität des Kontrollflusses im ViewBuilder:

Das funktioniert:

struct TestView: View {
    func hiddenComplexControlflowExpression() -> Bool {
        // complex condition goes here, like "if let" or "switch"
        return true
    }
    var body: some View {
        HStack() {
            if hiddenComplexControlflowExpression() {
                Text("Hello")
            } else {
                Image("test")
            }

            if hiddenComplexControlflowExpression() {
                Text("Without else")
            }
        }
    }
}

0

Verwenden Sie Group anstelle von HStack

var body: some View {
        Group {
            if keychain.get("api-key") != nil {
                TabView()
            } else {
                LoginView()
            }
        }
    }

0

Die Erweiterung mit dem Bedingungsparameter funktioniert gut für mich (iOS 14):

import SwiftUI

extension View {
   func showIf(condition: Bool) -> AnyView {
       if condition {
           return AnyView(self)
       }
       else {
           return AnyView(EmptyView())
       }

    }
}

Anwendungsbeispiel:

ScrollView { ... }.showIf(condition: shouldShow)

0

Wenn Sie mit NavigationLink zu zwei verschiedenen Ansichten navigieren möchten, können Sie mit dem ternären Operator navigieren.

    let profileView = ProfileView()
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
    
    let otherProfileView = OtherProfileView(data: user)
.environmentObject(profileViewModel())
.navigationBarTitle("\(user.fullName)", displayMode: .inline)
    
    NavigationLink(destination: profileViewModel.userName == user.userName ? AnyView(profileView) : AnyView(otherProfileView)) {
      HStack {
        Text("Navigate")
    }
    }
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.