재미있는 UI (Swift)

[TextField] placeholder Animaition

skyotter84 2024. 6. 6. 20:06

 

 

 

텍스트필드 사용하다 문제가 있어서 검색하던 중 재미있는 UI를 발견했다.

 

원 글은 아래 스택오버플로 질답이다.

(https://stackoverflow.com/questions/50773786/how-to-add-a-label-to-textfield-class-or-animate-placeholder)

 

 

 

간단히 포인트를 짚고 가자면

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()
        }
    }
}