관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] UI 컴포넌트 - 검색창 TextField (SearchTextField) 본문

UI 컴포넌트 (swift)

[iOS - swift] UI 컴포넌트 - 검색창 TextField (SearchTextField)

jake-kim 2021. 7. 17. 01:17

UI 포인트

  • leftView, rightView
  • padding 설정
    • eftViewRect 설정, rightViewRect 설정 
    • textRect(bounds:): 입력중이 아닌 resignFirstResponder 상태일 경우의 입력된 text 위치
    • editingRect(bounds:): 입력중의 텍스트 위치
    • placeholderRect(bounds:): placeholder의 위치
  • delegate를 통해 특정 타이밍에 leftView, rightView 사라지게 하는 방법
    • textFieldDidBeginEditing(:): 포커스를 얻은 경우
    • textFieldDidEndEditing(:): 포커스를 잃은 경우
    • textField(:shouldChangeCharactersIn:replacementString:): 입력 중 인터렉션 처리 (메소드 개념 참고)

String trim 처리 기본기

  • trimmingCharacters(in:): 문자열 앞뒤 공백 제거
let a = " 123 456 \n"
print("before trimmingCharacters: \(a.count) ") // 10
print("after trimmingCharacters: \(a.trimmingCharacters(in: .whitespacesAndNewlines).count) ") // 7

BaseTextField 정의

class BaseTextField: UITextField {
    override init(frame: CGRect) {
        super.init(frame: frame)

        configure()
    }

    required init?(coder: NSCoder) {
        fatalError("not implement required init?(coder: NSCoder)")
    }

    convenience init(isSecureEntry: Bool = false,
                     keyboardType: UIKeyboardType,
                     returnKeyType: UIReturnKeyType = .done) {
        self.init(frame: .zero)

        self.isSecureTextEntry = isSecureEntry
        self.keyboardType = keyboardType
        self.returnKeyType = returnKeyType
        self.autocapitalizationType = .none
    }

    func configure() {}
    func bind() {}
}

SearchTextField  정의

  • SearchTextField 클래스 생성
class SearchTextField: BaseTextField {

}
  • leftView, rightView에 들어갈 button 정의
    private lazy var searchButton: UIButton = {
        let button = UIButton()
        button.setImage(#imageLiteral(resourceName: "btnSearchRight"), for: .normal)

        return button
    }()

    private lazy var clearButton: UIButton = {
        let button = UIButton()
        button.setImage(#imageLiteral(resourceName: "btnClearRight"), for: .normal)
        button.addTarget(self, action: #selector(didTapClearButton), for: .touchUpInside)

        return button
    }()
  • SearchTextField의 기본 UI 세팅
    • 주의할 점: clearButtonMode = .never로 설정해야 rightView에 view를 넣었을때 적용
    override func configure() {
        super.configure()

        delegate = self
        borderStyle = .none
        textColor = .label
        font = .systemFont(ofSize: 16.0, weight: .bold)
        attributedPlaceholder = NSAttributedString(string: "검색 창",
                                                   attributes: [NSAttributedString.Key.foregroundColor: UIColor.placeholderText])

        layer.borderWidth = 1.0
        layer.borderColor = UIColor.lightGray.cgColor
        layer.cornerRadius = 4.0

        clearButtonMode = .never

        leftView = searchButton
        leftViewMode = .always

        rightView = clearButton
        rightViewMode = .whileEditing
    }
  • 커스텀할때 용이한 left/rightViewMode 4가지
    • always: 항상 해당 view 표출
    • never
    • unlessEditing: 입력중이 아닌 경우에만 해당 view 표출 (focus를 받은 경우에도 표출 안되는 것 주의)
    • whileEditing: 입력중만 해당 view 표출

  • Clear button 탭 인터렉션
    • 입력값을 모두 지우고 rightView(클리어 버튼)을 안보이도록 설정
    // MARK: - Interaction

    @objc
    func didTapClearButton() {
        text?.removeAll()
        rightViewMode = .never
    }
  • leftView와 rightView 패딩 설정
    override func leftViewRect(forBounds bounds: CGRect) -> CGRect {
        var padding = super.leftViewRect(forBounds: bounds)
        padding.origin.x += 12

        return padding
    }

    override func rightViewRect(forBounds bounds: CGRect) -> CGRect {
        var padding = super.rightViewRect(forBounds: bounds)
        padding.origin.x -= 12

        return padding
    }
  • resign되었을 때의 text, 입력되는 text, placeholder의 text 패딩 설정
    // MARK: - 텍스트 패딩 설정

    // editing 모드가 아닌 경우의 inset 값
    override func textRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: UIEdgeInsets(top: 14.0, left: 38.0, bottom: 14.0, right: 0.0))
    }

    // editing 모드인 경우의 inset 값
    override func editingRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: UIEdgeInsets(top: 14.0, left: 38.0, bottom: 14.0, right: 36.0))
    }

    override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
        return bounds.inset(by: UIEdgeInsets(top: 16.0, left: 38.0, bottom: 14.0, right: 36.0))
    }
  • Delegate 설정
    • focus mode는 textFieldDidBeginEditing(:)에서 작성
    • resign mode는 textFieldDidEndEditing(:)에서 작성
    • 텍스트가 입력되는 상황에서는 textField(:shouldChangeCharactersIn:replacementString:)에서 작성
extension SearchTextField: UITextFieldDelegate {

    func textFieldDidBeginEditing(_ textField: UITextField) {
        /// Focus mode

        /// 입력된 값이 없는 경우만 leftView(검색 이미지), rightView(클리어 버튼)를 보이지 않도록 설정
        if text?.isEmpty ?? true {
            rightViewMode = .never
        }
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        /// Resign mode

        // leftViewMode, rightViewMode를 .never로 설정하여 사라지게끔 하는 작업 가능
    }

    func textField(_ textField: UITextField,
                   shouldChangeCharactersIn range: NSRange,
                   replacementString string: String) -> Bool {

        /// newText: 새로 입력된 텍스트
        let newText = string.trimmingCharacters(in: .whitespacesAndNewlines)

        /// text: 기존에 입력되었던 text
        /// predictRange: 입력으로 예상되는 text의 range값 추측 > range값을 알면 기존 문자열에 새로운 문자를 위치에 알맞게 추가 가능
        guard let text = textField.text, let predictRange = Range(range, in: text) else { return true }

        /// predictedText: 기존에 입력되었던 text에 새로 입력된 newText를 붙여서, 현재까지 입력된 전체 텍스트
        let predictedText = text.replacingCharacters(in: predictRange, with: newText)
            .trimmingCharacters(in: .whitespacesAndNewlines)

        if predictedText.isEmpty {
            rightViewMode = .never
        } else {
            rightViewMode = .whileEditing
        }

        return true
    }
}

*  source code: https://github.com/JK0369/SearchTextField

Comments