[swift] textField를 감지하는 버튼 만들기


1️⃣ 목표

  • 문자가 있을 경우만 활성화되는 버튼을 구현할 예정입니다.

enabled button

  • 크게 delegate패턴, 이벤트감지 메서드 두가지 방법으로 나누어서 살펴보고,
  • 이벤트감지 메서드addTarget, addAction 두가지방법으로 구현해볼 예정입니다.
  • 두가지 방법 모두 다음과 같은 outlet변수를 만들어서 진행했습니다.

base code

  • button의 경우 프로퍼티 옵저버중 하나인 didSet을 이용하여 초기에 비활성화가 되도록 만들어 줬습니다.

2️⃣ delegate패턴으로 처리하기

  • swift에서는 여러가지 delegate를 지원하는데, textField에 관련된 delegate도 있습니다.
extension ViewController: UIViewController, UITextFieldDelegate {
	override func viewDidLoad() {
        super.viewDidLoad()
        self.textField.delegate = self
    }

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
             replacementString string: String) -> Bool { return Bool }
}
  • textField관련 delegate를 사용하기 위해서는 UITextFieldDelegate프로토콜을 채택해야 합니다.
  • 그중에서도 textField메서드를 사용했는데, 키를 입력하는 순간 동작하며 텍스트가 입력되기 직전에 작업을 처리해줍니다. 우리가 직접 키를감지하는 코드를 작성할 필요가 없고 UITextFieldDelegate가 대신 그 일들을 해주는데 이것이 delegate패턴을 이용하는 이유중 하나입니다.
self.textField.delegate = self
  • 위와 같이 textField아웃렛변수의 delegate를 self로 지정해주어 이 클래스에서 처리해주도록 합니다.
  • 이제 본 목적으로 돌아와 textField메서드를 이용하여 텍스트가 있을 때만 활성화되도록 만들 수 있는지 생각해 보겠습니다.

(1) backspace를 감지하여 처리하기 (안좋은 방법)

  • textField메서드의 파라미터중 textField는 현재 텍스트, string현재입력된 키값 하나를 가리킵니다.(키보드 버튼 하나씩 감지하므로 한글의 경우 자음,모음 단위로 감지)
  • 첫번째 방법으로 textField의 길이가 1인 상태에서 string값이 backspace이면 버튼이 비활성화 되는 방법을 생각했습니다.
  • swift에서 backspace를 찾아봤는데 다음의 스택오버글을 찾았습니다.

👉🏻👉🏻👉🏻 stackoverflow 참고글 (Detect backspace Event in UITextField)

func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
    if let char = string.cString(using: String.Encoding.utf8) {
        let isBackSpace = strcmp(char, "\\b")
        if (isBackSpace == -92) {
            print("Backspace was pressed")
        }
    }
    return true
}
  • 위의 코드가 스택오버플로우의 한 글에서 제시한 방법인데 하나하나 살펴보겠습니다.
  • 먼저, string변수를 utf8로 인코딩을 해주어 strcmp()메서드를 이용하여 \b문자와 비교해주고 있습니다. (swift에서 “\“는 “"문자를 가르킴)
  • utf8은 아스키코드가 나타낼 수 있는 범위내에서는 100%호환합니다.
  • 아래의 아스키코드표를 보면 \는 아스키코드 92입니다.

base code

  • strcmp()메서드의 경우 매개변수 2개의 값을 앞문자부터 비교하여 차이값을 출력해줍니다. (앞문자가 같으면 다음문자를 비교하고 모두 같을 경우 0을 출력)
  • 아스키코드에서 backspace의 값을 살펴보면 8입니다.
  • 그렇기 때문에 위에서 제시한 스택오버플로의 코드에서 isBackSpace의 값이 -84가 아닌 -92와 비교했을까요.
  • 다음의 코드로 실제로 textFieldDelegatetextField()메서드가 backspace를 어떻게 받아들이는지 확인해 봤습니다.
if let char = string.cString(using: String.Encoding.utf8) {
    let isBackSpace = strcmp(char, "\\b")
    print("string: ", string)
    print("char: ", char)
    print("isBackSpace: ", isBackSpace)
}
/* a 입력 */
string:  a
char:  [97, 0]
isBackSpace:  5

/* backspace 입력 */
string:
char:  [0]
isBackSpace:  -92
  • 위의 출력값을 보면 알듯이 textField()메서드는 backspace를 공백으로 감지했습니다. (char의 0은 문자의 끝을 나타내는 ‘\0’을 나타냄)
  • 그렇다면 굳이 \b와 비교할 필요가 있을까라는 생각이 들었습니다. 추가로 utf8로 변환할 필요없이 string값이 공백인지만 확인하면 될 것같습니다.
  • 이와 비슷한 생각을하는 다음의 스택오버플로우글을 찾았습니다.

👉🏻👉🏻👉🏻 스택오버플우 참고글 (Swift why strcmp of backspace returns -92?)

  • 위의 스택오버플로우에서도 \\b와 비교하는 알고리즘을 왜 쓰는지 잘 모르겠다고 하는데, 아마 가독성을 위한 것이 아닐까 생각이 듭니다.
  • 그렇다고 strint == ""이 꼭 backspace를 나타내는 것이 아니라고 말합니다. 실제로 입력값을 잘라내기하였을 때도 string값이 공백으로 출력됐습니다. 하지만 아스키코드도 동일하게 0이 출력됐기 때문에 잘라내기backspace를 비교할 방법이 없습니다.
  • 하지만 굳이 비교할 필요가 없는 것이 이번에 구현할 버튼같은 경우 굳이 backspace임을 확인할 필요없기 때문에 utf-8로 인코딩할 필요없이, 다음과 같이strint == ""임을 확인하는 코드로 작성해도 될 것 같습니다.
extension ViewController: UITextFieldDelegate {

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
            replacementString string: String) -> Bool {
        if string == "" && (textField.text?.count)! == 1{
            self.submitBtn.isEnabled = false
        } else {
            self.submitBtn.isEnabled = true
        }
        return true
    }
}
  • 하지만 문제가 생겼습니다. 다음의 움짤을 보면 알듯이 블록단위로 지우게 되면 비활성화가 되지 않습니다.

enabled button

(2) range 파라미터 이용하기 (좋은 방법)

  • textFieldDelegatetextField()메서드에는 range파라미터가 있습니다.
  • 다음과 같이 range의 값을 확인해봤습니다.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange,
        replacementString string: String) -> Bool {
    print("range: ", range)
    return true
}

enabled button

  • range의 타입은 다음과 같습니다.
typealias NSRange = _NSRange

public struct _NSRange {
  public var location: Int
  public var length: Int
}
  • 즉, range.locationrange.length값을 잘 사용하면 될 것 같습니다.

< delgate패턴이용한 최종 코드 >

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    if range.location == 0 && range.length != 0 {
        self.submitBtn.isEnabled = false
    } else {
        self.submitBtn.isEnabled = true
    }
    return true
}
  • 위아같이 range파라미터를 이용하면 블록단위로 지워도 잘 동작합니다. 또다른 버그가 있을 수 있지만 지금 선에서 적당한 해결방법인 것 같습니다.

3️⃣ 이벤트감지함수로 처리하기

(1)addTarget() 사용

  • 다음으로 delegate패턴을 사용하지않고 이벤트감지함수로 처리해보겠습니다.
class NoDelegateVC: UIViewController {
    /* 중략 */
	@IBOutlet weak var textField: UITextField!
    override func viewDidLoad() {
        super.viewDidLoad()
        self.textField.addTarget(self,
		    action: #selector(textFieldDidChange), for: .editingChanged)
    }

    @objc func textFieldDidChange(sender: UITextField) {
        if sender.text?.isEmpty == true {
            self.submitBtn.isEnabled = false
        } else {
            self.submitBtn.isEnabled = true
        }
    }
}
  • UITextField는 다음과 같이 UIControl를 상속하고 있습니다. 그렇기 때문에 각종 이벤트감지함수를 사용할 수 있습니다.

UITextField

  • 자바스크립트의 이벤트리스너함수와 비슷한 느낌의 함수 같습니다.
  • objective-c의 런타임 환경에서도 swift함수를 사용할 수 있게 하기 위해 @objc키워드를 붙여줍니다.
  • 그 중에서 .editingChangedUIControl.Event의 타입중 하나로 그중에서도 UITextField에서만 사용이 가능한 이벤트입니다. 위에서 delegate패턴에서 사용한 textField()메소드와 같이 문자단위로 이벤트를 감지하지만 이번에는 입력된 후에 처리합니다. 그렇기 때문에 생각보다 로직을 생각하기가 쉽습니다.

(2) addAction(_:for:) 사용 (iOS14 이상)

  • 다음의 스텍오버플로우글을 참고하면 iOS14이상부터는 addTarget()메서드의 기능을 하는 addAction()메서드를 사용할 수 있다고 합니다.

👉🏻👉🏻👉🏻 stackoverflow 참고글 (what is the difference between addAction and addTarget)

  • addAction(_:for:)을 사용해서 텍스트필드의 입력값에 반응하여 활성화 / 비활성화되는 버튼을 만들어 보겠습니다.
  • addTarget()메서드와 다른점은 클로저를 사용할 수 있습니다.
override func viewDidLoad() {
    /* 중략 */
    self.textField.addAction(UIAction(handler: { _ in
        if self.textField.text?.isEmpty == true {
            self.submitBtn.isEnabled = false
        } else {
            self.submitBtn.isEnabled = true
        }
    }), for: .editingChanged)
}
  • 아래와 같이 따로 핸들러함수를 만들어서 사용할 수도 있습니다. (클로져함수의 응용)
override func viewDidLoad() {
    /* 중략 */
    self.textField.addAction(UIAction(handler: self.textHandler), for: .editingChanged)
}

func textHandler(_ a: UIAction) -> Void {
    if self.textField.text?.isEmpty == true {
        self.submitBtn.isEnabled = false
    } else {
        self.submitBtn.isEnabled = true
    }
}
  • 이번에 텍스트필드의 입력값에 반응하여 활성화 / 비활성화되는 버튼을 구현해 보았습니다.
  • 제가 아는 지식선에서도 여러가지 방법이 있었습니다. 개인적인 생각으로는 delegate패턴이 가독성면에서 좀 더 좋다고 생각했습니다. 그 이유로 delegate패턴으로는 입력이전에 처리할 수 있기 때문에 텍스트관련 기능(유효성 검사등등..)도 같이 해줄 수 있는데, 기능적으로 통일성이 있을 것 같기 때문입니다.
  • 하지만 지금 구현한 코드는 매우 단순한 코드기 때문에 어떤 방법이 더 효율적인지 판단하기는 이른 것 같습니다.




© 2021.02. by kirim

Powered by kkrim