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.

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()
}
}
}
86869 次浏览

您的问题中没有包含它,但是我猜没有堆栈的情况下会出现以下错误?

函数声明了一个不透明的返回类型,但是在它的主体中没有返回语句来推断基础类型

这个错误为您提供了正在发生的事情的一个很好的提示,但是为了理解它,您需要理解 opaque return types的概念。这就是如何调用带有 some关键字前缀的类型。我没有看到任何苹果工程师在 WWDC 上深入探讨这个话题(也许我错过了相关的演讲?),这就是为什么我自己做了很多研究,并写了一篇关于这些类型如何工作,以及为什么他们被用作返回类型在 SwiftUI

🔗 SwiftUI 里的“ some”是什么意思?

There is also a detailed technical explanation in another

🔗 不透明结果类型上的堆栈溢出文章

如果你想完全了解发生了什么,我建议你两本都读一下。


作为一个简短的解释:

一般规则:

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

在您的示例中,您的 body属性返回 与众不同类型,具体取决于以下条件:

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

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

如果将条件包装在堆栈视图中,堆栈视图将在其自己的泛型类型中包含两个条件分支的具体类型:

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.


补充资料:

实际上有一个视图组件 SwiftUI专门为这个用例提供,它实际上就是栈内部使用的,正如你在上面的例子中看到的:

条件内容

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

ConditionalContent<TrueContent, FalseContent>

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

基于注释,我最终使用了这个解决方案,它将在 api 键更改时使用@Environment 对象重新生成视图。

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

Another approach using ViewBuilder (which relies on the mentioned ConditionalContent)

Build + 可选

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

(还有 如果,但是我还不知道它的语法。 ¯\_(ツ)_/¯)


还可以将结果 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


但感觉有点不对劲。


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

playground

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

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

而不是另一种方法:

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

因为它会返回一个可选的文本,所以框架可以很好地处理它。这也意味着,使用表达式代替 if 语句也是可以的,比如:

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

在你的例子中,两者可以结合起来:

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

使用 Beta 4

The simplest way to avoid using an extra container like HStack is to annotate your body property as @ViewBuilder, like this:

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

我需要有条件地将一个视图嵌入到另一个视图中,因此我最终创建了一个方便的 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.

在我的例子中,我需要将视图嵌入到 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)
}
}

更新: 在使用前请先阅读使用条件修饰符的缺点: < a href = “ https://www.objecc.io/blog/2021/08/24/provision-view-Amendment/”rel = “ norefrer”> https://www.objc.io/blog/2021/08/24/conditional-view-modifiers/

无论如何,问题仍然存在。 像那个页面上的所有例子一样思考 mvvm 会打破它。 视图中包含的 UI 逻辑。 在所有情况下都不可能编写单元测试来覆盖逻辑。

PS. I am still can't solve this.

更新

I am ended with solution,

查看文件:

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

How about that?

我有一个条件 ContentView,它可以是 短信也可以是 偶像。我就这样解决了问题。非常感谢您的评论,因为我不知道这是真的“迅速”还是只是“黑客攻击”,但它确实有效:

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

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

然后使用它来解决你的例子,你只需要反转 isVisible值,如下所示:

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

我已经考虑过把这个包装成某种“如果”的观点 取两个视图,一个是条件为真,另一个是条件为 错误,但是我认为我现在的解决方案更一般,也更多 可读。

如果错误消息是

包含控制流语句的闭包不能与函数生成器“ 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")
}
}
}
}

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

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

使用 小组代替 HStack

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

使用条件参数的扩展对我来说工作得很好(iOS14) :

import SwiftUI


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


}
}

示例用法:

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

如果要使用 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")
}
}