[Rxswift] RxCocoa를 이용해서 textfield 글자수 제한하기
1️⃣ 목표
delegate
대신에RxCocoa
를 이용해 UITextfield의 글자수를 제한하는 기능을 만들고 최종적으로MVVM패턴 을 적용해볼 예정입니다.-
RxCocoa
에 대해선 다음 블로그글을 참고하면 좋을 것 같습니다.
👉🏻👉🏻 토미의 개발노트
2️⃣ 구현
- 아래의 코드를 사용하면 간단하게 글자수를 제한할 수 있습니다.
func limitNumberOfText(maxNumber: Int) {
textField.rx.text.orEmpty
.map { $0.count <= maxNumber }
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] isEditable in
if !isEditable {
self?.textField.text = String(self?.textField.text?.dropLast() ?? "")
}
})
.disposed(by: disposeBag)
}
간단 코드 설명
- .orEmpty: String?옵셔널 타입을 String으로 바꿔주는 역할도 해서 사용하는 것이 좋을 것 같습니다.
.map { $0.count <= maxNumber }
는 다음을 축약한 형태입니다..map({ string -> Bool in if (string.count <= maxNumber) { return true } return false })
- subscribe의 onNext이벤트 클로저안에서 [weak self]를 해준 이유는
reference count 를 증가시키지 않게 하여 메모리릭(memory leak)이 발생하지 않게 하기 위함입니다. - .dropLast()를 이용해서 String의 마지막글자를 잘라줍니다. SubString타입으로 반환하기 때문에 String()으로 감싸서 형변환을 시켜서 사용합니다.
👉🏻👉🏻 SubString - Apple documentation
3️⃣ Signal 사용
- 위의 작업들은 기본적으로
UI 를 변경하는 이벤트들입니다. 그러기 때문에 애플 정책상 Main쓰레드에서 처리하도록 만들어야 합니다. 그래서 위의 코드에서 다음과 같은 코드를 사용한 것입니다.
.observe(on: MainScheduler.instance)
- 여기서 .instance 대신에 asyncInstance을 사용하면
비동기 적으로 처리할 수 있습니다. 하지만 메인쓰레드에서 동시에 이벤트가 발생하더라도 순차적으로 처리해도 상관없다고 생각했기 때문에 굳이 사용하지 않았습니다. Observable은 에러를 이벤트도 방출해줍니다. 하지만 UI적인 입장에서 봤을땐
에러 시에도 프로그램이 종료되지않는 것이 좋습니다. 그렇기 때문에 RxCocoa에서는 subject와 observable을 랩핑한 타입인 Relay, Signal, Driver를 제공해줍니다.
간단하게 다음과 같다고 생각하면 됩니다.- PublishRelay 는 error없는 PublishSubject
- BehaviorRelay 는 error없는 BehaviorSubject
- Driver는 error없는 Behavior같은 Observable
- Signal는 error없는 publishr같은 Observable
또한 이 타입들은 Main쓰레드에서 처리하도록 보장해줍니다. 즉
.observe(on: MainScheduler.instance)
와 같은 코드를 사용하여 스케줄을 관리해줄 필요가 없어집니다.- Signal을 이용하여 코드를 다시 작성하면 다음과 같습니다.
textField.rx.text.orEmpty
.map { $0.count <= maxNumber }
.asSignal(onErrorJustReturn: false)
.emit(onNext: { [weak self] isEditable in
if !isEditable {
self?.textField.text = String(self?.textField.text?.dropLast() ?? "")
}
})
.disposed(by: disposeBag)
4️⃣ 스트림재사용 (.share())
- 이제 현재 글자수를 감지하는 옵저버도 만들어 주겠습니다.
func limitNumberOfText(maxNumber: Int) {
textField.rx.text.orEmpty
.map { $0.count }
.asSignal(onErrorJustReturn: 0)
.emit { [weak self] nb in
let currentNumber = nb > maxNumber ? nb - 1 : nb
self?.limitNumberLabel.text = "글자수: \(currentNumber) 최대글자수: \(maxNumber)"
}
.disposed(by: disposeBag)
textField.rx.text.orEmpty
.map { $0.count <= maxNumber }
.asSignal(onErrorJustReturn: false)
.emit(onNext: { [weak self] isEditable in
if !isEditable {
self?.textField.text = String(self?.textField.text?.dropLast() ?? "")
}
})
.disposed(by: disposeBag)
}
하지만 위의 코드에서 다음의 코드를 재사용 합니다.
textField.rx.rext.orEmpty
이런식으로 사용하는 것은 또하나의 스트림을 할당하게 되어
메모리 낭비 가 됩니다.다행히 .share()를 사용하면 스트림을 재사용할 수 있습니다.
func limitNumberOfText(maxNumber: Int) {
let textObservable = textField.rx.text.orEmpty.share()
textObservable
.map { $0.count }
.asSignal(onErrorJustReturn: 0)
.emit { [weak self] nb in
/* 코드 생략 */
}
.disposed(by: disposeBag)
textObservable
.map { $0.count <= maxNumber }
.asSignal(onErrorJustReturn: false)
.emit(onNext: { [weak self] isEditable in
/* 코드 생략 */
})
.disposed(by: disposeBag)
}
5️⃣ MVVM패턴 적용하기
- 코드간결화를 위해
ViewController
나View
에서는 왠만하면 UI적인 부분만 처리하는 것이 좋습니다. - 그래서 UI에 필요한 데이터 처리부분은
ViewModel 를 만들어서 처리하도록 했습니다.
< SampleViewModel >
struct SampleViewModel {
//View -> ViewModel
let textObservable = BehaviorRelay<String>(value: "")
//ViewModel -> View
let currentLength: Signal<String>
let isEditable: Signal<Bool>
init(maxNumber: Int) {
let textObservable = textObservable.share()
currentLength = textObservable
.map { $0.count }
.map({ nb in
let currentNumber = nb > maxNumber ? nb - 1 : nb
return "글자수: \(currentNumber) 최대글자수: \(maxNumber)"
})
.asSignal(onErrorJustReturn: "")
isEditable = textObservable
.map { $0.count <= maxNumber }
.asSignal(onErrorJustReturn: false)
}
}
ViewModel 를 만들어줌에 따라 기존 ViewController에서는 데이터를 처리할 필요없이ViewModel 의 옵저버들만 구독하여 데이터를 얻어올 수 있습니다.- 코드의 길이가 짧기때문에 MVVM패턴을 적용 전후의 차이가 별로 느껴지지 않습니다. 하지만 명확한 업무 분할로 규모가 커질 수록 가독성과 코드수정이 용이해질 것 같습니다.
< SampleVC >
class SampleVC: UIViewController {
/* 코드 생략 */
func bind(viewModel: SampleViewModel = SampleViewModel(maxNumber: 6)) {
textField.rx.text.orEmpty
.bind(to: viewModel.textObservable)
.disposed(by: disposeBag)
viewModel.currentLength
.emit { [weak self] str in
self?.lengthLabel.text = str
}
.disposed(by: disposeBag)
viewModel.isEditable
.emit(onNext: { [weak self] isEditable in
if !isEditable {
self?.textField.text = String(self?.textField.text?.dropLast() ?? "")
}
})
.disposed(by: disposeBag)
}
}