Roen의 iOS 개발로그

UITextField에 입력도, 붙여넣기도 막아버리기

by Steady On

iOS

서론

UITextField는 사용자에게서 입력을 받을 때 많이 사용합니다. inputView 메서드를 통해서 UIPicker와 결합해서 쓰는 경우도 많고요. 이럴 때 우리가 입력 받아야 하는 건 "숫자만!", "8자 까지만!" 등등 입력을 제한 해야하는 경우도 많죠? 또 뜬금없는 데이터를 붙여넣기 해버리지 않도록 메뉴도 좀 비활성화 해주면 좋을 것 같아요. 오늘은 UITextField의 기능을 좀 제한 시켜 보겠습니다.

 

먼저 '붙여넣기'부터 막아보자

붙여넣기는 크게 두가지 방법으로 할 수 있는데요.

붙여넣기 메뉴

1. UITextField를 탭했을 때 나오는 UIMenu에서 "붙여넣기"버튼 누르기

2. 외장 키보드를 활용해서 Command + V 단축기로 붙여넣기

굳이 한가지를 더 추가하자면, 애플 제공 기본 키보드UI가 아닌 서드파티 키보드 앱에서 제공하는 클립보드 정도가 있겠지만, 2번과 크게 다르지 않습니다. 그럼 순서대로 1번인 저 UIMenu에서 붙여넣기부터 없애봅시다. 

그리고 미리 말하지만, 붙여넣기 옆에 나오는 TextScan 버튼은 뺄 수 없습니다(?) 제가 못찾은 걸수도 있지만, 어떻게해도 저건 안빠졌어요;;;

 

메뉴... 그거 띄우지마

우리는 붙여넣기를 막을거지만, 따져보면 이게 메뉴에서 "붙여넣기"가 안나오게 만들어야 하는 거거든요? 그럼 이 메뉴! 붙여넣기! 이 녀석들은 어디 소속일까요? 결론부터 말하자면, 이건 UIResponder클래스의 canPerformAction(_:withSender:)인스턴스 메서드를 통해 조절할 수 있는데요. UITextField를 하고 있는데 뜬금 UIResponder가 왜나와요...?

형이 왜 거기서 나와...?

라고 하실 수도 있으니까 UIResponder까지 거슬러 올라가 봅시다.

✔️ UIResponder -> UIView -> UIControl -> UITextField

UIComponent의 상속관계를 따져보면, UITextField는 위와 같이 UIControl, UIView를 지나 UIResponder까지 쭈우욱 상속을 받고 있습니다. 그러니까 UIResponder는 UITextField의 증조할아버지다~ 그런 말이죠. 그러니까 이 증조 할아버지는 어떤 분이신지 우리가 또 알아야 되겠잖아여?

 

UIResponder

이벤트에 응답하고 처리하기 위한 추상 인터페이스

Overview
UIResponder의 인스턴스인 Responder 객체는 UIKit 앱의 이벤트 처리 백본을 구성한다. UIApplication 객체, UIViewController 객체 및 모든 UIView 객체(UIWindow 포함)를 비롯한 많은 주요 객체는 Responder이기도 하다. 이벤트가 발생하면 UIKit은 이벤트를 처리하기 위해 앱의 Responder 객체로 디스패치(발송)한다.

touch events, motion events, remote-control events, and press events 등 여러 종류의 이벤트가 있고, 특정 유형의 이벤트를 처리하려면, Responder가 해당 메서드를 override 해야 한다. 예를 들어 touch events를 처리하기 위해서 Responder는 touchesBegan(_:with:), touchesMoved(_:with:), touchesEnded(_:with:), 그리고 touchesCancelled(_:with:) 메서드를 구현한다. 터치의 경우 응답자는 UIKit에서 제공하는 이벤트 정보를 사용하여 해당 터치의 변경 사항을 추적하고 앱의 인터페이스를 적절하게 업데이트 한다.

이벤트 처리 외에도 UIKit Responder는 처리되지 않은 이벤트를 앱의 다른 부분으로 전달하는 것도 관리한다. 지정된 Responder가 이벤트를 처리하지 않으면 해당 이벤트를 Responder 체인의 다음 이벤트로 전달한다. UIKit은 어떤 객체가 이벤트를 수신해야 하는지 결정하기 위해 미리 정의된 규칙을 사용하여 Responder 체인을 동적으로 관리한다. 예를 들어 view는 이벤트를 super view로 전달하고 계층 구조의 Root view는 이벤트를 view controller로 전달한다.

Responder는 UIEvent 객체를 처리하지만 input view를 통해 사용자 지정 input을 받을 수도 있다. 시스템의 키보드는 입력 보기의 가장 확실한 예이다. 사용자가 화면에서 UITextField 및 UITextView 객체를 탭하면 보기가 첫 번째 responder가 되고 시스템 키보드인 input view를 표시한다. 마찬가지로 custom input view를 만들고 다른 responder가 활성화될 때 표시할 수 있다. custom input view를 responder와 연결하려면 해당 view를 responder의 inputView 속성에 할당하면 된다.

 

즉, UIResponder는 요약하자면!

1. UIKit 앱의 모든 이벤트를 처리하고 응답한다.

2. inputView 프로퍼티를 통해서 기본적으로 설정된 것 외의 커스텀 처리도 가능하다.

로 볼 수 있다.

 

예를 들어, UITextField를 중심으로 보면, textFieldShouldBeginEditing(\_:) 같이 텍스트 필드 편집 시작이나 입력값이 바뀔 때 라던지 delegate를 통해서 메서드를 override하면 Responder에서 처리해 준다.

UITextFieldDelegate를 통해서 재정의할 수 있는 event들

그리고 UITextField의 기본 inputView는 키보드지만 여기다가 UIPicker를 할당해주면, UIPicker가 나타나는 식으로 커스텀처리도 가능하다는 것!

UIPicker를 inputView에 할당한 예시 화면


그럼 이제 본론으로 넘어가서 이 UIResponder에서 Menu를 비활성화 하는 canPerformAction(_:withSender:) 메서드에 대해 알아봅시다.

 

canPerformAction(_:withSender:)

 

canPerformAction

사용자 인터페이스에서 지정된 명령을 활성화 또는 비활성화하도록 수신 응답자에게 요청한다.

Parameter
- action: command와 관련된 메서드를 식별하는 selector. 편집 메뉴의 경우 UIResponderStandardEditActions 약식 프로토콜(예: copy:)에서 선언한 편집 메서드 중 하나이다.
- sender: 이 메서드를 호출하는 객체. 편집 메뉴 command의 경우 이것은 공유된 UIApplication 객체다. 컨텍스트에 따라 명령을 활성화해야 하는지 여부를 결정하는 데 도움이 되는 정보를 sender로 쿼리할 수 있다.

Return Value
action으로 식별된 명령이 활성화되어야 하는 경우 true이고 비활성화되어야 하는 경우 false.

Discussion
이 메서드의 기본 구현은 responder 클래스가 요청된 action을 구현하는 경우 true를 반환하고 그렇지 않은 경우 다음 responder를 호출한다. 서브클래스는 이 메서드를 override하여 현재 상태에 따라 메뉴 command를 활성화 할 수 있다. 예를 들어, selection이 있으면 Copy command를 활성화 하거나 pasteboard(clipboard나 복사되어져 있는 것)가 올바른 pasteboard 표시 타입의 데이터를 담고 있지 않으면 Paste command를 비활성화 할 수 있다. 만약 클래스가 command에서 false를 반환하더라도 responder 체인에서 상위에 있는 또 다른 responder가 true를 반환하여 command를 활성화 할 수 있다는 것에 주의해야 한다.

이 메서드는 동일한 action에 대해 다른 sender와 함께 한 번 이상 호출될 수 있다. nil을 포함한 모든 종류의 sender에 대비해야 한다.

canPerformAction 메서드는 UIResponder의 인스턴스 메서드 중 하나입니다. UI Component에 따라 이미 기본적으로 설정되어 있는 명령들을 비활성화 하도록 override를 통해서 responder에 알려줄 수 있어요. 파라미터는 그닥 어려울게 없죠? sender는 스토리보드에서 IBOutlet 연결할때 많이 보던 그거 맞습니다! 어느 컴포넌트에서 오는거냐! 하는거에요. UITextField로 sender를 다운캐스팅하면 text도 볼 수 있고 그렇겠죠? action은 인제 "복사", "붙여넣기" 등 기본으로 제공될 명령어들입니다. 이것들을 뭐 어떻게저떻게 잘 내 입맛대로 조건문처리를 잘해서 내가 원하는 타이밍에 원하는 명령어만 보이도록 Bool값을 리턴해주도록 하면 돼요!

 

근데 지금 우리가 하려는건 UITextField에 아무것도 못하게 하고 싶은거잖아요? 그럼 그냥 냅다 false를 return해버리면 됩니다. 그럼 Scan 메뉴밖에 안떠요 ㅎㅎ 그럼 이걸 어디서 override 하면 되느냐?! 하실텐데, 이건 Custom UIComponent를 만들어야 합니다.

class UITextFieldForPicker: UITextField {
    // MARK: UITextField 메뉴(복사, 붙여넣기 등) 띄우지 않기
    override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        return false
    }
}

이렇게 UITextField를 상속받은 컴포넌트를 만들고 내부에서 override 해주면, 이제 이 클래스로 만든 객체는 더이상 UIMenu를 띄우지 않게 될거에요! 그리고 Command + V를 통한 붙여넣기도 무시됩니다.

 

그럼 입력자체를 막아보자

위에서 UI를 통한 붙여넣기 입력을 막아보았습니다. 하지만, 우리에겐 문제가 남았죠? 붙여넣기를 못하게 하고 inputView로 Picker를 제공한다고 해도 사용자가 외부 키보드를 사용한다고 하면, 또 막 써지더라고요? 그럼 뭐 어쩌겠어요. 아예 입력을 막아버려야죠 뭐.

UIPicker를 넣어도.... 외부 키보드의 입력은 막을 수 없다....
 

사실 아예 입력을 막아버리는건 생각보다 간단합니다! UITextFieldDelegate에서 제공하는 메서드를 하나 정의하기만 하면 되거든요!

 

textField(\_:shouldChangeCharactersIn:replacementString:)

delegate에게 지정된 텍스트의 변경 여부를 묻는다.

Parameters
- textField: 텍스트를 담고 있는 text field
- range: 대체될 문자의 범위
- string: 지정된 범위의 대체 문자열. 입력하는 동안 이 매개변수에는 일반적으로 입력된 새 문자 하나만 포함되지만 사용자가 텍스트를 붙여넣는 경우 더 많은 문자가 포함될 수 있다. 사용자가 하나 이상의 문자를 삭제하면 대체 문자열은 비게 된다.

Return Value
지정된 텍스트 범위를 교체해야 하는 경우 true, 그렇지 않으면 false로 이전상태를 유지한다.

Discussion
텍스트 필드는 사용자 작업으로 인해 텍스트가 변경될 때마다 이 메서드를 호출합니다. 이 메서드를 사용하여 사용자가 입력한 텍스트의 유효성을 검사합니다. 예를 들어 이 방법을 사용하여 사용자가 숫자 값 외에는 아무것도 입력하지 못하도록 할 수 있습니다.

이친구는 사용자가 TextField에 한번에 입력하는 그만큼씩 range 매개변수로 index range와 그 해당하는 문자열을 string 매개변수로 받아와요. 이게 무슨말이냐면, 예를 들어 12345를 입력한다고 하면, 1을 눌렀을 때 range로 {0, 0}, string으로 "1"이 들어오고 2를 누르면 range로 {1, 0}, string으로 "2"가 들어옵니다!

TextField에 보여지는 값 range(location, length) string
1 {0, 0} 1
12 {1, 0} 2
123 {2, 0} 3
1234 {3, 0} 4
12345 {4, 0} 5

range는 왜 저모양이죠? 하신다면, 또 대답해드리는게 인지상정!

저기 나오는 range는 우리가 잘 아는 Range가 아니고 NSRange 입니당... 막... 1...5 이렇게 쓰는 우리와 친숙한 그친구랑은 다릅니당;;;;; NSRange는 제가 표에 써놨다시피 location이랑 length가 있는데요. location은 우리가 아는 그친구! 인덱스 입니다! 전체에서 몇번째 인덱스부터 시작한다 하는거구요. length는 이전 값이랑 비교했을 때 변경된 값이 몇개인가!를 뜻해요. 그러니까 내가 작성한 길이가 아니고, 오히려 글자를 하나 지우면 1이 된다는 말입니다. 글자를 추가하는 경우는 기존 값에서 바뀌는게 없으니 0이고요!

 

그리고 string의 경우는 한번의 입력으로 들어오는 문자열값! 그러니까 위에서 말하는 range에 해당하는 문자열값입니다. 만약 12345를 붙여넣기로 한번에 넣는다면 string은 12345가 될거에요.

 

그럼 return값은 "텍스트 범위를 교체해야 하는 경우 true, 그렇지 않으면 false로 이전상태를 유지"라고 되어있는데 이게 말이 좀 헷갈릴 수 있잖아요. "텍스트 범위를 교체한다"는 말에 현혹될 필요가 없습니다! 그냥 입력값이 들어오면서 일어난 변경 사항을 반영할거야? 하는 겁니다. true를 반환하면 변경을 반영하겠다. 즉, 입력을 허용하겠다는 거고요. false를 하면 "이전상태를 유지"해야 하니까 변경을 반영하지 않겠다. 즉, '입력을 허용하지 않겠다'는 의미입니다.

 

우리가 하려는건 UIPickerView로 선택하는 것 외의 입력은 전부 막겠다는 것이었잖아요? 그러니까 간단하게 그냥 냅다 false를 리턴해버리면 됩니다.

extension ViewController: UITextFieldDelegate {
    // MARK: 키보드 입력 막기
    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        return false
    }
}

이렇게 하더라도 사용자가 UI를 통해 입력하는 것을 막는것이지 코드적으로 textField.text = 하고 할당해 주는 값까지 금지되는 것은 아닙니다! 그래서 UIPicker로 선택한 값을 넣어줄 수 있게 되는 것이지요!ㅎㅎㅎ

 

사실 여기에 이어서 입력 값을 제한하는 방법도 쓰면 좋은데 글이 너무 길어지는 것 같아서 끊고 다음 포스팅으로 써보겠습니당!

 

마무리

UITextField에 UIPicker를 넣으면서 뜬금 또 거기에 꽂혀서 붙여넣기도 금지! 외부 키보드 입력도 금지를 해야해!! 하면서 막 찾아본 것들을 정리해봤습니다.

UIResponder가 어떤건지, UITextField의 Delegate가 가진 기능도 하나 파볼 수 있어서 유익한 시간이었던것 같습니다!

 

 

 

 

 
 

블로그의 정보

Roen의 iOS 개발로그

Steady On

활동하기