Imagine I'm designing a SwiftUI screen that asks for the user to enter a Username. The screen will do some checking to ensure the username is valid. If the username is invalid, it'll show an error message. If the user taps "Dismiss", it'll hide the error message.
In the end I may end up with something like this:
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()
}
}
}
}
As long as my "dismiss" action is a Button
, it's easy to achieve the dismiss behavior:
Button("Dismiss", action: { self.model.dismissUsernameResultError() })
This will easily show error messages and dismiss them correctly.
Now imagine I want to use a different component instead of Button to call the dismiss method. Furthermore, imagine the component I use only takes in a Binding
(e.g. a Toggle
). (Note: I realize this is not an ideal component to use, but this is for illustrative purposes in this simplified demo app.) I may attempt to create a computed property to abstract this behavior and end up with:
@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() })
... however, this is not valid syntax and produces the following error on the @State
line:
Property wrapper cannot be applied to a computed property
How can I create a bindable computed property? I.e. a Binding
with a custom getter and setter.
Although not ideal as it would (A) only provide a setter, and (B) adds state duplication (which goes against SwiftUI's single source of truth principal), I thought I would be able to solve this with a normal state variable:
@State private var bindableIsVisibleError: Bool = true {
didSet { self.model.dismissUsernameResultError() }
}
This does not work, though as didSet
is never called.
Swift Computed Property For example, class Calculator { // define stored property var num1: Int = 0 ... } Here, num1 is a stored property, which stores some value for an instance of Calculator . Here, sum is a computed property that doesn't store a value, rather it computes the addition of two values.
Defining a computed property To start, you have to write a variable and explicitly declare the type of the property to help the compiler know what kind of value will be assigned to it. Do not assign a default value. Instead open a bracket after the type declaration and start working on the getter.
Stored properties store constant and variable values as part of an instance, whereas computed properties calculate (rather than store) a value. Computed properties are provided by classes, structures, and enumerations. Stored properties are provided only by classes and structures.
A computed property is a property that calculates and returns a value, rather than just store it.
Here is an approach I prefer with computed property & binding "on-the-fly"
private var bindableIsVisibleError: Binding<Bool> { Binding (
get: { self.model.usernameResult.isVisibleError },
set: { if !$0 { self.model.dismissUsernameResultError() } }
)
}
and usage (as specified)
Toggle(isOn: bindableIsVisibleError, label: { EmptyView() })
One solution is to use a Binding directly, which allows you to specify an explicit getter and setter:
func bindableIsVisibleError() -> Binding<Bool> {
return Binding(
get: { return self.model.usernameResult.isVisibleError },
set: { if !$0 { self.model.dismissUsernameResultError() } })
}
You would then use it like so:
Toggle(isOn: bindableIsVisibleError(), label: { EmptyView() })
Although this works, it doesn't look as clean as using a computed property, and I'm not sure what the best way to create the Binding is? (I.e. using a function as in the example, using a get-only variable, or something else.)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With