티스토리 뷰

RxSwift로 UITextField 글자 수 제한하기

Swift Version 5.3
Xcode Version 12.3

RxSwift가 대세라길래 공부를 하고 있는 중입니다.
RxSwift가 무엇인지 등에 대한 기본적인 내용들은 다른 블로그에서 많이 설명이 되어있기 때문에,
제 나름대로 이해한 내용을 바탕으로 프로젝트를 만들어볼까 합니다.

정말 정말 왕초보의 글이기 때문에 오류가 있는 경우, 댓글로 알려주세요!

제가 RxSwift를 공부하면서 가장 많이 참고한 자료는 곰튀김님의 유튜브 영상입니다.
곰튀김님의 영상은 다들 꼭 보시길 바랍니다!!

베이스 프로젝트

저는 글또 활동을 하고 있는데요, 지난 글 중 소영님의 글을 보고 이 내용을 차용해서 Rx를 적용해보고 싶다는 생각이 들었습니다.
조건은 다음과 같습니다.

1. UITextField에는 6자까지만 입력할 수 있습니다.
2. 그 이상을 입력할 경우 키보드가 내려갑니다.
3. 입력값이 2자 이상 6자 이하가 아닌 경우 빨간색 안내문이 보이며,
   입력값이 2자 이상 6자 이하인 경우 초록색 안내문이 보입니다.

화면

화면은 간단하게 텍스트 필드와 라벨을 추가했습니다.
각각의 이름은 idTextField, infoLabel 입니다.

0. RxSwift 설정하기

프로젝트에 RxSwift와 RxCocoa를 사용하기 위해서는 CocoaPod이 설치되어 있어야 합니다.
Pod을 설치하신 후 프로젝트 폴더에서 터미널을 여신 후 pod init 을 통해 Podfile을 생성합니다.

Podfile에 아래 내용을 입력한 후,

1
2
pod RxSwift
pod RxCocoa
cs

pod install 명령어를 통해 설치해줍니다.

이제 .xcworkspace 파일을 열어서 작업을 진행할 때

1
2
import RxSwift
import RxCocoa
cs

를 사용할 수 있습니다!

 

조건 1. UITextField에는 6자까지만 입력할 수 있습니다.

idTextField에는 6자까지만 입력되고, 그 이상은 입력되지 않도록 막기 위해 아래의 함수를 추가했습니다.

1
2
3
4
5
6
private func trimId(_ id: String) {
  if id.count > 6 {
    let index = id.index(id.startIndex, offsetBy: 6)
    self.idTextField.text = String(id[..<index])
  }
}
cs

그리고 이 함수를 실행시키는 rx 함수는 다음과 같습니다.

1
2
3
4
5
idTextField.rx.text.orEmpty
            .subscribe(onNext: { s in
                self.trimId(s)
            })
.disposed(by: disposeBag)
cs

idTextField의 text 입력을 감지하고, 입력값이 있을 때만 감지할 것이라서
idTextField.rx.text.orEmpty를 subscribe로 구독합니다.
onNext로 idTextField에 입력된 값이 String으로 넘어올 것이기 때문에
해당 값을 바로 self.trimId의 argument로 전달하면 됩니다.
또한 메모리 누수를 방지하기 위해 disposeBag에 담아주어야 합니다.

 

조건 2. 그 이상을 입력할 경우 키보드가 내려갑니다.

6자까지 입력 가능하기 때문에 7자 이상으로 입력할 경우를 확인하기 위해서 아래의 함수를 추가합니다.

1
2
3
private func checkIdCount(_ id: String-> Bool {
    return id.count > 6
}
cs

이 함수를 통해 조건이 넘는 7자 이상의 글자가 입력되면 true를 반환하게 됩니다.

1
2
3
4
5
6
7
8
idTextField.rx.text.orEmpty
    .map(checkIdCount(_:))
    .subscribe(onNext: { b in
        if b {
            self.idTextField.resignFirstResponder()
        }
    })
    .disposed(by: disposeBag)
cs

이번에는 아까와 유사한데, map이 추가되었습니다!

출처: ReactiveX 공식 홈페이지 (http://reactivex.io/)

map 안에서 실행되는 함수에 맞게 변환하여 출력하는 함수입니다.
1번에서 사용했던 rx 함수를 생각해보면, 원래는 idTextField에 입력된 값을 그대로 리턴하겠죠?
하지만 checkIdCount를 통해 해당 값의 count가 7자 이상인지 아닌지에 대한 boolean 값이 리턴되도록 변경되었습니다.
따라서 우리는 7자 이상이면 -- boolean 값이 true -- 이면 resignFirstResponder를 호출하여 키보드가 내려가도록 설정할 수 있습니다.

 

조건3. 입력값이 2자 이상 6자 이하가 아닌 경우 빨간색 안내문이 보이며,
입력값이 2자 이상 6자 이하인 경우 초록색 안내문이 보입니다.

조건 3은 조건 2와 똑같습니다.

1
2
3
private func checkIdValid(_ id: String-> Bool {
    return id.count < 2 || id.count > 6
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
idTextField.rx.text.orEmpty
    .map(checkIdValid(_:))
    .subscribe(onNext: { b in
        if b {
            self.infoLabel.textColor = .red
            self.infoLabel.text = "아이디는 2자 이상 6자 이하로 입력 해주세요."
        } else {
            self.infoLabel.textColor = .green
            self.infoLabel.text = "사용할 수 있는 아이디입니다."
        }
    })
    .disposed(by: disposeBag)
cs

이렇게 세 가지의 조건을 구현했습니다.

이 때 조건 1과 조건 2의 함수를 합쳐서 구현할 수 있습니다.

 

조건 1 + 조건 2

1
2
3
4
5
6
7
8
9
private func checkAndTrimId(_ id: String-> Bool {
    if id.count > 6 {
        let index = id.index(id.startIndex, offsetBy: 6)
        self.idTextField.text = String(id[..<index])
        
        return true
    }
    return false
}
cs

rx 실행함수도 아래와 같이 간단해집니다.

1
2
3
4
5
6
7
8
idTextField.rx.text.orEmpty
    .map(checkId(_:))
    .subscribe(onNext: { b in
        if b {
            self.idTextField.resignFirstResponder()
        }
    })
    .disposed(by: disposeBag)
cs

 

조건 1 + 2 + 3

RxSwift 사용으로 인한 극한의 효율을 보이기 위해서는 세 가지의 조건 모두 합쳐서 작성할 수 있습니다.

(그렇다면 왜 위에서 조건 1 + 2를 했냐고 여쭤보신다면, 이 글을 정리하면서 갑자기 생각이 나서 그렇습니다.)

1
2
3
4
5
enum IdRange {
    case over
    case under
    case normal
}
cs

IdRange 라는 enum을 만들어서 해당 입력 값의 범위를 저장할 수 있도록 합니다.
6자가 넘으면 넘는 길이만큼 자르고 over를, 2자가 되지 않으면 under를, 그렇지 않으면 normal을 리턴하는 함수를 만들었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
private func checkId(_ id: String-> IdRange {
    if id.count > 6 {
        let index = id.index(id.startIndex, offsetBy: 6)
        self.idTextField.text = String(id[..<index])
            
        return .over
    }
        
    if id.count < 2 {
        return .under
    }
    return .normal
}
cs

Rx 함수 또한 굉장히 짧아졌습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
idTextField.rx.text.orEmpty
    .map(checkId(_:))
    .subscribe(onNext: { e in
        switch e {
            case .over:
                self.infoLabel.textColor = .red
                self.infoLabel.text = "아이디는 2자 이상 6자 이하로 입력 해주세요."
                    
                self.idTextField.resignFirstResponder()
            case .under:
                self.infoLabel.textColor = .red
                self.infoLabel.text = "아이디는 2자 이상 6자 이하로 입력 해주세요."
            case .normal:
                self.infoLabel.textColor = .green
                self.infoLabel.text = "사용할 수 있는 아이디입니다."
        }
    })
    .disposed(by: disposeBag)
cs

위 함수를 통해서 조건 1, 2, 3에 해당하는 모든 내용을 하나로 함축할 수 있습니다.
기존에 시행되었던 delegate 등록이나, NotificationCenter 등록 같은 절차가 없어도 된다는 뜻입니다!

사실 조건 1+2+3은 극한의 효율을 위한 부분인 것 같고,
제가 실제 개발을 하는 중이었다면 조건 1+2까지만 사용해서 textField를 관리하는 함수와 label과 관련된 함수로 나누어 가독성을 높이는 데에 신경을 쓰지 않을까 싶습니다.

앞으로 RxSwift 공부를 더 해 보면서 기존 프로젝트를 변환하는 방법이나 이론적인 부분에 대해서도 좀 더 다뤄보겠습니다.

댓글