Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deriving binding from existing SwiftUI @States

I've been playing around with SwiftUI and Combine and feel like there is probably a way to get a hold of the existing @State properties in a view and create a new one.

For example, I have a password creation View which holds a password and a passwordConfirm field for the user. I want to take those two @State properties and derive a new @State that I can use in my view that asserts if the input is valid. So for simplicity: not empty and equal.

The Apple docs say there is a publisher on a binding, though I can't appear to get ahold of it.

This is some non-functioning pseudo code:

import SwiftUI
import Combine

struct CreatePasswordView : View {
    @State var password = ""
    @State var confirmation = ""
    lazy var valid = {
        return self.$password.publisher()
            .combineLatest(self.$confirmation)
            .map { $0 != "" && $0 == $1 }
    }

    var body: some View {
        SecureField($password, placeholder: Text("password"))

        SecureField($confirmation, placeholder: Text("confirm password"))

        NavigationButton(destination: NextView()) { Text("Done") }
            .disabled(!valid)
    }
}

Anyone found. the appropriate way of going about this / if it's possible?

UPDATE Beta 2:

As of beta 2 publisher is available so the first half of this code now works. The second half of using the resulting publisher within the View I've still not figured out (disabled(!valid)).

import SwiftUI
import Combine

struct CreatePasswordView : View {
    @State var password = ""
    @State var confirmation = ""

    lazy var valid = {
        Publishers.CombineLatest(
            password.publisher(),
            confirmation.publisher(),
            transform: { String($0) != "" && $0 == $1 }
        )
    }()

    var body: some View {
        SecureField($password, placeholder: Text("password"))

        SecureField($confirmation, placeholder: Text("confirm password"))

        NavigationButton(destination: NextView()) { Text("Done") }
            .disabled(!valid)
    }
}

Thanks.

like image 727
freebie Avatar asked Jun 10 '19 15:06

freebie


1 Answers

I wouldn't be playing with @State/@Published as Combine is in beta at the moment, but here's a simple workaround for what you're trying to achieve.

I'd implement a view model to hold password, password confirmation, and whether it's valid or not

class ViewModel: NSObject, BindableObject {

    var didChange = PassthroughSubject<Void, Never>()

    var password: String = "" {
        didSet {
            didChange.send(())
        }
    }

    var passwordConfirmation: String = "" {
        didSet {
            didChange.send(())
        }
    }

    var isPasswordValid: Bool {
        return password == passwordConfirmation && password != ""
    }

}

In this way, the view is recomputed anytime the password or the confirmation changes.

Then I would make a @ObjectBinding to the view model.

struct CreatePasswordView : View {

    @ObjectBinding var viewModel: ViewModel

    var body: some View {
        NavigationView {
            VStack {
                SecureField($viewModel.password,
                            placeholder: Text("password"))
                SecureField($viewModel.passwordConfirmation,
                            placeholder: Text("confirm password"))
                NavigationButton(destination: EmptyView()) { Text("Done") }
                    .disabled(!viewModel.isPasswordValid)
            }
        }
    }

}

I had to put the views in a NavigationView, because NavigationButton doesn't seem to enable itself if it isn't in one of them.

like image 143
Matteo Pacini Avatar answered Oct 26 '22 16:10

Matteo Pacini