Issue #325

  • Use UILabel as placeholder and move it
  • When label is moved up, scale it down 80%. It means it has 10% padding on the left and right when shrinked, so offsetX for translation is 10%
  • Translation transform should happen before scale
  • Ideally we can animate font and color change using CATextLayer, but with UILabel we can use UIView.transition
final class MaterialInputView: UIView {
    lazy var label: UILabel = {
        return UILabel()
    }()

    lazy var textField: UITextField = {
        let textField = UITextField()
        textField.tintColor = R.color.primary
        textField.textColor = R.color.lightText
        textField.font = R.customFont.medium(16)
        textField.autocapitalizationType = .none
        textField.autocorrectionType = .no

        return textField
    }()

    lazy var line: UIView = {
        let line = UIView()
        line.backgroundColor = R.color.primary
        return line
    }()

    // Whether label should be moved to top
    private var isUp: Bool = false {
        didSet {
            styleLabel(isUp: isUp)
            moveLabel(isUp: isUp)
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)

        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    private func setup() {
        addSubviews([textField, label, line])
        textField.delegate = self

        NSLayoutConstraint.on([
            textField.leftAnchor.constraint(equalTo: leftAnchor, constant: 16),
            textField.rightAnchor.constraint(equalTo: rightAnchor, constant: -16),
            textField.topAnchor.constraint(equalTo: topAnchor, constant: 16),

            label.leftAnchor.constraint(equalTo: textField.leftAnchor),
            label.centerYAnchor.constraint(equalTo: textField.centerYAnchor),

            line.leftAnchor.constraint(equalTo: textField.leftAnchor),
            line.rightAnchor.constraint(equalTo: textField.rightAnchor),
            line.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 8),
            line.heightAnchor.constraint(equalToConstant: 2)
        ])

        styleLabel(isUp: false)
    }

    private func styleLabel(isUp: Bool) {
        UIView.transition(
            with: label,
            duration: 0.15,
            options: .curveEaseInOut,
            animations: {
                if isUp {
                    self.label.font = R.customFont.regular(12)
                    self.label.textColor = R.color.primary
                } else {
                    self.label.font = R.customFont.medium(16)
                    self.label.textColor = R.color.grayText
                }
            },
            completion: nil
        )
    }

    private func moveLabel(isUp: Bool) {
        UIView.animate(
            withDuration: 0.15,
            delay: 0,
            options: .curveEaseInOut,
            animations: {
                if isUp {
                    let offsetX = self.label.frame.width * 0.1
                    let translation = CGAffineTransform(translationX: -offsetX, y: -24)
                    let scale = CGAffineTransform(scaleX: 0.8, y: 0.8)
                    self.label.transform = translation.concatenating(scale)
                } else {
                    self.label.transform = .identity
                }
            },
            completion: nil
        )
    }
}

extension MaterialInputView: UITextFieldDelegate {
    func textFieldDidBeginEditing(_ textField: UITextField) {
        if !isUp {
            isUp = true
        }
    }

    func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
        guard let text = textField.text else {
            return false
        }

        if isUp && text.isEmpty {
            isUp = false
        }
        return true
    }
}