swiftvalidationswiftuicombine

SwiftUI Combine ViewModel Email Validation


How can I validate the email inside view model combining that two codes using combine?

This is inside my RegisterViewModel

private var isEmailValidPublisher: ValidatePublisher {
        $email
            .removeDuplicates()
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).count >= 10}
            .handleEvents(receiveOutput: { [weak self] in $0 ? (self?.emailMessage = "") : (self?.emailMessage = "Invalid email")})
            .eraseToAnyPublisher()
    }

I want to add this validation inside isEmailValidPublisher.

func isValidEmail() -> Bool {
        let emailRegEx = "(?:[\\p{L}0-9!#$%\\&'*+/=?\\^_`{|}~-]+(?:\\.[\\p{L}0-9!#$%\\&'*+/=?\\^_`{|}" + "~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\" + "x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[\\p{L}0-9](?:[a-" + "z0-9-]*[\\p{L}0-9])?\\.)+[\\p{L}0-9](?:[\\p{L}0-9-]*[\\p{L}0-9])?|\\[(?:(?:25[0-5" + "]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-" + "9][0-9]?|[\\p{L}0-9-]*[\\p{L}0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21" + "-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])"
        
        let emailValidation = NSPredicate(format:"SELF MATCHES[c] %@", emailRegEx)
        return emailValidation.evaluate(with: self)
    }

how can I mix both of them inside isEmailValidPublisher so that I can use that info in my signupview like below

 Image(systemName: "envelope")
                    .foregroundColor(registerVM.emailMessage != "" ? .red: .blue)

Solution

  • Let's say you have a simple View like this:

    struct ContentView: View {
        
        @StateObject var controller = Controller()
        
        var body: some View {
            VStack {
                TextField("email", text: $controller.email)
                    .foregroundColor(controller.inputValid ? .primary : .red)
                controller.validationMessage.map { message in
                    Text(message)
                        .foregroundColor(.red)
                }
            }
        }
    }
    

    and you want to have the logic in your ObservableObject:

    class Controller: ObservableObject {
        
        @Published var email = ""
        @Published private(set) var validationMessage: String?
        
        var inputValid: Bool {
            validationMessage == nil
        }
        
        
        init() {
            // you subscribe to any changes to the email field input
            $email
            // you ignore the first empty value that it gets initialised with
                .dropFirst()
            // you give the user a bit of time to finish typing
                .debounce(for: 0.6, scheduler: RunLoop.main)
            // you get rid of duplicated inputs as they do not change anything in terms of validation
                .removeDuplicates()
            // you validate the input string and in case of problems publish an error message 
                .map { input in
                    guard !input.isEmpty else {
                        return "Email cannot be left empty"
                    }
                    guard input.isValidEmail() else {
                        return "Email is not valid"
                    }
            // in case the input is valid the error message is nil
                    return nil
                }
            // you publish the error message for the view to react to
                .assign(to: &$validationMessage)
        }
    }