简体   繁体   English

SwiftUI 中有条件地使用视图

[英]Conditionally use view in SwiftUI

I'm trying to figure out the correct way to conditionally include a view with swiftui. I wasn't able to use the if directly inside of a view and had to use a stack view to do it.我试图找出正确的方法来有条件地包含带有 swiftui 的视图。我无法直接在视图内部使用if并且必须使用堆栈视图来执行此操作。

This works but there seems like there would be a cleaner way.这行得通,但似乎会有一种更清洁的方法。

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

The simplest way to avoid using an extra container like HStack is to annotate your body property as @ViewBuilder , like this:避免使用像HStack这样的额外容器的最简单方法是将body属性注释为@ViewBuilder ,如下所示:

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

I needed to embed a view inside another conditionally, so I ended up creating a convenience if function:我需要有条件地在另一个视图中嵌入一个视图,所以我最终创建了一个方便的if函数:

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

This does return an AnyView, which is not ideal but feels like it is technically correct because you don't really know the result of this during compile time.这确实返回了一个 AnyView,这并不理想,但感觉它在技术上是正确的,因为您在编译期间并不真正知道其结果。

In my case, I needed to embed the view inside a ScrollView, so it looks like this:在我的例子中,我需要将视图嵌入到 ScrollView 中,所以它看起来像这样:

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

But you could also use it to conditionally apply modifiers too:但是您也可以使用它来有条件地应用修饰符:

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

UPDATE: Please read the drawbacks of using conditional modifiers before using this:https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/更新:请在使用此之前阅读使用条件修饰符的缺点:https ://www.objc.io/blog/2021/08/24/conditional-view-modifiers/

You didn't include it in your question but I guess the error you're getting when going without the stack is the following?您没有在问题中包含它,但我猜您在没有堆栈时遇到的错误如下?

Function declares an opaque return type, but has no return statements in its body from which to infer an underlying type函数声明了一个不透明的返回类型,但它的主体中没有返回语句来推断基础类型

The error gives you a good hint of what's going on but in order to understand it, you need to understand the concept of opaque return types .该错误为您提供了一个很好的提示,但为了理解它,您需要了解opaque return types的概念。 That's how you call the types prefixed with the some keyword.这就是你如何调用以some关键字为前缀的类型。 I didn't see any Apple engineers going deep into that subject at WWDC (maybe I missed the respective talk?), which is why I did a lot of research myself and wrote an article on how these types work and why they are used as return types in SwiftUI .我没有看到任何 Apple 工程师在 WWDC 上深入研究该主题(也许我错过了各自的演讲?),这就是为什么我自己做了很多研究并写了一篇关于这些类型如何工作以及为什么将它们用作SwiftUI中的返回类型。

🔗 What's this “some” in SwiftUI? 🔗 SwiftUI 中的“一些”是什么?

There is also a detailed technical explanation in another另一个中也有详细的技术解释

🔗 Stackoverflow post on opaque result types 🔗 Stackoverflow 关于不透明结果类型的帖子

If you want to fully understand what's going on I recommend reading both.如果您想完全了解发生了什么,我建议您阅读两者。


As a quick explanation here:作为这里的快速解释:

General Rule:一般规则:

Functions or properties with an opaque result type ( some Type )具有不透明结果类型( some Type )的函数或属性
must always return the same concrete type .必须始终返回相同的具体类型

In your example, your body property returns a different type, depending on the condition:在您的示例中,您的body属性根据条件返回不同的类型:

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

If someConditionIsTrue , it would return a TabView , otherwise a LoginView .如果someConditionIsTrue ,它将返回TabView ,否则返回LoginView This violates the rule which is why the compiler complains.这违反了编译器抱怨的规则。

If you wrap your condition in a stack view, the stack view will include the concrete types of both conditional branches in its own generic type:如果您将条件包装在堆栈视图中,堆栈视图将在其自己的泛型类型中包含两个条件分支的具体类型:

HStack<ConditionalContent<TabView, LoginView>>

As a consequence, no matter which view is actually returned, the result type of the stack will always be the same and hence the compiler won't complain.结果,无论实际返回哪个视图,堆栈的结果类型将始终相同,因此编译器不会抱怨。


💡 Supplemental: 💡补充:

There is actually a view component SwiftUI provides specifically for this use case and it's actually what stacks use internally as you can see in the example above:实际上, SwiftUI专门为此用例提供了一个视图组件,它实际上是堆栈内部使用的组件,如您在上面的示例中所见:

ConditionalContent 条件内容

It has the following generic type, with the generic placeholder automatically being inferred from your implementation:它具有以下泛型类型,泛型占位符会自动从您的实现中推断出来:

ConditionalContent<TrueContent, FalseContent>

I recommend using that view container rather that a stack because it makes its purpose semantically clear to other developers.我建议使用该视图容器而不是堆栈,因为它使其他开发人员在语义上清楚其目的。

Anyway, the issue still exists.无论如何,问题仍然存在。 Thinking mvvm-like all examples on that page breaks it.像 mvvm 一样思考该页面上的所有示例都会破坏它。 Logic of UI contains in View. UI 的逻辑包含在 View 中。 In all cases is not possible to write unit-test to cover logic.在所有情况下都不可能编写单元测试来覆盖逻辑。

PS. PS。 I am still can't solve this.我仍然无法解决这个问题。

UPDATE更新

I am ended with solution,我以解决方案结束,

View file:查看文件:

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

Based on the comments I ended up going with this solution that will regenerate the view when the api key changes by using @EnvironmentObject.根据评论,我最终选择了这个解决方案,当 api 键更改时,使用 @EnvironmentObject 重新生成视图。

UserData.swift用户数据.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内容视图.swift

import SwiftUI

struct ContentView : View {

    @EnvironmentObject var userData: UserData

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

Another approach using ViewBuilder (which relies on the mentioned ConditionalContent )使用ViewBuilder的另一种方法(依赖于提到的ConditionalContent

buildEither + optional 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

(There's also buildIf , but I couldn't figure out its syntax yet. ¯\_(ツ)_/¯ ) (还有buildIf ,但我还不知道它的语法¯\_(ツ)_/¯


One could also wrap the result View into AnyView也可以将结果View包装到AnyView

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

But it kinda feels wrong...但是感觉有点不对...


Both examples produce the same result:这两个示例产生相同的结果:

操场

I chose to solve this by creating a modifier that makes a view "visible" or "invisible".我选择通过创建一个使视图“可见”或“不可见”的修饰符来解决这个问题。 The implementation looks like the following:实现如下所示:

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

Then to use it to solve your example, you would simply invert the isVisible value as seen here:然后使用它来解决您的示例,您只需反转isVisible值,如下所示:

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

I have considered wrapping this into some kind of an "If" view that would take two views, one when the condition is true and one when the condition is false, but I decided that my present solution is both more general and more readable.我已经考虑将其包装成某种“如果”视图,该视图将采用两种视图,一种在条件为真时,另一种在条件为假时,但我认为我目前的解决方案更通用且更具可读性。

Extension with the condition param works well for me (iOS 14):带有条件参数的扩展对我来说效果很好(iOS 14):

import SwiftUI

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

    }
}

Example usage:示例用法:

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

Previous answers were correct, however, I would like to mention, you may use optional views inside you HStacks.以前的答案是正确的,但是,我想提一下,您可以在 HStacks 中使用可选视图。 Lets say you have an optional data eg.假设您有一个可选数据,例如。 the users address.用户地址。 You may insert the following code:您可以插入以下代码:

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

Instead of the other approach:而不是其他方法:

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

Since it would return an Optional text, the framework handles it fine.由于它会返回一个可选文本,因此框架可以很好地处理它。 This also means, using an expression instead of the if statement is also fine, like:这也意味着,使用表达式而不是 if 语句也可以,例如:

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

In your case, the two can be combined:在您的情况下,可以将两者结合起来:

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

Using beta 4使用测试版 4

I extended @gabriellanata's answer for up to two conditions.我将@gabriellanata 的答案扩展到最多两个条件。 You can add more if needed.如果需要,您可以添加更多。 You use it like this:你像这样使用它:

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

The 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 }
    }
}

How about that?那个怎么样?

I have a conditional contentView , which either is a text or an icon .我有一个条件contentView ,它可以是texticon I solved the problem like this.我解决了这样的问题。 Comments are very appreciated, since I don't know if this is really "swifty" or just a "hack", but it works:非常感谢评论,因为我不知道这是否真的是“swifty”或只是“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)
    }
}

After Googling for like 20 minutes, I ended up writing this container view with a conditional initializer. 在谷歌搜索大约20分钟之后,我最终使用条件初始化程序编写了该容器视图。 It writes super cleanly, and its definition is short, but it doesn't automatically update. 它写得很干净,定义很短,但是不会自动更新。 For that behavior, use Michael 's answer. 对于这种行为,请使用迈克尔的答案。

It works because SwiftUI throws out nil views automatically. 之所以有效,是因为SwiftUI会自动抛出nil视图。

For run-once conditionals 对于一次性条件

Usage 用法

If(someBoolean) {
    Text("someBoolean is true!")
}

Source 资源

struct If<Output : View> : View {
    init?(_ value: Bool, product: @escaping () -> Output) {
        if value {
            self.product = product
        } else {
            return nil
        }
    }

    private let product: () -> Output

    var body: some View {
        product()
    }
}

If the error message is如果错误信息是

Closure containing control flow statement cannot be used with function builder 'ViewBuilder'包含控制流语句的闭包不能与函数构建器“ViewBuilder”一起使用

Just hide the complexity of the control flow from the ViewBuilder:只需从 ViewBuilder 中隐藏控制流的复杂性:

This works:这有效:

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

Use Group instead of HStack使用Group而不是 HStack

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

If you want to navigate to two different views using NavigationLink, you can navigate using ternary operator.如果要使用 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")
    }
    }

Here's a very simple to use modifier which uses a boolean test to decide if a view will be rendered.这是一个非常简单易用的修饰符,它使用 boolean 测试来决定是否将呈现视图。 Unlike other solutions posted here it doesn't rely on the use of ÀnyView .与此处发布的其他解决方案不同,它不依赖于ÀnyView的使用。 This is how to use it:这是如何使用它:

var body: some View {
    VStack {
        FooView()
            .onlyIf(someCondition)
    }
}

This reads nicer than the default ifthen construct as it removes the additional indentation.这比默认的ifthen construct 更好,因为它删除了额外的缩进。

To replace an ifthenelse construct, this is the obvious solution:要替换ifthenelse构造,这是显而易见的解决方案:

var body: some View {
    VStack {
        FooView()
            .onlyIf(someCondition)

        BarView()
            .onlyIf(!someCondition)
    }
}

This is the definition of the onlyIf modifier:这是onlyIf修饰符的定义:

struct OnlyIfModifier: ViewModifier {
    var condition: Bool

    func body(content: Content) -> some View {
        if condition {
            content
        }
    }
}

extension View {
    func onlyIf(_ condition: Bool) -> some View {
        modifier(OnlyIfModifier(condition: condition))
    }
}

Give it a try – it will surely clean up your code and improve overall readability.试一试——它肯定会清理你的代码并提高整体可读性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM