ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [TextField] placeholder Animaition
    재미있는 UI (Swift) 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()
            }
        }
    }

    '재미있는 UI (Swift)' 카테고리의 다른 글

    [Image Transition] 썸네일 -> 디테일 샷  (2) 2024.06.10
Designed by Tistory.