[Rxswift] RxCocoa를 이용해서 textfield 글자수 제한하기


1️⃣ 목표

  1. delegate대신에 RxCocoa를 이용해 UITextfield의 글자수를 제한하는 기능을 만들고 최종적으로 MVVM패턴을 적용해볼 예정입니다.
  2. RxCocoa에 대해선 다음 블로그글을 참고하면 좋을 것 같습니다.
    👉🏻👉🏻 토미의 개발노트
finished version

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의 마지막글자를 잘라줍니다. document of .dropLast() SubString타입으로 반환하기 때문에 String()으로 감싸서 형변환을 시켜서 사용합니다.
    👉🏻👉🏻 SubString - Apple documentation

3️⃣ Signal 사용

  • 위의 작업들은 기본적으로 UI를 변경하는 이벤트들입니다. 그러기 때문에 애플 정책상 Main쓰레드에서 처리하도록 만들어야 합니다. 그래서 위의 코드에서 다음과 같은 코드를 사용한 것입니다.
.observe(on: MainScheduler.instance)
  • 여기서 .instance 대신에 asyncInstance을 사용하면 비동기적으로 처리할 수 있습니다. 하지만 메인쓰레드에서 동시에 이벤트가 발생하더라도 순차적으로 처리해도 상관없다고 생각했기 때문에 굳이 사용하지 않았습니다.
  • Observable은 에러를 이벤트도 방출해줍니다. 하지만 UI적인 입장에서 봤을땐 에러시에도 프로그램이 종료되지않는 것이 좋습니다. 그렇기 때문에 RxCocoa에서는 subject와 observable을 랩핑한 타입인 Relay, Signal, Driver를 제공해줍니다.
    간단하게 다음과 같다고 생각하면 됩니다.

    • PublishRelayerror없는 PublishSubject
    • BehaviorRelayerror없는 BehaviorSubject
    • Drivererror없는 Behavior같은 Observable
    • Signalerror없는 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패턴 적용하기

  • 코드간결화를 위해 ViewControllerView 에서는 왠만하면 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)
  }
}




© 2021.02. by kirim

Powered by kkrim