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

Customize errorMessage position #271

Merged
merged 13 commits into from
Jan 7, 2020
178 changes: 167 additions & 11 deletions Sources/SkyFloatingLabelTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@

import UIKit

public enum ErrorLabelPlacement {
case defaultPlacement
case bottomPlacement
}

/**
A beautiful and flexible textfield implementation with support for title label, error message and placeholder.
*/
Expand All @@ -30,9 +35,16 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
if isLTRLanguage {
textAlignment = .left
titleLabel.textAlignment = .left
errorLabel.textAlignment = .left
} else {
textAlignment = .right
titleLabel.textAlignment = .right
titleLabel.textAlignment = .right
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy paste error, you meant to paste in errorLabel here.

}

//Override error message default alignment
if let errorMessageAlignment = errorMessageAlignment {
errorLabel.textAlignment = errorMessageAlignment
}
}

Expand Down Expand Up @@ -72,6 +84,19 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
updatePlaceholder()
}
}

open var errorMessagePlacement: ErrorLabelPlacement = .defaultPlacement {
didSet {
updateControl()
updatePlaceholder()
}
}

open var errorMessageAlignment: NSTextAlignment? {
didSet {
updateTextAligment()
}
}

fileprivate func updatePlaceholder() {
guard let placeholder = placeholder, let font = placeholderFont ?? font else {
Expand Down Expand Up @@ -196,6 +221,7 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty

/// The internal `UILabel` that displays the selected, deselected title or error message based on the current state.
open var titleLabel: UILabel!
open var errorLabel: UILabel!

// MARK: Properties

Expand Down Expand Up @@ -329,6 +355,7 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
borderStyle = .none
createTitleLabel()
createLineView()
createErrorLabel()
updateColors()
addEditingChangedObserver()
updateTextAligment()
Expand Down Expand Up @@ -359,6 +386,17 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
self.titleLabel = titleLabel
}

fileprivate func createErrorLabel() {
let errorLabel = UILabel()
errorLabel.autoresizingMask = [.flexibleWidth, .flexibleHeight]
errorLabel.font = titleFont
errorLabel.alpha = 0.0
errorLabel.textColor = errorColor

addSubview(errorLabel)
self.errorLabel = errorLabel
}

fileprivate func createLineView() {

if lineView == nil {
Expand Down Expand Up @@ -457,7 +495,7 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty

if !isEnabled {
titleLabel.textColor = disabledColor
} else if hasErrorMessage {
} else if hasErrorMessage && errorMessagePlacement == .defaultPlacement {
titleLabel.textColor = titleErrorColor ?? errorColor
} else {
if editingOrSelected || isHighlighted {
Expand Down Expand Up @@ -486,9 +524,27 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
}

var titleText: String?
if hasErrorMessage {
titleText = titleFormatter(errorMessage!)
} else {
var errorText: String?

if errorMessagePlacement == .defaultPlacement {
if hasErrorMessage {
titleText = titleFormatter(errorMessage!)
} else {
if editingOrSelected {
titleText = selectedTitleOrTitlePlaceholder()
if titleText == nil {
titleText = titleOrPlaceholder()
}
} else {
titleText = titleOrPlaceholder()
}
}
}
else {
if hasErrorMessage {
errorText = titleFormatter(errorMessage!)
}

if editingOrSelected {
titleText = selectedTitleOrTitlePlaceholder()
if titleText == nil {
Expand All @@ -498,10 +554,15 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
titleText = titleOrPlaceholder()
}
}

titleLabel.text = titleText
titleLabel.font = titleFont

errorLabel.text = errorText
errorLabel.font = titleFont

updateTitleVisibility(animated)
updateErrorVisibility(animated)
}

fileprivate var _titleVisible: Bool = false
Expand All @@ -527,7 +588,16 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
- returns: True if the title is displayed on the control, false otherwise.
*/
open func isTitleVisible() -> Bool {
return hasText || hasErrorMessage || _titleVisible
if errorMessagePlacement == .defaultPlacement {
return hasText || hasErrorMessage || _titleVisible
}
else {
return hasText || _titleVisible
}
}

open func isErrorVisible() -> Bool {
return hasErrorMessage
}

fileprivate func updateTitleVisibility(_ animated: Bool = false, completion: ((_ completed: Bool) -> Void)? = nil) {
Expand All @@ -553,6 +623,29 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
}
}

fileprivate func updateErrorVisibility(_ animated: Bool = false, completion: ((_ completed: Bool) -> Void)? = nil) {
let alpha: CGFloat = isErrorVisible() ? 1.0 : 0.0
let frame: CGRect = errorLabelRectForBounds(bounds, editing: isErrorVisible())
let updateBlock = { () -> Void in
self.errorLabel.alpha = alpha
self.errorLabel.frame = frame
}
if animated {
#if swift(>=4.2)
let animationOptions: UIView.AnimationOptions = .curveEaseOut
#else
let animationOptions: UIViewAnimationOptions = .curveEaseOut
#endif
let duration = isErrorVisible() ? titleFadeInDuration : titleFadeOutDuration
UIView.animate(withDuration: duration, delay: 0, options: animationOptions, animations: { () -> Void in
updateBlock()
}, completion: completion)
} else {
updateBlock()
completion?(true)
}
}

// MARK: - UITextField text/placeholder positioning overrides

/**
Expand All @@ -563,12 +656,18 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
override open func textRect(forBounds bounds: CGRect) -> CGRect {
let superRect = super.textRect(forBounds: bounds)
let titleHeight = self.titleHeight()


var height = superRect.size.height - titleHeight - selectedLineHeight

if errorMessagePlacement == .bottomPlacement {
height = height - errorHeight()
}

let rect = CGRect(
x: superRect.origin.x,
y: titleHeight,
width: superRect.size.width,
height: superRect.size.height - titleHeight - selectedLineHeight
height: height
)
return rect
}
Expand All @@ -582,11 +681,17 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
let superRect = super.editingRect(forBounds: bounds)
let titleHeight = self.titleHeight()

var height = superRect.size.height - titleHeight - selectedLineHeight

if errorMessagePlacement == .bottomPlacement {
height = height - errorHeight()
}

let rect = CGRect(
x: superRect.origin.x,
y: titleHeight,
width: superRect.size.width,
height: superRect.size.height - titleHeight - selectedLineHeight
height: height
)
return rect
}
Expand All @@ -597,11 +702,17 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
- returns: The rectangle that the placeholder should render in
*/
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
var height = bounds.size.height - titleHeight() - selectedLineHeight

if errorMessagePlacement == .bottomPlacement {
height = height - errorHeight()
}

let rect = CGRect(
x: 0,
y: titleHeight(),
width: bounds.size.width,
height: bounds.size.height - titleHeight() - selectedLineHeight
height: height
)
return rect
}
Expand All @@ -621,6 +732,27 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
return CGRect(x: 0, y: titleHeight(), width: bounds.size.width, height: titleHeight())
}

/**
Calculate the bounds for the error label. Override to create a custom size error field.
- parameter bounds: The current bounds of the title
- parameter editing: True if the control is selected or highlighted
- returns: The rectangle that the title label should render in
*/
open func errorLabelRectForBounds(_ bounds: CGRect, editing: Bool) -> CGRect {
if errorMessagePlacement == .defaultPlacement {
return CGRect.zero
}
else {
let lineRect = lineViewRectForBounds(bounds, editing: editing)

if editing {
return CGRect(x: 0, y: lineRect.origin.y + selectedLineHeight, width: bounds.size.width, height: errorHeight())
}

return CGRect(x: 0, y: lineRect.origin.y + selectedLineHeight + errorHeight(), width: bounds.size.width, height: errorHeight())
}
}

/**
Calculate the bounds for the bottom line of the control.
Override to create a custom size bottom line in the textbox.
Expand All @@ -630,7 +762,13 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
*/
open func lineViewRectForBounds(_ bounds: CGRect, editing: Bool) -> CGRect {
let height = editing ? selectedLineHeight : lineHeight
return CGRect(x: 0, y: bounds.size.height - height, width: bounds.size.width, height: height)

if errorMessagePlacement == .bottomPlacement {
return CGRect(x: 0, y: textRect(forBounds: bounds).maxY, width: bounds.size.width, height: height)
}
else {
return CGRect(x: 0, y: bounds.size.height - height, width: bounds.size.width, height: height)
}
}

/**
Expand All @@ -645,6 +783,18 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
return 15.0
}

/**
Calculate the height of the error label.
-returns: the calculated height of the error label. Override to size the error with a different height
*/
open func errorHeight() -> CGFloat {
if let errorLabel = errorLabel,
let font = errorLabel.font {
return font.lineHeight
}
return 15.0
}

/**
Calcualte the height of the textfield.
-returns: the calculated height of the textfield. Override to size the textfield with a different height
Expand Down Expand Up @@ -678,6 +828,7 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
super.layoutSubviews()

titleLabel.frame = titleLabelRectForBounds(bounds, editing: isTitleVisible() || _renderingInInterfaceBuilder)
errorLabel.frame = errorLabelRectForBounds(bounds, editing: isErrorVisible() || _renderingInInterfaceBuilder)
lineView.frame = lineViewRectForBounds(bounds, editing: editingOrSelected || _renderingInInterfaceBuilder)
}

Expand All @@ -687,7 +838,12 @@ open class SkyFloatingLabelTextField: UITextField { // swiftlint:disable:this ty
- returns: the content size to be used for auto layout
*/
override open var intrinsicContentSize: CGSize {
return CGSize(width: bounds.size.width, height: titleHeight() + textHeight())
if errorMessagePlacement == .bottomPlacement {
return CGSize(width: bounds.size.width, height: titleHeight() + textHeight() + errorHeight())
}
else {
return CGSize(width: bounds.size.width, height: titleHeight() + textHeight())
}
}

// MARK: - Helpers
Expand Down