Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ErrorTextField validation #1082

Merged
merged 5 commits into from
Jun 6, 2018
Merged

Added ErrorTextField validation #1082

merged 5 commits into from
Jun 6, 2018

Conversation

OrkhanAlikhanov
Copy link
Contributor

@OrkhanAlikhanov OrkhanAlikhanov commented May 31, 2018

First change is that, new errorLabel field is added to ErrorTextField. It will be shown in place of detailLabel when isErrorRevealed is set to true. Obviously, it's added to let botherrorLabel and detailLabel have their own style and text.

Secondly, ErrorTextFieldValidator is added which allows to set various validation criteria and to check if textField.text passes them or not. On failure errorLabel will be shown with corresponding error message set alongside the validation criterion. Basic setup is like so:

let usernameField = ErrorTextField()
usernameField.placeholder = "Username"
usernameField.detail = "You can use letters, numbers & periods"
usernameField.validator
  .notEmpty(msg: "Choose username")
  .min(length: 3, msg: "Min 3 characters")
  .noWhitespaces(msg: "Username cannot contain spaces")
  .username(msg: "Unallowed characters in username")

Validation will be checked on the sequence that's provided, so order is important.

Calling usernameField.isValid() will return boolean value indicating if validation passed. On failure corresponding error message will be shown. If developer needs to know validation status without showing show error message on fail, he can pass true to deferred parameter. usernameField.isValid(deferred: true). Note that when a validation fails others (in the sequence) will not be checked.

Custom validation closure can also be set to if provided ones does not cover developer's criteria

usernameField.validator.validate(msg: "Letter 'A' is not allowed") { text in
  return text.contains("A") == false // must not contain 'A'
}

By default auto validation is delayed until first error message appears. After first validation failure, validation will be rechecked on every change in textField:

Auto-validation is turned off until validation fails first time, which happens when isValid() is called on Sign Up button is tapped
However, we have option turn auto validation on without waiting first error to occur.

usernameField.autoValidateOnlyIfErrorIsShownOnce = false //now the field will be auto-validated as user types

Auto-validation runs as user types

Turning auto validation off:

usernameField.autoValidationType = .none // turned off
usernameField.autoValidationType = .default // turned on

Setting custom auto validation which will be called on every textField change:

usernameField.autoValidationType = .custom { textField in
  _ = textField.isValid() // this is default behavior, simply reruns validation 
}

import UIKit
import Motion

open class ErrorTextFieldValidator: NSObject {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually there is no need to subclass NSObject

@OrkhanAlikhanov
Copy link
Contributor Author

Demo project to play: demo.zip

@OrkhanAlikhanov
Copy link
Contributor Author

Somewhat related to #1041 because of implementation changes.

@daniel-jonathan
Copy link
Member

Adding #1017.

Copy link
Member

@daniel-jonathan daniel-jonathan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!
Here are a few comments for us to discuss.

layoutSubviews()
}
}

open var isErrorRevealed: Bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should rename this to isErrorMessageRevealed. This will allow the isError... prefix to be used and helpful when searching for flags through Xcode's code completion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually isErrorRevealed name was in ErrorTextField even before my changes. I kept the name same. Also note that errorLabel.text has convenience field var error: String? just like detailLabel.text can be changed by var detail: String?. I mean that field is error not errorMessage. Should I still rename to isErrorMessageRevealed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is fine. It can stay. I thought we were introducing a new piece and so the format should be discussed in general. A style guide needs to be developed from the current code base, and changes we would like to make for future releases.

Sources/iOS/ErrorTextField.swift Show resolved Hide resolved
//
// Created by Orkhan Alikhanov on 30/05/2018.
// Copyright © 2018 CosmicMind, Inc. All rights reserved.
//
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header file comment needs to have the Material license added, and the default header comment removed. You can add your name as the originator at the top in the license if you like. Everything else would need to be the same. For example:

/*
 * Copyright (C) 2018, Daniel Dahan and CosmicMind, Inc. <http://cosmicmind.com>.
 * All rights reserved.
 *
 * Original Inspiration & Author
 * Copyright (c) 2018 Orkhan Alikhanov <[email protected]>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  *  Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 *
 *  *  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  *  Neither the name of CosmicMind nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

self.textField = textField
super.init()

textField.addTarget(self, action: #selector(checkIfErrorHasGone), for: .editingChanged)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's always move all preparation code to their own prepare function called in an instance prepare function. Material follows this setup pretty much everywhere. Mainly, it is to give clear insight into code changes. For example, if I comment out a prepare function, I know that all prepare logic is contained. Where if we scatter the logic, it could lead to undesirable behavior.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, should I call removeTarget somewhere, like in deinit? I couldn't find answer. Note that textField is weak.

Copy link
Member

@daniel-jonathan daniel-jonathan Jun 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No you don't need to call that. The reference to target is not long lived, so you don't need to detach from it.

case .custom(let closure):
closure(textField)
case .default:
_ = textField.isValid() // will hide if needed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add @discardableResult to isValid we can remove the _ = .


open func isValid(deferred: Bool) -> Bool {
guard let textField = textField else { return false }
if !deferred { textField.isErrorRevealed = false }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's change deferred to isDeferred. The is... prefix is helpful when Xcode's auto completion pops up, as it will display all boolean or similar is... flags, which is an easy way to investigate an API.


open class ErrorTextFieldValidator: NSObject {
public typealias ValidationClosure = (String) -> Bool
open var closures: [(code: ValidationClosure, msg: String)] = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shortening variables is okay in function bodies, but for function definitions, let's use full variable names. For example: msg: String. It is more helpful when searching API definitions and it is more clear to the reader.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean it should be like this?

(code: ValidationClosure, message: String)

I actually changed my mind, I think all msg parameters in the function signatures should be in full form. What do you say?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that is correct, and agreed :)

@OrkhanAlikhanov
Copy link
Contributor Author

@DanielDahan Regarding #1041, does setting isErrorRevealed have to fire UIAccessibilityAnnouncementNotification? Can it have some side effects?

We can add option something like shouldPostAccesibilityNotificationWhenErrorRevealed: Bool, or maybe add a method like setErrorReveal(_ isRevealed: Bool, postAccessibilityNotification: Bool) to control posting that notification. Or we can simply delay it until someone asks for.

@OrkhanAlikhanov
Copy link
Contributor Author

@DanielDahan we can merge autoValidateOnlyIfErrorIsShownOnce to autoValidationType by introducing new case there. What do you think?

@daniel-jonathan
Copy link
Member

@OrkhanAlikhanov We will need to add comments to the function definitions and any class/instance variables.

@daniel-jonathan
Copy link
Member

@OrkhanAlikhanov for the auto validation, you mean making an enum of different types? Yeah, I think that it would be a better approach.

@daniel-jonathan
Copy link
Member

@OrkhanAlikhanov I think for UIAccessibilityAnnouncementNotification, I think we need to look at accessibility as a separate issue. Come up with an entire solution for the framework. Let's leave it out for now and make a note that we need it here.

@OrkhanAlikhanov
Copy link
Contributor Author

Latest demo project. demo.zip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

2 participants