[TextField] placeholder Animaition
텍스트필드 사용하다 문제가 있어서 검색하던 중 재미있는 UI를 발견했다.
원 글은 아래 스택오버플로 질답이다.
간단히 포인트를 짚고 가자면
1. UITextField와 UILabel를 UIView에 담기
(만약 UILabel을 텍스트 필드 안에서만 움직일 거라면 UIView 없이 UILabel을 UITextField에 넣어도 될 것 같다.)
2. UITextfiledDelegate를 이용해 커서가 작동하는 시점, 끝나는 시점에 이벤트 발생시키기
3. CGAffineTransForm으로 스케일 애니메이션 주기
1. 반복 사용하도록 TextFieldWithAnimaition이라는 클래스를 만들었고, placeholder 텍스트를 받도록 했다.
레이아웃까지 잡고 나면 이 정도가 기본코드이다.
class TextFiledWithAnimaition: UIView {
let placeholder: String?
lazy var label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var textField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.borderStyle = .roundedRect
textField.backgroundColor = .blue.withAlphaComponent(0.2)
return textField
}()
init(placeholder: String, frame: CGRect) {
self.placeholder = placeholder
super.init(frame: frame)
configureView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureView() {
label.text = placeholder ?? ""
[label, textField].forEach {
self.addSubview($0)
}
NSLayoutConstraint.activate([
textField.centerXAnchor.constraint(equalTo: self.centerXAnchor),
textField.centerYAnchor.constraint(equalTo: self.centerYAnchor),
textField.widthAnchor.constraint(equalTo: self.widthAnchor),
label.centerYAnchor.constraint(equalTo: textField.centerYAnchor, constant: 0),
label.leadingAnchor.constraint(equalTo: textField.leadingAnchor, constant: spacing)
])
}
}
1-1. 뷰컨에 이를 사용해 이름 / 패스워드 칸을 잡아주는 코드까지 보자
class ViewController: UIViewController {
lazy var tfUserName = TextFiledWithAnimaition(placeholder: "이름", frame: .zero)
lazy var tfPassword = TextFiledWithAnimaition(placeholder: "패스워드", frame: .zero)
override func viewDidLoad() {
super.viewDidLoad()
configureView()
}
func configureView() {
self.view.backgroundColor = .white
[tfUserName, tfPassword].forEach {
self.view.addSubview($0)
$0.translatesAutoresizingMaskIntoConstraints = false
}
NSLayoutConstraint.activate([
tfUserName.bottomAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -5),
tfUserName.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 50),
tfUserName.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -50),
tfUserName.heightAnchor.constraint(equalToConstant: 60),
tfPassword.topAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 5),
tfPassword.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 50),
tfPassword.trailingAnchor.constraint(equalTo: self.view.trailingAnchor, constant: -50),
tfPassword.heightAnchor.constraint(equalToConstant: 60)
])
}
}
2. 이제 TextField의 이벤트를 받기 위해 TextFiledWithAnimaition에 extenstion 작업을 해보면
이렇게 3가지만 사용할 것이다. 아, 델리게이트 주는 거 잊지 말자.
class TextFiledWithAnimaition: UIView {
func configureView() {
// 요 한 줄 추가
textField.delegate = self
🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻
🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻
🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻
}
}
extension TextFiledWithAnimaition: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
// 키보드에서 리턴 눌렀을 때 이벤트 -> 텍스트필드 리스폰더 해제
}
func textFieldDidBeginEditing(_ textField: UITextField) {
// 텍스트 필드를 선택했을 때 이벤트 -> UILabel의 텍스의 위치 조정 및 사이즈 조정
}
func textFieldDidEndEditing(_ textField: UITextField) {
// 텍스트 필드에서 커서가 사라질 때 이벤트 -> UILabel의 텍스의 위치 및 사이즈 원래대로
}
}
2-2. 작업내용
UILabel의 오토레이아웃을 변경시켜야 하는데, 이번 예제를 통해 팁을 하나 배웠다.
변경할 오토레이아웃은 프라퍼티로 관리하면 된다는 것을...
위 예제와 같은 경우는 변경할 레이아웃이 2개, UILabel의 y위치와 leading인데
y 위치를 바꾸는 건 당연하게 보이는데 leading은 뭔가 싶었다.
이유는, CGAffineTransform으로 스케일을 조정하면, 그 개체의 center를 고정으로 사이즈가 변한다.
아래서 언급하겠지만 이게 글자 수에도 영향을 미치는 듯 하다.
class TextFiledWithAnimaition: UIView {
private var labelYAnchorConstraint: NSLayoutConstraint!
private var labelLeadingAnchor: NSLayoutConstraint!
let placeholder: String?
🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻
🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻
🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻🏊🏻
func configureView() {
label.text = placeholder ?? ""
[label, textField].forEach {
self.addSubview($0)
}
labelYAnchorConstraint = label.centerYAnchor.constraint(equalTo: textField.centerYAnchor,
constant: 0)
labelLeadingAnchor = label.leadingAnchor.constraint(equalTo: textField.leadingAnchor,
constant: spacing)
NSLayoutConstraint.activate([
textField.centerXAnchor.constraint(equalTo: self.centerXAnchor),
textField.centerYAnchor.constraint(equalTo: self.centerYAnchor),
textField.widthAnchor.constraint(equalTo: self.widthAnchor),
textField.heightAnchor.constraint(equalToConstant: 60),
label.widthAnchor.constraint(equalToConstant: 100),
labelYAnchorConstraint,
labelLeadingAnchor
])
}
3. 이제 어떻게 조정을 할 것이냐
조정하려는 것이 무엇인지 명확히만 하면 간단한 코드다.
extension TextFiledWithAnimaition: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return textField.resignFirstResponder()
}
func textFieldDidBeginEditing(_ textField: UITextField) {
labelYAnchorConstraint.constant = -25
labelLeadingAnchor.constant = -9
performAnimation(transform: CGAffineTransform(scaleX: 0.7, y: 0.7))
}
func textFieldDidEndEditing(_ textField: UITextField) {
if let text = textField.text, text == "" {
labelYAnchorConstraint.constant = 0
labelLeadingAnchor.constant = spacing
performAnimation(transform: CGAffineTransform(scaleX: 1, y: 1))
}
}
private func performAnimation(transform: CGAffineTransform) {
UIView.animate(withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1,
options: .curveEaseOut) {
self.label.transform = transform
self.layoutIfNeeded()
}
}
}
# TroubleSome
클래스를 만들어 똑같이 사용한 텍스트필드인데 줄어든 라벨의 leading 위치가 다르다.
뭐 하나 쉽게 되는 게 읎다.
순서를 바꿔봐도 그대로인 걸 보니 아마 글자 수가 문제이지 않을까 해서 봤더니 빙고.
아무리 오토레이웃으로 리딩을 잡아줘도 CGAffineTransform의 스케일 조정이 가운데를 고정으로 조절되는 게 문제가 되는 거 같아서
또 구글님에게 CGAffineTransform의 기준을 왼쪽으로 잡을 방법을 하사해주십사 했지만
그러다 문득 UILabel의 크기를 똑같이 고정시켜 가면 되지 않을까 해서 시도했더니 또 빙고.
class TextFieldWithAnimation: UIView {
func configureView() {
label.text = placeholder ?? ""
[label, textField].forEach {
self.addSubview($0)
}
labelYAnchorConstraint = label.centerYAnchor.constraint(equalTo: textField.centerYAnchor,
constant: 0)
labelLeadingAnchor = label.leadingAnchor.constraint(equalTo: textField.leadingAnchor,
constant: spacing)
NSLayoutConstraint.activate([
textField.centerXAnchor.constraint(equalTo: self.centerXAnchor),
textField.centerYAnchor.constraint(equalTo: self.centerYAnchor),
textField.widthAnchor.constraint(equalTo: self.widthAnchor),
textField.heightAnchor.constraint(equalToConstant: 60),
// 요 한 줄 추가
label.widthAnchor.constraint(equalToConstant: 100),
labelYAnchorConstraint,
labelLeadingAnchor
])
}
}
전체코드도 참고 얍.
import UIKit
class TextFiledWithAnimaition: UIView {
private var labelYAnchorConstraint: NSLayoutConstraint!
private var labelLeadingAnchor: NSLayoutConstraint!
let placeholder: String?
var spacing: CGFloat = 5
lazy var label: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
lazy var textField: UITextField = {
let textField = UITextField()
textField.translatesAutoresizingMaskIntoConstraints = false
textField.borderStyle = .roundedRect
textField.backgroundColor = .blue.withAlphaComponent(0.2)
return textField
}()
init(placeholder: String, frame: CGRect) {
self.placeholder = placeholder
super.init(frame: frame)
configureView()
textField.delegate = self
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configureView() {
label.text = placeholder ?? ""
[label, textField].forEach {
self.addSubview($0)
}
labelYAnchorConstraint = label.centerYAnchor.constraint(equalTo: textField.centerYAnchor,
constant: 0)
labelLeadingAnchor = label.leadingAnchor.constraint(equalTo: textField.leadingAnchor,
constant: spacing)
NSLayoutConstraint.activate([
textField.centerXAnchor.constraint(equalTo: self.centerXAnchor),
textField.centerYAnchor.constraint(equalTo: self.centerYAnchor),
textField.widthAnchor.constraint(equalTo: self.widthAnchor),
textField.heightAnchor.constraint(equalToConstant: 60),
label.widthAnchor.constraint(equalToConstant: 100),
labelYAnchorConstraint,
labelLeadingAnchor
])
}
}
extension TextFiledWithAnimaition: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
return textField.resignFirstResponder()
}
func textFieldDidBeginEditing(_ textField: UITextField) {
labelYAnchorConstraint.constant = -25
labelLeadingAnchor.constant = -9
performAnimation(transform: CGAffineTransform(scaleX: 0.7, y: 0.7))
}
func textFieldDidEndEditing(_ textField: UITextField) {
if let text = textField.text, text == "" {
labelYAnchorConstraint.constant = 0
labelLeadingAnchor.constant = spacing
performAnimation(transform: CGAffineTransform(scaleX: 1, y: 1))
}
}
private func performAnimation(transform: CGAffineTransform) {
UIView.animate(withDuration: 0.3,
delay: 0,
usingSpringWithDamping: 1,
initialSpringVelocity: 1,
options: .curveEaseOut) {
self.label.transform = transform
self.layoutIfNeeded()
}
}
}