How to make material UITextField with floating label in iOS

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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
}
}

Comments