在 SwiftUI 中创建计算的 @State 变量
Create a computed @State variable in SwiftUI
假设我正在设计一个要求用户输入用户名的 SwiftUI 屏幕。屏幕将进行一些检查以确保用户名有效。如果用户名无效,它会显示一条错误消息。如果用户点击 "Dismiss",它会隐藏错误消息。
最后我可能会得到这样的结果:
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() })
这将很容易地显示错误消息并正确地消除它们。
现在假设我想使用不同的组件而不是 Button 来调用 dismiss 方法。此外,假设我使用的组件只接受 Binding
(例如 Toggle
)。 (注意:我意识到这不是一个理想的组件,但在这个简化的演示应用程序中用于说明目的。)我可能会尝试创建一个 computed property 来抽象此行为并最终得到:
@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() })
... 但是,这不是有效的语法并在 @State
行产生以下错误:
Property wrapper cannot be applied to a computed property
如何创建可绑定计算 属性? IE。带有自定义 getter 和 setter.
的 Binding
虽然不理想,因为它 (A) 仅提供 setter,并且 (B) 添加状态重复(这违背了 SwiftUI 的单一真相原则),但我认为我能够解决这有一个正常的状态变量:
@State private var bindableIsVisibleError: Bool = true {
didSet { self.model.dismissUsernameResultError() }
}
这不起作用,因为 didSet
从未被调用过。
一种解决方案是直接使用 Binding,它允许您明确指定 getter 和 setter:
func bindableIsVisibleError() -> Binding<Bool> {
return Binding(
get: { return self.model.usernameResult.isVisibleError },
set: { if ![=10=] { self.model.dismissUsernameResultError() } })
}
然后你会像这样使用它:
Toggle(isOn: bindableIsVisibleError(), label: { EmptyView() })
虽然这可行,但它看起来不如使用计算的 属性 干净,而且我不确定创建绑定的最佳方法是什么? (即使用示例中的函数,使用只获取变量,或其他东西。)
我更喜欢使用计算 属性 和“即时”绑定的方法
private var bindableIsVisibleError: Binding<Bool> { Binding (
get: { self.model.usernameResult.isVisibleError },
set: { if ![=10=] { self.model.dismissUsernameResultError() } }
)
}
和用法(按规定)
Toggle(isOn: bindableIsVisibleError, label: { EmptyView() })
假设我正在设计一个要求用户输入用户名的 SwiftUI 屏幕。屏幕将进行一些检查以确保用户名有效。如果用户名无效,它会显示一条错误消息。如果用户点击 "Dismiss",它会隐藏错误消息。
最后我可能会得到这样的结果:
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() })
这将很容易地显示错误消息并正确地消除它们。
现在假设我想使用不同的组件而不是 Button 来调用 dismiss 方法。此外,假设我使用的组件只接受 Binding
(例如 Toggle
)。 (注意:我意识到这不是一个理想的组件,但在这个简化的演示应用程序中用于说明目的。)我可能会尝试创建一个 computed property 来抽象此行为并最终得到:
@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() })
... 但是,这不是有效的语法并在 @State
行产生以下错误:
Property wrapper cannot be applied to a computed property
如何创建可绑定计算 属性? IE。带有自定义 getter 和 setter.
的Binding
虽然不理想,因为它 (A) 仅提供 setter,并且 (B) 添加状态重复(这违背了 SwiftUI 的单一真相原则),但我认为我能够解决这有一个正常的状态变量:
@State private var bindableIsVisibleError: Bool = true {
didSet { self.model.dismissUsernameResultError() }
}
这不起作用,因为 didSet
从未被调用过。
一种解决方案是直接使用 Binding,它允许您明确指定 getter 和 setter:
func bindableIsVisibleError() -> Binding<Bool> {
return Binding(
get: { return self.model.usernameResult.isVisibleError },
set: { if ![=10=] { self.model.dismissUsernameResultError() } })
}
然后你会像这样使用它:
Toggle(isOn: bindableIsVisibleError(), label: { EmptyView() })
虽然这可行,但它看起来不如使用计算的 属性 干净,而且我不确定创建绑定的最佳方法是什么? (即使用示例中的函数,使用只获取变量,或其他东西。)
我更喜欢使用计算 属性 和“即时”绑定的方法
private var bindableIsVisibleError: Binding<Bool> { Binding (
get: { self.model.usernameResult.isVisibleError },
set: { if ![=10=] { self.model.dismissUsernameResultError() } }
)
}
和用法(按规定)
Toggle(isOn: bindableIsVisibleError, label: { EmptyView() })