【问题标题】:Create a computed @State variable in SwiftUI在 SwiftUI 中创建一个计算的 @State 变量
【发布时间】:2020-04-07 23:54:00
【问题描述】:

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

最后我可能会得到这样的结果:

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

只要我的“解雇”动作是Button,就很容易实现解雇行为:

Button("Dismiss", action: { self.model.dismissUsernameResultError() })

这将很容易显示错误消息并正确关闭它们。

现在假设我想使用不同的组件而不是 Button 来调用解除方法。此外,想象一下我使用的组件只接受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 行产生以下错误:

属性包装器不能应用于计算属性

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


虽然不理想,因为它 (A) 只提供了一个设置器,并且 (B) 添加了状态重复(这违背了 SwiftUI 的单一事实来源原则),但我认为我可以使用正常的状态变量来解决这个问题:

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

这不起作用,尽管didSet 从未被调用过。

【问题讨论】:

    标签: swiftui computed-properties


    【解决方案1】:

    这是我更喜欢的计算属性和“即时”绑定的方法

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

    和用法(按规定)

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

    【讨论】:

    • 有没有办法创建一个只读的计算属性绑定?即没有设置器的绑定。
    • @JAHelia,是的,只需提供空设置器,例如set: { _ in }
    • 还有private(set)
    【解决方案2】:

    一种解决方案是直接使用 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() })
    

    虽然这可行,但它看起来不像使用计算属性那样干净,而且我不确定创建 Binding 的最佳方法是什么? (即使用示例中的函数,使用 get-only 变量或其他东西。)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-11-10
      • 2022-12-17
      • 2019-11-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-10-19
      相关资源
      最近更新 更多