-
[TextField] placeholder Animaition재미있는 UI (Swift) 2024. 6. 6. 20:06
텍스트필드 사용하다 문제가 있어서 검색하던 중 재미있는 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() } } }
'재미있는 UI (Swift)' 카테고리의 다른 글
[Image Transition] 썸네일 -> 디테일 샷 (2) 2024.06.10