Skyscanner / SkyFloatingLabelTextField

A beautiful and flexible text field control implementation of "Float Label Pattern". Written in Swift.
Apache License 2.0
4.09k stars 542 forks source link

Localization #136

Open Maryom opened 7 years ago

Maryom commented 7 years ago

Hey,

Thanks for your awesome repo. I need to localize the fields to Arabic language, as you can see in the attached image, the fields turned to the right perfectly. However, the placeholders didn't change although I wrote: "YGo-es-Lf5.placeholder" = "كلمة المرور"; in Main.strings(Arabic) file.

screen shot 2017-05-11 at 12 08 23 pm

Any help?

Thanks.

pichirichi commented 7 years ago

I don't see such problem on my App in RTL languages. img_1137 Do you have a sample code that reproduce this problem?

Maryom commented 7 years ago

Thanks for your reply, I set placeholder values in storyboard, Do you think that I have to set them programmatically ?

pichirichi commented 7 years ago

I don't think it will make a different. Can you provide a sample code?

Maryom commented 7 years ago

Hey,

I set all the attributes in storyboard as you can see in the below image:

screen shot 2017-05-14 at 9 52 05 am

Then, I connected it: @IBOutlet weak var loginEmailInputView: SkyFloatingLabelTextField!

After that I wrote: "BcR-hT-edQ.placeholder" = "يرجى إدخال بريدك الإلكتروني."; in Main.strings(Arabic) file.

Any help?

Thank you so much.

bogren commented 7 years ago

When is "BcR-hT-edQ.placeholder" = "يرجى إدخال بريدك الإلكتروني."; set? You have placeholder set the Storyboard and then set it again in code? Remove the line in your screenshot.

Maryom commented 7 years ago

@bogren Thanks for your reply. You have placeholder set the Storyboard and then set it again in code? I just set it in Storyboard for English language. Then I wrote this 👇🏼 line in Main.strings(Arabic) file to localize it: "BcR-hT-edQ.placeholder" = "يرجى إدخال بريدك الإلكتروني."; Still it doesn't work :( Any help?

bogren commented 7 years ago

Ah now I understand your setup and your problem. Can you provide sample code?

Maryom commented 7 years ago

@bogren I set all the attributes in storyboard. Then, I connected it: @IBOutlet weak var loginEmailInputView: SkyFloatingLabelTextField!

After that I wrote: "BcR-hT-edQ.placeholder" = "يرجى إدخال بريدك الإلكتروني."; in Main.strings(Arabic) file.

bogren commented 7 years ago

Does it work for other languages? Is other labels and test translated? If you create a sample project and upload here with the same setup I can take a look at the code.

Maryom commented 7 years ago

Does it work for other languages? I just need Arabic and English.

Is other labels and test translated? Yes all localization work perfectly, except SkyFloatingLabelTextField :(

bogren commented 7 years ago

hmm.. 🤔 I'll try to setup a demo project.

Maryom commented 7 years ago

@bogren Thank you so much 🙏🏼

k0nserv commented 7 years ago

@Maryom How are you getting on?

Maryom commented 7 years ago

@k0nserv Thanks. I got busy with other projects and I didn't check it again. but all the needed information are provided above, what extra information you need?

k0nserv commented 7 years ago

Can you reproduce it in a small example project?

Maryom commented 7 years ago

I will try ASAP. Thank you.

neatree commented 6 years ago

I had a same problem. when you open the attributes inspector, you can see two placeholders. don't input the first one, use the second Placeholder on the Text Field section. It simply worked for me.

Maryom commented 6 years ago

@neatree Thank you 🙏🏼

ebeem commented 6 years ago

I can confirm that this issue exists, and @neatree 's solution works perfectly

hendksoliman commented 5 years ago

Hello there , I have the same problem . when the app started and i choose Arabic language to be the app language, the skyfloating textfield still exists on the left not on the right. but i noticed that when i close the app and open it again everything works well at its place. Any Help please ? Thanks in advance

k0nserv commented 5 years ago

I think we should start setting textAlignment to NSTextAlignmentNatural everywhere and get rid of the custom RTL stuff. Since iOS 9.0 this should be the solution. I can look at this, but if anyone wants to PR it I'd be happy

Hassan-iOS commented 4 years ago

Hey,

Thanks for your awesome repo. I need to localize the fields to Arabic language, as you can see in the attached image, the fields turned to the right perfectly. However, the placeholders didn't change although I wrote: "YGo-es-Lf5.placeholder" = "كلمة المرور"; in Main.strings(Arabic) file.

screen shot 2017-05-11 at 12 08 23 pm

Any help?

Thanks.

import UIKit import MOLH

@IBDesignable open class CustomTextField: UITextField { // swiftlint:disable:this type_body_length /* A Boolean value that determines if the language displayed is LTR. Default value set automatically from the application language settings. / @objc open var isLTRLanguage: Bool = UIApplication.shared.userInterfaceLayoutDirection == .leftToRight { didSet { updateTextAligment() } }

func isEnglishApp() -> Bool {
    if MOLHLanguage.isRTLLanguage() { return false } else { return true }}

fileprivate func updateTextAligment() {
    if isEnglishApp() {
        self.textAlignment = .left
        self.titleLabel.textAlignment = .left
    } else {
        self.textAlignment = .right
        self.titleLabel.textAlignment = .right
    }
}

// MARK: Animation timing

/// The value of the title appearing duration
@objc dynamic open var titleFadeInDuration: TimeInterval = 0.2
/// The value of the title disappearing duration
@objc dynamic open var titleFadeOutDuration: TimeInterval = 0.3

// MARK: Colors

fileprivate var cachedTextColor: UIColor?

/// A UIColor value that determines the text color of the editable text
@IBInspectable
override dynamic open var textColor: UIColor? {
    set {
        cachedTextColor = newValue
        updateControl(false)
    }
    get {
        return cachedTextColor
    }
}

/// A UIColor value that determines text color of the placeholder label
@IBInspectable dynamic open var placeholderColor: UIColor = UIColor.lightGray {
    didSet {
        updatePlaceholder()
    }
}

/// A UIFont value that determines text color of the placeholder label
@objc dynamic open var placeholderFont: UIFont? {
    didSet {
        updatePlaceholder()
    }
}

fileprivate func updatePlaceholder() {
    guard let placeholder = placeholder, let font = placeholderFont ?? font else {
        return
    }
    let color = isEnabled ? placeholderColor : disabledColor
    #if swift(>=4.2)
    attributedPlaceholder = NSAttributedString(
        string: placeholder,
        attributes: [
            NSAttributedString.Key.foregroundColor: color, NSAttributedString.Key.font: font
        ]
    )
    #elseif swift(>=4.0)
    attributedPlaceholder = NSAttributedString(
        string: placeholder,
        attributes: [
            NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.font: font
        ]
    )
    #else
    attributedPlaceholder = NSAttributedString(
        string: placeholder,
        attributes: [NSForegroundColorAttributeName: color, NSFontAttributeName: font]
    )
    #endif
}

/// A UIFont value that determines the text font of the title label
@objc dynamic open var titleFont: UIFont = .systemFont(ofSize: 13) {
    didSet {
        updateTitleLabel()
    }
}

/// A UIColor value that determines the text color of the title label when in the normal state
@IBInspectable dynamic open var titleColor: UIColor = .gray {
    didSet {
        updateTitleColor()
    }
}

/// A UIColor value that determines the color of the bottom line when in the normal state
@IBInspectable dynamic open var lineColor: UIColor = .lightGray {
    didSet {
        updateLineView()
    }
}

/// A UIColor value that determines the color used for the title label and line when the error message is not `nil`
@IBInspectable dynamic open var errorColor: UIColor = .red {
    didSet {
        updateColors()
    }
}

/// A UIColor value that determines the color used for the line when error message is not `nil`
@IBInspectable dynamic open var lineErrorColor: UIColor? {
    didSet {
        updateColors()
    }
}

/// A UIColor value that determines the color used for the text when error message is not `nil`
@IBInspectable dynamic open var textErrorColor: UIColor? {
    didSet {
        updateColors()
    }
}

/// A UIColor value that determines the color used for the title label when error message is not `nil`
@IBInspectable dynamic open var titleErrorColor: UIColor? {
    didSet {
        updateColors()
    }
}

/// A UIColor value that determines the color used for the title label and line when text field is disabled
@IBInspectable dynamic open var disabledColor: UIColor = UIColor(white: 0.88, alpha: 1.0) {
    didSet {
        updateControl()
        updatePlaceholder()
    }
}

/// A UIColor value that determines the text color of the title label when editing
@IBInspectable dynamic open var selectedTitleColor: UIColor = .blue {
    didSet {
        updateTitleColor()
    }
}

/// A UIColor value that determines the color of the line in a selected state
@IBInspectable dynamic open var selectedLineColor: UIColor = .black {
    didSet {
        updateLineView()
    }
}

// MARK: Line height

/// A CGFloat value that determines the height for the bottom line when the control is in the normal state
@IBInspectable dynamic open var lineHeight: CGFloat = 0.5 {
    didSet {
        updateLineView()
        setNeedsDisplay()
    }
}

/// A CGFloat value that determines the height for the bottom line when the control is in a selected state
@IBInspectable dynamic open var selectedLineHeight: CGFloat = 1.0 {
    didSet {
        updateLineView()
        setNeedsDisplay()
    }
}

// MARK: View components

/// The internal `UIView` to display the line below the text input.
open var lineView: UIView!

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

// MARK: Properties

/**
 The formatter used before displaying content in the title label.
 This can be the `title`, `selectedTitle` or the `errorMessage`.
 The default implementation converts the text to uppercase.
 */
open var titleFormatter: ((String) -> String) = { (text: String) -> String in
    if #available(iOS 9.0, *) {
        return text
    } else {
        return text
    }
}

/**
 Identifies whether the text object should hide the text being entered.
 */
override open var isSecureTextEntry: Bool {
    set {
        super.isSecureTextEntry = newValue
        fixCaretPosition()
    }
    get {
        return super.isSecureTextEntry
    }
}

/// A String value for the error message to display.
@IBInspectable
open var errorMessage: String? {
    didSet {
        updateControl(true)
    }
}

/// The backing property for the highlighted property
fileprivate var _highlighted: Bool = false

/**
 A Boolean value that determines whether the receiver is highlighted.
 When changing this value, highlighting will be done with animation
 */
override open var isHighlighted: Bool {
    get {
        return _highlighted
    }
    set {
        _highlighted = newValue
        updateTitleColor()
        updateLineView()
    }
}

/// A Boolean value that determines whether the textfield is being edited or is selected.
open var editingOrSelected: Bool {
    return super.isEditing || isSelected
}

/// A Boolean value that determines whether the receiver has an error message.
open var hasErrorMessage: Bool {
    return errorMessage != nil && errorMessage != ""
}

fileprivate var _renderingInInterfaceBuilder: Bool = false

/// The text content of the textfield
@IBInspectable
override open var text: String? {
    didSet {
        updateControl(false)
    }
}

/**
 The String to display when the input field is empty.
 The placeholder can also appear in the title label when both `title` `selectedTitle` and are `nil`.
 */
@IBInspectable
override open var placeholder: String? {
    didSet {
        setNeedsDisplay()
        updatePlaceholder()
        updateTitleLabel()
    }
}

/// The String to display when the textfield is editing and the input is not empty.
@IBInspectable open var selectedTitle: String? {
    didSet {
        updateControl()
    }
}

/// The String to display when the textfield is not editing and the input is not empty.
@IBInspectable open var title: String? {
    didSet {
        updateControl()
    }
}

// Determines whether the field is selected. When selected, the title floats above the textbox.
open override var isSelected: Bool {
    didSet {
        updateControl(true)
    }
}

// MARK: - Initializers

/**
 Initializes the control
 - parameter frame the frame of the control
 */
override public init(frame: CGRect) {
    super.init(frame: frame)
    init_SkyFloatingLabelTextField()
}

/**
 Intialzies the control by deserializing it
 - parameter aDecoder the object to deserialize the control from
 */
required public init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    init_SkyFloatingLabelTextField()
}

fileprivate final func init_SkyFloatingLabelTextField() {
    borderStyle = .none
    createTitleLabel()
    createLineView()
    updateColors()
    addEditingChangedObserver()
    updateTextAligment()
}

fileprivate func addEditingChangedObserver() {
    self.addTarget(self, action: #selector(HassanTxtField.editingChanged), for: .editingChanged)
}

/**
 Invoked when the editing state of the textfield changes. Override to respond to this change.
 */
@objc open func editingChanged() {
    updateControl(true)
    updateTitleLabel(true)
}

// MARK: create components

fileprivate func createTitleLabel() {
    let titleLabel = UILabel()
    titleLabel.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    titleLabel.font = titleFont
    titleLabel.alpha = 0.0
    titleLabel.textColor = titleColor

    addSubview(titleLabel)
    self.titleLabel = titleLabel
}

fileprivate func createLineView() {

    if lineView == nil {
        let lineView = UIView()
        lineView.isUserInteractionEnabled = false
        self.lineView = lineView
        configureDefaultLineHeight()
    }

    lineView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin]
    addSubview(lineView)
}

fileprivate func configureDefaultLineHeight() {
    let onePixel: CGFloat = 1.0 / UIScreen.main.scale
    lineHeight = 2.0 * onePixel
    selectedLineHeight = 2.0 * self.lineHeight
}

// MARK: Responder handling

/**
 Attempt the control to become the first responder
 - returns: True when successfull becoming the first responder
 */
@discardableResult
override open func becomeFirstResponder() -> Bool {
    let result = super.becomeFirstResponder()
    updateControl(true)
    return result
}

/**
 Attempt the control to resign being the first responder
 - returns: True when successfull resigning being the first responder
 */
@discardableResult
override open func resignFirstResponder() -> Bool {
    let result = super.resignFirstResponder()
    updateControl(true)
    return result
}

/// update colors when is enabled changed
override open var isEnabled: Bool {
    didSet {
        updateControl()
        updatePlaceholder()
    }
}

// MARK: - View updates

fileprivate func updateControl(_ animated: Bool = false) {
    updateColors()
    updateLineView()
    updateTitleLabel(animated)
}

fileprivate func updateLineView() {
    guard let lineView = lineView else {
        return
    }

    lineView.frame = lineViewRectForBounds(bounds, editing: editingOrSelected)
    updateLineColor()
}

// MARK: - Color updates

/// Update the colors for the control. Override to customize colors.
open func updateColors() {
    updateLineColor()
    updateTitleColor()
    updateTextColor()
}

fileprivate func updateLineColor() {
    guard let lineView = lineView else {
        return
    }

    if !isEnabled {
        lineView.backgroundColor = disabledColor
    } else if hasErrorMessage {
        lineView.backgroundColor = lineErrorColor ?? errorColor
    } else {
        lineView.backgroundColor = editingOrSelected ? selectedLineColor : lineColor
    }
}

fileprivate func updateTitleColor() {
    guard let titleLabel = titleLabel else {
        return
    }

    if !isEnabled {
        titleLabel.textColor = disabledColor
    } else if hasErrorMessage {
        titleLabel.textColor = titleErrorColor ?? errorColor
    } else {
        if editingOrSelected || isHighlighted {
            titleLabel.textColor = selectedTitleColor
        } else {
            titleLabel.textColor = titleColor
        }
    }
}

fileprivate func updateTextColor() {
    if !isEnabled {
        super.textColor = disabledColor
    } else if hasErrorMessage {
        super.textColor = textErrorColor ?? errorColor
    } else {
        super.textColor = cachedTextColor
    }
}

// MARK: - Title handling

fileprivate func updateTitleLabel(_ animated: Bool = false) {
    guard let titleLabel = titleLabel else {
        return
    }

    var titleText: String?
    if hasErrorMessage {
        titleText = titleFormatter(errorMessage!)
    } else {
        if editingOrSelected {
            titleText = selectedTitleOrTitlePlaceholder()
            if titleText == nil {
                titleText = titleOrPlaceholder()
            }
        } else {
            titleText = titleOrPlaceholder()
        }
    }
    titleLabel.text = titleText
    titleLabel.font = titleFont

    updateTitleVisibility(animated)
}

fileprivate var _titleVisible: Bool = false

/*
 *   Set this value to make the title visible
 */
open func setTitleVisible(
    _ titleVisible: Bool,
    animated: Bool = false,
    animationCompletion: ((_ completed: Bool) -> Void)? = nil
    ) {
    if _titleVisible == titleVisible {
        return
    }
    _titleVisible = titleVisible
    updateTitleColor()
    updateTitleVisibility(animated, completion: animationCompletion)
}

/**
 Returns whether the title is being displayed on the control.
 - returns: True if the title is displayed on the control, false otherwise.
 */
open func isTitleVisible() -> Bool {
    return hasText || hasErrorMessage || _titleVisible
}

fileprivate func updateTitleVisibility(_ animated: Bool = false, completion: ((_ completed: Bool) -> Void)? = nil) {
    let alpha: CGFloat = isTitleVisible() ? 1.0 : 0.0
    let frame: CGRect = titleLabelRectForBounds(bounds, editing: isTitleVisible())
    let updateBlock = { () -> Void in
        self.titleLabel.alpha = alpha
        self.titleLabel.frame = frame
    }
    if animated {
        #if swift(>=4.2)
        let animationOptions: UIView.AnimationOptions = .curveEaseOut
        #else
        let animationOptions: UIViewAnimationOptions = .curveEaseOut
        #endif
        let duration = isTitleVisible() ? 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

/**
 Calculate the rectangle for the textfield when it is not being edited
 - parameter bounds: The current bounds of the field
 - returns: The rectangle that the textfield should render in
 */
override open func textRect(forBounds bounds: CGRect) -> CGRect {
    let superRect = super.textRect(forBounds: bounds)
    let titleHeight = self.titleHeight()

    let rect = CGRect(
        x: superRect.origin.x,
        y: titleHeight,
        width: superRect.size.width,
        height: superRect.size.height - titleHeight - selectedLineHeight
    )
    return rect
}

/**
 Calculate the rectangle for the textfield when it is being edited
 - parameter bounds: The current bounds of the field
 - returns: The rectangle that the textfield should render in
 */
override open func editingRect(forBounds bounds: CGRect) -> CGRect {
    let superRect = super.editingRect(forBounds: bounds)
    let titleHeight = self.titleHeight()

    let rect = CGRect(
        x: superRect.origin.x,
        y: titleHeight,
        width: superRect.size.width,
        height: superRect.size.height - titleHeight - selectedLineHeight
    )
    return rect
}

/**
 Calculate the rectangle for the placeholder
 - parameter bounds: The current bounds of the placeholder
 - returns: The rectangle that the placeholder should render in
 */
override open func placeholderRect(forBounds bounds: CGRect) -> CGRect {
    let rect = CGRect(
        x: 0,
        y: titleHeight(),
        width: bounds.size.width,
        height: bounds.size.height - titleHeight() - selectedLineHeight
    )
    return rect
}

// MARK: - Positioning Overrides

/**
 Calculate the bounds for the title label. Override to create a custom size title 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 titleLabelRectForBounds(_ bounds: CGRect, editing: Bool) -> CGRect {
    if editing {
        return CGRect(x: 0, y: 0, width: bounds.size.width, height: titleHeight())
    }
    return CGRect(x: 0, y: titleHeight(), width: bounds.size.width, height: titleHeight())
}

/**
 Calculate the bounds for the bottom line of the control.
 Override to create a custom size bottom line in the textbox.
 - parameter bounds: The current bounds of the line
 - parameter editing: True if the control is selected or highlighted
 - returns: The rectangle that the line bar should render in
 */
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)
}

/**
 Calculate the height of the title label.
 -returns: the calculated height of the title label. Override to size the title with a different height
 */
open func titleHeight() -> CGFloat {
    if let titleLabel = titleLabel,
        let font = titleLabel.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
 */
open func textHeight() -> CGFloat {
    guard let font = self.font else {
        return 0.0
    }

    return font.lineHeight + 7.0
}

// MARK: - Layout

/// Invoked when the interface builder renders the control
override open func prepareForInterfaceBuilder() {
    if #available(iOS 8.0, *) {
        super.prepareForInterfaceBuilder()
    }

    borderStyle = .none

    isSelected = true
    _renderingInInterfaceBuilder = true
    updateControl(false)
    invalidateIntrinsicContentSize()
}

/// Invoked by layoutIfNeeded automatically
override open func layoutSubviews() {
    super.layoutSubviews()

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

/**
 Calculate the content size for auto layout

 - returns: the content size to be used for auto layout
 */
override open var intrinsicContentSize: CGSize {
    return CGSize(width: bounds.size.width, height: titleHeight() + textHeight())
}

// MARK: - Helpers

fileprivate func titleOrPlaceholder() -> String? {
    guard let title = title ?? placeholder else {
        return nil
    }
    return titleFormatter(title)
}

fileprivate func selectedTitleOrTitlePlaceholder() -> String? {
    guard let title = selectedTitle ?? title ?? placeholder else {
        return nil
    }
    return titleFormatter(title)
}

} // swiftlint:disable:this file_length

extension UITextField { /// Moves the caret to the correct position by removing the trailing whitespace func fixCaretPosition() { // Moving the caret to the correct position by removing the trailing whitespace // http://stackoverflow.com/questions/14220187/uitextfield-has-trailing-whitespace-after-securetextentry-toggle

    let beginning = beginningOfDocument
    selectedTextRange = textRange(from: beginning, to: beginning)
    let end = endOfDocument
    selectedTextRange = textRange(from: end, to: end)
}

}