web-dev-qa-db-ja.com

SwiftUIで計算された@State変数を作成する

ユーザーにユーザー名の入力を求める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() })

これにより、エラーメッセージが簡単に表示され、正しく閉じることができます。

ここで、dismissメソッドを呼び出すために、Buttonの代わりに別のコンポーネントを使用したいとします。さらに、私が使用するコンポーネントがBinding(たとえばToggle)のみを受け取ることを想像してください。 (注:これは使用するのに理想的なコンポーネントではないことに気づきましたが、これはこの簡略化されたデモアプリでの例示目的です。) 計算されたプロパティ を作成してこの動作を抽象化し、 :

@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行:

プロパティラッパーは計算されたプロパティに適用できません

バインド可能な計算プロパティを作成するにはどうすればよいですか?つまりカスタムゲッターとセッターを備えたBinding.


(A)セッターのみを提供し、(B)状態の複製を追加する(SwiftUIの真のプリンシパルの単一ソースに反する)ので理想的ではありませんが、通常の状態変数でこれを解決できると思いました。

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

これは機能しませんが、didSetが呼び出されることはありません。

6
Senseful

1つの解決策は、Bindingを直接使用することです。これにより、明示的なゲッターとセッターを指定できます。

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変数を使用するか、または何か他のものを使用します。)

0
Senseful