Ich versuche herauszufinden, wie ich eine Ansicht mit Swiftui bedingt einschließen kann. Ich konnte das if
direkt 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()
}
}
}
ConditionalContent
scheint mir eine entweder / oder Art von Struktur zu sein, die vom Compiler beim Interpretieren eines@ViewBuilder
Blocks generiert wird . Ich denke, so sind unsereifs/elses
inneren Gruppen. Stapel usw. werden übersetzt. Ich denke schon, weil es a ergibtView
. In Ihrem Fall wird das in aif/else
übersetztConditionalContent<TabView, LoginView>
.SwiftUI
geht, eine zu definierenbest 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 dasTabView
oderLoginView
angezeigt werden soll, und mutieren Sie diesen Status dann über ein Ansichtsmodell - über aBinding
.HStack { ... }
nur verwendet wird, um eine "äußere Gruppe" bereitzustellen (damit das if-else kompiliert wird), können SieGroup { ... }
stattdessen auch verwenden .if/else
in einem@ViewBuilder
Block einConditionalStatement
auf Compilerebene ergibt : i.imgur.com/VtI4yLg.png .Antworten:
Der einfachste Weg, die Verwendung eines zusätzlichen Containers wie zu vermeiden,
HStack
besteht darin, Ihrebody
Immobilie wie folgt zu kommentieren@ViewBuilder
:@ViewBuilder var body: some View { if user.isLoggedIn { MainView() } else { LoginView() } }
quelle
Ich musste eine Ansicht bedingt in eine andere einbetten, sodass ich eine praktische
if
Funktion 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) } }
quelle
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?
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
some
Schlü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:
In Ihrem Beispiel gibt Ihre
body
Eigenschaft je nach Bedingung einen anderen Typ zurück:var body: some View { if someConditionIsTrue { TabView() } else { LoginView() } }
Wenn
someConditionIsTrue
, würde es a zurückgebenTabView
, sonst aLoginView
. 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.
quelle
Cannot convert value of type '() -> ()' to expected argument type 'ConditionalContent<_, _>.Storage'
var body: some View { ConditionalContent() { if userData.apiKey != nil { TabView() } else { LoginView() } } }
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 dieConditionalContent
richtige Verwendung geben kann.ConditionalContent
ist 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, sondernViewBuilder
als ein paar Methoden, die tatsächlich a zurückgebenConditionContent
. Ich vermute, dass die Verwendung einerif
Aussage der einzige Weg ist, dies zu erreichen.ConditionalContent
noch? Ihr Link gibt einen 404.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)) } } }
quelle
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 setzenmakeView()
inRootView
.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() } } } }
quelle
if let
erhalte 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-ead04edd439fEin anderer Ansatz mit ViewBuilder (der auf dem genannten basiert
ConditionalContent
)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.
¯\_(ツ)_/¯
)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:
quelle
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
quelle
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
isVisible
Wert 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.
quelle
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) } }
quelle
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 } } }
quelle
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") } } } }
quelle
Verwenden Sie Group anstelle von HStack
var body: some View { Group { if keychain.get("api-key") != nil { TabView() } else { LoginView() } } }
quelle
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)
quelle
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") } }
quelle