🍎 IOS/UIKit

[Swift/UIKIt] TextField 유효성 검사 구현

콩리또 2022. 9. 18. 15:43

안녕하세요 콩리또 입니다.🌯

Swift 왕초보로써 제가 코딩하면서 어려웠던 점을 포스팅하고 있는데요

오늘은 TextField 유효성 검사 기능을 구현한 부분에 대해 말씀드리겠습니다!

 

모든 포스팅 말투는 편한 말투로 작성합니다!

피드백은 댓글로 부탁드려요!

감사합니다🥰

 


🤔 배경 

TextField 입력값에 실시간으로 반응해주는 유효성검사

TableVIew에 행을 추가할때, 중복되는 이름은 추가할 수 없도록 동일한 텍스트에 대한 유효성 검사를 구현하고 싶었음

근데 따로 버튼을 누르지않고, 중복 텍스트가 입력됐을 때 바로 TextField 밑에 해당 문구가 보이도록 만들고 싶어

인터넷을 뒤지던 중, 요 블로그(https://velog.io/@sso0022/iOS-TextField-글자수-제한하기-한글 )를 보고

'오호 NotificationCenter를 사용하면 되겠군..!' 하고 사용해봄

가 보 자 고 ~

 


 🤔 1차 오류성 검사 제작 과정

AddNewCategory 뷰컨

카테고리명을 입력하는 ViewController의 viewDidLoad()에 → NotificationCenter addObserver 선언

  • selector : 실행할 @objc 함수를 지정
  • name : 감시하고 싶은 UI 타입 및 어떤 감시법?을 쓸건지 지정
  • object : 내가 감시하고싶은 텍스트필드를 지정
override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
        render()
        
        // 텍스트필드 입력갑을 옵저버하는 부분
        NotificationCenter.default.addObserver(self, selector: #selector(checkTextValidation(_:)), name: UITextField.textDidChangeNotification, object: categoryTextField)
    }

② @objc 함수에 유효성 로직 구현

  1. 함수 파라미터에 (_ notification: Notification)라고 작성
  2. ①에 정의한 object를 해당 타입으로 설정
  3. contains는 배열의 값을 검색해줌 (값 ⭕️ : true / 값 ❌ : false를 반환)
//텍스트 필드를 감시해서 동일 문자열 입력 시 유효성검사를 띄우려고함..
    @objc private func checkTextValidation(_ notification: Notification) {

        if let textField = notification.object as? UITextField {
            if let text = textField.text {

                //categories의 1번 인덱스의 categoryName과 텍스트필드 입력값이 일치하면 유효성검사
								//1번 인텍스 categoryName = "아이돌"
                if categories[1].categoryName.contains(text) == true {

                    validationLabel.text = "Category name already exists."
                    validationLabel.textColor = .red

                } else {

                    validationLabel.text = "Good name😎"
                    validationLabel.textColor = .blue

                }
            }
        }
    }

😨 1차 결과화면

오 쉣!!!!!!!!!😱😱😱😱😱

array에 .contains를 주니 한 글자씩 찾아줌..😭

나는 “아이돌” 단어일때 유효성검사 문구를 띄우고 싶었는데

결과 화면을 보면 “아” / “아이” / “아이돌” → 이 3가지 상황에 반응을 함;;;;;;;

찾아보니 array에 .contains를 주면 해당하는 한글자 한글자씩 다 돌아서 시간도 오래걸리고 효율이 좋지않다고함..

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


🤓 중간 리서치

개복치 개발 초보는 안되면 절망에 빠짐..

'다 포기하고 그냥 버튼을 눌렀을때 유효성 검사를 하게 할까..?'라고 고민 하던 중

도서관에서 Swift 쌉고수 켐선생님을 만나 조언을 구함

 


 

🐶  켐선생님의 개꿀 조언 🍯

  • contains는 한글자씩 다 찾아줌 → array에서 값을 통째로 찾아주는 다른 메소드 찾기
  • categories > 전체 Category > CategoryName를 잡으려면 .map를 사용하기
// 이렇게 하면 카테고리의 모든 카테고리네임을 잡을 수 있다고함
categories.map { $0.categoryName }

 

이걸 바탕으로 구글링을 하니

  • 일단 array에서 값을 통째로 찾아주는 다른 메소드는 없는 것 같음..ㅠㅠ🥲
  • Set에서 contains를 사용하면 문자열을 하나씩 찾아주지 않고 통째로 찾아준다고 함
    • 그리고 Array에서 contains를 사용했을때보다 시간효율성이 매우 높다고 함
  • Map을 이용하면 데이터 변형 할 수 있고 더 가독성 높아짐

아무튼 이 정보를 가지고 2차 코딩 들어감

 

참고 블로그

Set : https://stackoverflow.com/questions/34161786/reduce-array-to-set-in-swift

Map : https://shark-sea.kr/entry/Swift-고차함수-Map-Filter-Reduce-알아보기

 


🤔 2차 오류성 검사 제작 과정

① 전체 CategoryName를 Set으로 선언

// contains에서 전체 문자열을 통째로 찾기 위해 array를 Set로 선언
let nameSet = Set(categories.map {$0.categoryName})

② 유효성 검사 로직 수정

그리고 그 Set에 contains를 사용해 값을 찾고 중복되면 이미 있다는 문구를 내보냄

@objc private func checkTextValidation(_ notification: Notification) {
        if let textField = notification.object as? UITextField {
            if let text = textField.text {
                
                //categories의 categoryName을 set으로 모아놓은 배열을 만듦
                let nameSet = Set(categories.map {$0.categoryName})

                //위에서 선언한 Set에 contains(text)를 줘서 중복되면 유효성검사 및 SAVE 버튼 비활성화
                if nameSet.contains(text) == true {
                    validationLabel.text = "Category name already exists 🥲"
                    validationLabel.textColor = .red
										navigationItem.rightBarButtonItem?.isEnabled = false

                    
                } else if text.isEmpty {
                    validationLabel.text = ""
                    
                } else {
                    validationLabel.text = "Good name 😎"
                    validationLabel.textColor = .blue
                }
            }
            
        }
    }

2차 결과화면!!!

VERY GOOD!!🥰🥰🥰🥰🥰

array에서 set로 바꾸니 한글자씩 인식하던게 통째로 인식됨!!!!!!

 

오늘의 결론

모르는게 있으면 포기하지말고 개발자를 찾아가자!

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


🖥 전체 코드

혹시 상세한 코드가 궁금하실 수 있어서 전체코드 올려놓겠습니다!

더보기

 

import UIKit

class AddNewCategory: UIViewController {
    
    // MARK: - Properties
    lazy var categoryTextField: UITextField = {
        let categoryTextField = UITextField()
        categoryTextField.placeholder = "Enter up to 10 characters"
        categoryTextField.delegate = self
        categoryTextField.clearButtonMode = .whileEditing
        categoryTextField.borderStyle = .none
        return categoryTextField
    }()
    
    lazy var validationLabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 16, weight: .regular)
        return label
    }()
    
    var categoryNameDelegate: CategoryNameProtocol?
    
    // 더미데이터 입니다. 데이터가 들어오면 삭제할 예정입니다.
    let word1 = KWord(name: "비티짱1", isFavorite: true, isOriginal: true, description: "바티바티", relatedWords: ["바티짱2", "바티짱3"])
    let word2 = KWord(name: "비티짱2", isFavorite: false, isOriginal: true, description: "짱", relatedWords: ["바티짱1", "바티짱3"])
    let word3 = KWord(name: "비티짱3", isFavorite: true, isOriginal: true, description: "ㅋㅋㅋㅋ", usages: [Usage(korean: "지난 주 뮤직쇼 봤어?", english: "music show you see?"), Usage(korean: "지난 치티치티치티?", english: "티clclslsl?")], relatedWords: ["바티장1", "바티짱2"])
    let word4 = KWord(name: "아오나1", isFavorite: true, isOriginal: false)
    let word5 = KWord(name: "zz", isFavorite: false, isOriginal: true, description: "zzzz")
    let word6 = KWord(name: "gg1", isFavorite: false, isOriginal: false, description: "zz")
    lazy var categories: [Category] = [Category(categoryName: "💜BTS💜", count: "8 Words", kwords: [word1, word2, word3]),Category(categoryName: "아이돌", count: "8 Words", kwords: [word4, word5]), Category(categoryName: "소녀시대", count: "8 Words", kwords: [word6])]
    
    // MARK: - Lifecycle
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        underLine()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        configureUI()
        render()
        
        // 텍스트필드 입력갑을 옵저버하는 부분
        NotificationCenter.default.addObserver(self, selector: #selector(checkTextValidation(_:)), name: UITextField.textDidChangeNotification, object: categoryTextField)
    }
    
    private func configureUI() {
        view.backgroundColor = UIColor.white
        self.navigationItem.title = "Category Name"
        self.navigationController?.navigationBar.prefersLargeTitles = true
        self.navigationItem.rightBarButtonItem =
        UIBarButtonItem.init(title: "Save", style: .done, target: self, action: #selector(addNewCategory(_:)))
        navigationItem.leftBarButtonItem = UIBarButtonItem.init(title: "Cancel", style: .done, target: self, action: #selector(dismissModal(_:)))
    }
    
    private func render() {
        view.addSubview(categoryTextField)
        categoryTextField.anchor(top: view.safeAreaLayoutGuide.topAnchor, left: view.safeAreaLayoutGuide.leftAnchor, right: view.safeAreaLayoutGuide.rightAnchor, paddingTop: 20, paddingLeft: 16, paddingRight: 16)
        
        view.addSubview(validationLabel)
        validationLabel.anchor(top: categoryTextField.bottomAnchor, left: view.safeAreaLayoutGuide.leftAnchor, right: view.safeAreaLayoutGuide.rightAnchor, paddingTop: 20, paddingLeft: 16, paddingRight: 16)
    }
    
    func underLine() {
        let border = CALayer()
        border.frame = CGRect(x: 0, y: categoryTextField.frame.size.height+10, width: categoryTextField.frame.width, height: 1)
        border.borderWidth = 1
        border.backgroundColor = UIColor.gray.cgColor
        categoryTextField.layer.addSublayer(border)
    }
    
    @objc private func checkTextValidation(_ notification: Notification) {
        if let textField = notification.object as? UITextField {
            if let text = textField.text {
                
                //categories의 categoryName을 set으로 모아놓은 배열을 만듦
                let nameSet = Set(categories.map {$0.categoryName})

                //위에서 선언한 Set에 contains(text)를 줘서 중복되면 유효성검사를 반환하게 만듦
                if nameSet.contains(text) == true {
                    validationLabel.text = "Category name already exists 🥲"
                    validationLabel.textColor = .red
										navigationItem.rightBarButtonItem?.isEnabled = false
                    
                } else if text.isEmpty {
                    validationLabel.text = ""
                    
                } else {
                    validationLabel.text = "Good name 😎"
                    validationLabel.textColor = .blue
                }
            }
            
        }
    }
    
    @objc private func addNewCategory(_ sender: Any) {
        if let text = categoryTextField.text {
            categoryNameDelegate?.categoryNameSend(name: text)
        }
        dismiss(animated: true, completion: nil)
    }
    
    @objc private func dismissModal(_ sender: Any) {
        dismiss(animated: true, completion: nil)
    }
}

extension AddNewCategory: UITextFieldDelegate {
    
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        if let char = string.cString(using: String.Encoding.utf8) {
            let isBackSpace = strcmp(char, "\\b")
            if isBackSpace == -92 {
                return true
            }
        }
        
        guard categoryTextField.text!.count < 10 else { return false }
        return true
    }
}

protocol CategoryNameProtocol {
    func categoryNameSend(name: String)
}