我正在尝试找出正确的方法来有条件地包含swiftui视图。我无法if
直接使用视图的内部,而不得不使用堆栈视图来做到这一点。
这可行,但似乎会有更清洁的方法。
var body: some View {
HStack() {
if keychain.get("api-key") != nil {
TabView()
} else {
LoginView()
}
}
}
我正在尝试找出正确的方法来有条件地包含swiftui视图。我无法if
直接使用视图的内部,而不得不使用堆栈视图来做到这一点。
这可行,但似乎会有更清洁的方法。
var body: some View {
HStack() {
if keychain.get("api-key") != nil {
TabView()
} else {
LoginView()
}
}
}
ConditionalContent
在我看来,解释一个@ViewBuilder
块时从编译器生成的一种或/或一种类型的结构。我认为这就是我们ifs/elses
内部小组的方式。堆栈等已翻译。我认为是因为它会产生一个View
。在您的情况下,该代码if/else
将转换为ConditionalContent<TabView, LoginView>
。
SwiftUI
,因此定义会花费一些时间best practice
。代码看起来不错,所以继续吧!您可以做的改进:在视图中确定一个状态,以决定是否显示TabView
or LoginView
,然后通过视图模型(通过)更改该状态Binding
。
HStack { ... }
仅用于提供“外部组”(以进行if-else编译),则也可以使用Group { ... }
。
Answers:
避免使用多余的容器的最简单方法HStack
是将body
属性注释为@ViewBuilder
,如下所示:
@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
}
}
}
这确实会返回一个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)
}
}
您没有在问题中包含它,但是我想以下是您在没有堆栈时遇到的错误?
函数声明了不透明的返回类型,但是在其主体中没有用于从其推断基础类型的返回语句
该错误为您提供了一个很好的提示,但要了解它,您需要了解不透明返回类型的概念。这就是您调用带有some
关键字前缀的类型的方式。我没有在WWDC上看到任何Apple工程师对此主题进行深入研究(也许我错过了各自的演讲?),这就是为什么我自己进行了大量研究并撰写了有关这些类型的工作方式以及为什么将它们用作研究对象的文章的原因。在SwiftUI中返回类型。
另外还有详细的技术说明
如果您想完全了解正在发生的事情,建议您同时阅读两者。
作为此处的简要说明:
一般规则:
结果类型不透明(
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'
var body: some View { ConditionalContent() { if userData.apiKey != nil { TabView() } else { LoginView() } } }
ConditionalContent
应该是这里的正确工具,并提供其文档:查看显示两个可能子级之一的内容。我在Twitter上读了一些帖子,提到了SwiftUI中仍然存在的一些错误。也许这就是其中之一。现在,我将使用堆栈或组,或者希望其他人可以为如何ConditionalContent
正确使用提供良好的答案。
ConditionalContent
确权的工具来使用,但如果你仔细看,你会看到,它没有公开的初始化,所以你不应该直接使用它,但ViewBuilder
一对夫妇的方法实际上返回ConditionContent
。我的猜测是,使用if
语句是实现此目标的唯一方法。
ConditionalContent
仍然存在?您的链接返回一个404
无论如何,问题仍然存在。认为该页面上的所有示例都像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))
}
}
}
makeView()
构成特定视图的your不应属于View Model。视图不包含域逻辑,但可以包含表示逻辑。你可以放进makeView()
去RootView
。
基于这些评论,我最终使用了该解决方案,当使用@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()
}
}
}
}
if let
,出现编译错误:Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
,这可能是相关的:medium.com/q42-engineering/swiftui-optionals-ead04edd439f
使用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,但是我还不能弄清楚它的语法。¯\_(ツ)_/¯
)
也可以将结果包装
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
但这有点不对劲...
这两个示例产生相同的结果:
先前的答案是正确的,但是,我想提一下,您可以在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
我选择通过创建一个使视图“可见”或“不可见”的修饰符来解决此问题。该实现如下所示:
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时,但是我认为我现在的解决方案既更通用又更易读。
那个怎么样?
我有一个有条件的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)
}
}
我将@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 }
}
}
如果错误信息是
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")
}
}
}
}
如果要使用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")
}
}