在SwiftUI中有条件地使用视图


79

我正在尝试找出正确的方法来有条件地包含swiftui视图。我无法if直接使用视图的内部,而不得不使用堆栈视图来做到这一点。

这可行,但似乎会有更清洁的方法。

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

1
“有人可以解释如何阅读此声明吗?” 这是一个标准的通用名称。有什么困惑?
马特

ConditionalContent在我看来,解释一个@ViewBuilder块时从编译器生成的一种或/或一种类型的结构。我认为这就是我们ifs/elses内部小组的方式。堆栈等已翻译。我认为是因为它会产生一个View。在您的情况下,该代码if/else将转换为ConditionalContent<TabView, LoginView>
Matteo Pacini

3
@MichaelStClair涉及到的都是新手SwiftUI,因此定义会花费一些时间best practice。代码看起来不错,所以继续吧!您可以做的改进:在视图中确定一个状态,以决定是否显示TabViewor LoginView,然后通过视图模型(通过)更改该状态Binding
Matteo Pacini,

3
如果HStack { ... }仅用于提供“外部组”(以进行if-else编译),则也可以使用Group { ... }
马丁·R

3
我刚刚验证了if/else在一个@ViewBuilder块中产生一个ConditionalStatement编译器级别的信息:i.imgur.com/VtI4yLg.png
Matteo Pacini

Answers:


138

避免使用多余的容器的最简单方法HStack是将body属性注释为@ViewBuilder,如下所示:

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

2
这正是我想要的!谢谢
Michael St Clair,

使用这种方式导致我的动画停止工作。在我的案例中,if语句是在另一个布尔值视图上以动画方式切换的,以便通过向其添加过渡修饰符在if语句内显示/隐藏视图。
EdiZ

@IanWarburton这可能会帮助您:什么启用了SwiftUI的DSL?
pawello2222

非常感谢!这个问题一直困扰着我整个上午。
WeZZard

使用的是不喜欢🤦‍♂️的三元运算符
Paul Fitzgerald

36

我需要有条件地将视图嵌入另一个视图,因此最终创建了一个便捷if函数:

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

这确实会返回一个AnyView,虽然这不是理想的方法,但是从技术上来说它是正确的,因为您在编译时实际上并不知道其结果。

就我而言,我需要将视图嵌入ScrollView中,因此看起来像这样:

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

但是您也可以使用它来有条件地应用修饰符:

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

7
反引号`中断了SwiftUI预览,我只是将`if`改为ifConditional,并且效果很好。
Mike Lee

26

您没有在问题中包含它,但是我想以下是您在没有堆栈时遇到的错误?

函数声明了不透明的返回类型,但是在其主体中没有用于从其推断基础类型的返回语句

该错误为您提供了一个很好的提示,但要了解它,您需要了解不透明返回类型的概念。这就是您调用带有some关键字前缀的类型的方式。我没有在WWDC上看到任何Apple工程师对此主题进行深入研究(也许我错过了各自的演讲?),这就是为什么我自己进行了大量研究并撰写了有关这些类型的工作方式以及为什么将它们用作研究对象的文章的原因。在SwiftUI中返回类型。

Swift SwiftUI中的“一些”是什么?

另外还有详细的技术说明

🔗关于不透明结果类型的Stackoverflow帖子

如果您想完全了解正在发生的事情,建议您同时阅读两者。


作为此处的简要说明:

一般规则:

结果类型不透明(some Type)的函数或属性
必须始终返回相同的具体类型

在您的示例中,您的body属性根据条件返回不同的类型:

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

如果someConditionIsTrue返回,则返回TabView,否则返回LoginView。这违反了规则,这就是编译器抱怨的原因。

如果将条件包装在堆栈视图中,则堆栈视图将以其自己的通用类型包括两个条件分支的具体类型:

HStack<ConditionalContent<TabView, LoginView>>

结果,无论实际上返回哪个视图,堆栈的结果类型将始终相同,因此编译器不会抱怨。


💡补充:

实际上,有一个SwiftUI为此用例专门提供的视图组件,实际上是栈内部使用的视图组件,如您在上面的示例中看到的:

条件内容

它具有以下通用类型,其中通用占位符可从您的实现中自动推断出来:

ConditionalContent<TrueContent, FalseContent>

我建议使用该视图容器,而不要使用堆栈,因为它在语义上对其他开发人员明确了其目的。


我曾尝试使用条件内容,但遇到错误,我该怎么使用呢?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

老实说,我不知道为什么这行不通。我自己尝试过,遇到了同样的错误。我理解它们的方式,ConditionalContent 应该是这里的正确工具,并提供其文档:查看显示两个可能子级之一的内容。我在Twitter上读了一些帖子,提到了SwiftUI中仍然存在的一些错误。也许这就是其中之一。现在,我将使用堆栈或组,或者希望其他人可以为如何ConditionalContent正确使用提供良好的答案。
Mischa

1
ConditionalContent确权的工具来使用,但如果你仔细看,你会看到,它没有公开的初始化,所以你不应该直接使用它,但ViewBuilder一对夫妇的方法实际上返回ConditionContent。我的猜测是,使用if 语句是实现此目标的唯一方法。
rraphael

3
是否ConditionalContent仍然存在?您的链接返回一个404
SPEG

12

无论如何,问题仍然存在。认为该页面上的所有示例都像mvvm一样会破坏它。UI的逻辑包含在View中。在所有情况下,都不可能编写单元测试来涵盖逻辑。

PS。我仍然无法解决这个问题。

更新

我以解决方案告终,

查看文件:

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
尝试了许多其他解决方案,但这是唯一对我有用的解决方案。将视图包装在if中的AnyView中。
菲利普·阿尔萨斯

2
在最初为WPF开发的MVVM中,视图模型是视图的抽象,所以我认为makeView()构成特定视图的your不应属于View Model。视图不包含域逻辑,但可以包含表示逻辑。你可以放进makeView()RootView
中泽

@ManabuNakazawa我将其放在此处的唯一原因是不将SwiftUI包含在单元测试的目标中。“你可以放”-是的,这个例子只是一个例子,最终版本在特定视图和虚拟机上有更多抽象。
Mike Glukhov

6

基于这些评论,我最终使用了该解决方案,当使用@EnvironmentObject更改api密钥时,该解决方案将重新生成视图。

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

在Xcode 11 beta 6中,使用时if let,出现编译错误:Closure containing control flow statement cannot be used with function builder 'ViewBuilder',这可能是相关的:medium.com/q42-engineering/swiftui-optionals-ead04edd439f
Sajjon 19'Aug

4

使用ViewBuilder的另一种方法(依赖于提及ConditionalContent

buildEither +可选

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

(还有buildIf,但是我还不能弄清楚它的语法。¯\_(ツ)_/¯


也可以将结果包装ViewAnyView

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

但这有点不对劲...


这两个示例产生相同的结果:

操场


2

先前的答案是正确的,但是,我想提一下,您可以在HStacks内部使用可选视图。假设您有一个可选数据,例如。用户地址。您可以插入以下代码:

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

代替其他方法:

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

由于它将返回Optional文本,因此框架可以很好地处理它。这也意味着,使用表达式代替if语句也可以,例如:

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

在您的情况下,可以将两者结合使用:

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

使用Beta 4


2

我选择通过创建一个使视图“可见”或“不可见”的修饰符来解决此问题。该实现如下所示:

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

然后使用它来解决您的示例,您只需将isVisible值反转即可,如下所示:

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

我已经考虑过将其包装到某种“ If”视图中,该视图将包含两种视图,一种是在条件为true时,另一种是在条件为false时,但是我认为我现在的解决方案既更通用又更易读。


请注意,我现在已经在此溶液中加入我的“KSSCore”图书馆提供给在GitHub上公开github.com/klassen-software-solutions/KSSCore/blob/master/...
史蒂芬W.克拉森

1

那个怎么样?

我有一个有条件的contentView,它可以是文本图标。我解决了这样的问题。评论非常感谢,因为我不知道这是真的“花招”还是仅仅是“ hack”,但是它的工作原理是:

    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

我将@gabriellanata的答案扩展到最多两个条件。如果需要,您可以添加更多。您可以这样使用它:

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

编码:

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

如果错误信息是

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

只需从ViewBuilder隐藏控制流的复杂性即可:

这有效:

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

使用而不是HStack

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

0

使用条件参数进行扩展对我来说效果很好(iOS 14):

import SwiftUI

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

    }
}

用法示例:

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

0

如果要使用NavigationLink导航到两个不同的视图,则可以使用三元运算符进行导航。

    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")
    }
    }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.