web-dev-qa-db-fra.com

Créer une variable @State calculée dans SwiftUI

Imaginez que je conçois un écran SwiftUI qui demande à l'utilisateur d'entrer un nom d'utilisateur. L'écran fera quelques vérifications pour s'assurer que le nom d'utilisateur est valide. Si le nom d'utilisateur n'est pas valide, il affichera un message d'erreur. Si l'utilisateur appuie sur "Ignorer", le message d'erreur sera masqué.

En fin de compte, je peux me retrouver avec quelque chose comme ça:

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

Tant que mon action "rejeter" est un Button, il est facile d'obtenir le comportement de rejet:

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

Cela affichera facilement les messages d'erreur et les ignorera correctement.

Imaginez maintenant que je souhaite utiliser un composant différent au lieu de Button pour appeler la méthode de rejet. De plus, imaginez que le composant que j'utilise ne prend qu'un Binding (par exemple un Toggle). (Remarque: je me rends compte que ce n'est pas un composant idéal à utiliser, mais c'est à des fins d'illustration dans cette application de démonstration simplifiée.) Je peux essayer de créer un propriété calculée pour résumer ce comportement et finir avec :

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

... cependant, ce n'est pas une syntaxe valide et génère l'erreur suivante sur le @State ligne:

L'encapsuleur de propriété ne peut pas être appliqué à une propriété calculée

Comment puis-je créer une propriété calculée pouvant être liée? C'est à dire. un Binding avec un getter et un setter personnalisés.


Bien que cela ne soit pas idéal car il (A) ne fournirait qu'un setter et (B) ajouterait une duplication d'état (ce qui va à l'encontre de la source unique de vérité de SwiftUI), j'ai pensé que je serais en mesure de résoudre cela avec une variable d'état normal:

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

Cela ne fonctionne pas, bien que didSet ne soit jamais appelé.

6
Senseful

Une solution consiste à utiliser directement une liaison, ce qui vous permet de spécifier un getter et un setter explicites:

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

Vous l'utiliseriez alors comme ceci:

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

Bien que cela fonctionne, cela n'a pas l'air aussi propre que d'utiliser une propriété calculée, et je ne sais pas quelle est la meilleure façon de créer la liaison? (C'est-à-dire en utilisant une fonction comme dans l'exemple, en utilisant une variable get-only ou autre chose.)

0
Senseful