在SwiftUI中创建一个计算的@State变量

37

假设我正在设计一个SwiftUI屏幕,要求用户输入用户名。该屏幕将进行一些检查以确保用户名有效。如果用户名无效,则会显示错误消息。如果用户点击“关闭”,它将隐藏错误消息。

最终,我可能会得到像这样的东西:

enter image description here

enum UsernameLookupResult: Equatable {
    case success
    case error(message: String, dismissed: Bool)

    var isSuccess: Bool { return self == .success }
    var isVisibleError: Bool {
        if case .error(message: _, dismissed: false) = self {
            return true
        } else {
            return false
        }
    }
    var message: String {
        switch self {
        case .success:
            return "That username is available."
        case .error(message: let message, dismissed: _):
            return message
        }
    }
}

enum NetworkManager {
    static func checkAvailability(username: String) -> UsernameLookupResult {
        if username.count < 5 {
            return .error(message: "Username must be at least 5 characters long.", dismissed: false)
        }

        if username.contains(" ") {
            return .error(message: "Username must not contain a space.", dismissed: false)
        }

        return .success
    }
}

class Model: ObservableObject {
    @Published var username = "" {
        didSet {
            usernameResult = NetworkManager.checkAvailability(username: username)
        }
    }
    @Published var usernameResult: UsernameLookupResult = .error(message: "Enter a username.", dismissed: false)

    func dismissUsernameResultError() {
        switch usernameResult {
        case .success:
            break
        case .error(message: let message, dismissed: _):
            usernameResult = .error(message: message, dismissed: true)
        }
    }
}

struct ContentView: View {
    @ObservedObject var model: Model

    var body: some View {
        VStack {
            Form {
                TextField("Username", text: $model.username)
                Button("Submit", action: {}).disabled(!model.usernameResult.isSuccess)
            }
            Spacer()
            if model.usernameResult.isSuccess || model.usernameResult.isVisibleError {
                HStack(alignment: .top) {
                    Image(systemName: model.usernameResult.isSuccess ? "checkmark.circle" : "xmark.circle")
                        .foregroundColor(model.usernameResult.isSuccess ? Color.green : Color.red)
                        .padding(.top, 5)
                    Text(model.usernameResult.message)
                    Spacer()
                    if model.usernameResult.isSuccess {
                        EmptyView()
                    } else {
                        Button("Dismiss", action: { self.model.dismissUsernameResultError() })
                    }
                }.padding()
            } else {
                EmptyView()
            }
        }
    }
}
只需将我的“dismiss”操作设置为Button,就可以轻松实现dismiss行为:
Button("Dismiss", action: { self.model.dismissUsernameResultError() })

这将轻松显示错误信息并正确解除它们。

现在想象一下,我想使用一个不同的组件来调用解除方法。 此外,想象一下,我使用的组件仅接受 Binding (例如 Toggle )。 (注意:我意识到这不是一个理想的组件,但这是为了在这个简化的演示应用程序中说明)我可以尝试创建计算属性来抽象此行为,最终得到:

<code><code>@State private var bindableIsVisibleError: Bool {
    get { return self.model.usernameResult.isVisibleError }
    set { if !newValue { self.model.dismissUsernameResultError() } }
}

// ...


// replace Dismiss Button with:
Toggle(isOn: $bindableIsVisibleError, label: { EmptyView() })
</code></code>

... 然而,这不是有效的语法,并在 @State 行上产生以下错误:

属性包装器无法应用于计算属性

如何创建可绑定的计算属性?即具有自定义 getter 和 setter 的 Binding


虽然不太理想,因为它(A)仅提供了一个 setter,并且(B)添加了状态重复(这与 SwiftUI 的单一真相原则相抵触),我认为可以使用普通状态变量来解决此问题:

@State private var bindableIsVisibleError: Bool = true {
    didSet { self.model.dismissUsernameResultError() }
}

然而这并不起作用,因为didSet从未被调用。

3个回答

63

这里是我喜欢的一种使用计算属性和“即时”绑定的方法

private var bindableIsVisibleError: Binding<Bool> { Binding (
    get: { self.model.usernameResult.isVisibleError },
    set: { if !$0 { self.model.dismissUsernameResultError() } }
    )
}

和使用方式(如规定)

Toggle(isOn: bindableIsVisibleError, label: { EmptyView() })

4
有没有一种方法可以创建一个只读的计算属性绑定?即,一个没有 setter 的绑定。 - JAHelia
13
@JAHelia,是的,只需要提供一个空的setter,例如 set: { _ in } - Asperi
1
还有private(set) - shim
@shim 如何准确使用 private(set) - Alexandre G
private(set) var foo: Bar - shim

3

一种解决方法是直接使用 Binding,它允许你指定明确的 getter 和 setter:

func bindableIsVisibleError() -> Binding<Bool> {
    return Binding(
        get: { return self.model.usernameResult.isVisibleError },
        set: { if !$0 { self.model.dismissUsernameResultError() } })
}

那么你就可以像这样使用它:

Toggle(isOn: bindableIsVisibleError(), label: { EmptyView() })

尽管这个方法可行,但它没有使用计算属性看起来那么干净整洁,而且我不确定创建绑定的最佳方法是什么?(例如使用函数,像示例中一样使用只读变量,还是其他方法。)

0

考虑在这种情况下使用.onReceive().onChange()而不是didSet可能会很有用。 如果由于某种原因不能满足您的需求,我建议采用类似的方法。由于didSet仅在声明了这些变量(带有didSet)的视图内部触发@StateBinding变量,可以使用以下扩展来在值更改时执行代码,无论如何。

extension Binding {
    func didSet(_ didSet: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                didSet(newValue)
            }
        )
    }
}

然后你可以像这样实现你想要的:
...
@State private var isVisibleError: Bool = false
...
                        // replace Dismiss Button with:
                        Toggle(isOn: $isVisibleError.didSet({
                            if !$0 { self.model.dismissUsernameResultError() }
                        }),
                               label: { EmptyView() })
...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接