在SwiftUI中有条件地使用视图

时间:2019-06-09 19:07:48

标签: swift swiftui

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

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

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

15 个答案:

答案 0 :(得分:26)

避免使用诸如let elements = document.body.getElementsByTagName('div'); Array.prototype.forEach.call(elements, e => { e.innerHTML = "foo"; }); 这样的额外容器的最简单方法是将HStack属性注释为body,如下所示:

@ViewBuilder

答案 1 :(得分:5)

无论如何,问题仍然存在。 认为该页面上的所有示例都像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))
        }
    }
}

答案 2 :(得分:4)

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

extension View {
   func `if`<Content: View>(_ conditional: Bool, content: (Self) -> Content) -> some View {
        if conditional {
            return AnyView(content(self))
        } else {
            return AnyView(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)
    }
}

答案 3 :(得分:3)

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

  

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

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

?What’s this “some” in SwiftUI?

另外还有一个详细的技术说明

?Stackoverflow post on opaque result types

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


在这里作为快速说明:

  

一般规则:

     

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

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

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

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

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

HStack<ConditionalContent<TabView, LoginView>>

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


?补充:

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

ConditionalContent

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

ConditionalContent<TrueContent, FalseContent>

我建议使用该视图容器而不是堆栈,因为它在语义上使其他开发人员可以清楚地了解其用途。

答案 4 :(得分:2)

基于这些评论,我最终使用了该解决方案,当使用@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()
            }
        }
    }
}

答案 5 :(得分: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)
    }
}

我已经考虑过将其包装到某种“如果”视图中 采取两种观点,一种是当条件为真时,另一种是条件为真时 错误,但我认为我目前的解决方案既更通用 可读。

答案 6 :(得分: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)
    }
}

答案 7 :(得分: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 }
    }
}

答案 8 :(得分:0)

在谷歌搜索大约20分钟之后,我最终使用条件初始化程序编写了该容器视图。它写得很干净,定义很短,但是不会自动更新。对于这种行为,请使用 Michael 的答案。

之所以有效,是因为SwiftUI会自动抛出nil个视图。

用于一次性条件

用法

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

来源

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

答案 9 :(得分:0)

使用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

但这有点不对劲...


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

playground

答案 10 :(得分:0)

以前的答案是正确的,但是,我想提一下,您可以在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

答案 11 :(得分: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")
            }
        }
    }
}

答案 12 :(得分:0)

使用 Group 代替HStack

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

答案 13 :(得分:0)

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

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

用法示例:

fp

答案 14 :(得分: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")
    }
    }