UITextField에서 rx.text로 변경된 텍스트를 감지하기

sanghoon Ahn·2021년 2월 20일
5

Daily Issue

목록 보기
3/10

Daily Issue #3

안녕하세요 dvHuni입니다 ~

데일리 이슈의 세번째 포스트 시작해보도록 하겠습니다!!

날씨가 많이 오락가락하네요 다들 건강조심하세요 ~! 🥶


무엇이 문제입니까?

오늘은 RxCocoa 환경에서 개발을 하게되면 누구나 한번쯤 겪을만한 주제입니다!!

바로 UITextField에서 rx.text로 변경된 텍스트를 감지하여 API를 호출 입니다.

textfield.rx.text를 사용하여 textfield의 값을 계속해서 받고, 그 값으로 API를 호출 하는데,
값이 변경되고 나서도 원하지 않게 API가 호출되는 현상이 있었습니다.

제가 겪었던 문제를 함께 보면서 해결법도 같이 찾아보죠!

그러면 시작해 봅시다! 🙂

우선 rx환경이므로 texfield의 text를 subscribe하고 ..

테스트를 해보면...?

import RxCocoa
import RxSwift 

var diposeBag = DisposeBag()
let textfield = UITextField()

textfield.rx.text
	.subScribe(onNext: { changedText in
		 print("Changed Text ::: \(changedText)")
	})
	.disposed(by: disposeBag())

// focus textfield & typing dvHuni
// Changed Text ::: Optional("")
// Changed Text ::: Optional("d")
// Changed Text ::: Optional("dv")
// Changed Text ::: Optional("dvH")
// Changed Text ::: Optional("dvHu")
// Changed Text ::: Optional("dvHun")
// Changed Text ::: Optional("dvHuni")

흠... 잘되네요..

어 근데 왜 옵셔널일까요 ??

RxCocoa의 UITextField.text Definition을 가봅시다.

/// Reactive wrapper for `text` property.
public var text: RxCocoa.ControlProperty<String?> { get }

Optional String을 던져주는군요. OK.

Optional String을 걸러주는 orEmpty를 적용해봅시다.

textfield.rx.text
	.orEmpty
	.subScribe(onNext: { changedText in
		 print("Changed Text ::: \(changedText)")
	})
	.disposed(by: disposeBag())

// focus textfield & typing dvHuni
// Changed Text ::: 
// Changed Text ::: d
// Changed Text ::: dv
// Changed Text ::: dvH
// Changed Text ::: dvHu
// Changed Text ::: dvHun
// Changed Text ::: dvHuni

근데 왜 처음에 공백이 찍히지...?

아! 첫 subScribe시에 값을 가져오는구나! 이정도는 가볍습니다!!

지정한 횟수 만큼 이벤트를 skip해주는 .skip을 1로 적용해서 테스트 해봅시다~!

textfield.rx.text
	.orEmpty
	.skip(1)
	.subScribe(onNext: { changedText in
		 print("Changed Text ::: \(changedText)")
	})
	.disposed(by: disposeBag())

// focus textfield & typing dvHuni
// Changed Text ::: d
// Changed Text ::: dv
// Changed Text ::: dvH
// Changed Text ::: dvHu
// Changed Text ::: dvHun
// Changed Text ::: dvHuni
// close keyoboard
// Changed Text ::: dvHuni

그렇지 그렇지 ~ 첫 공백 이벤트가 찍히지 않네요~!!

근데 ...

키보드를 닫았는데 왜 찍힐까요 ..?? 🥺

이벤트가 걸려있나 보군요.

자세히 보기위해 다시 text의 Definition를 들어가봅시다.

/// Reactive wrapper for `text` property.
public var text: ControlProperty<String?> {
    value
}
    
/// Reactive wrapper for `text` property.
public var value: ControlProperty<String?> {
    return base.rx.controlPropertyWithDefaultEvents(
        getter: { textField in
            textField.text
        },
        setter: { textField, value in
            // This check is important because setting text value always clears control state
            // including marked text selection which is imporant for proper input 
            // when IME input method is used.
            if textField.text != value {
                textField.text = value
            }
        }
    )
}

으음... rx.controlPropertyWithDefaultEvents 이녀석이 이벤트에 관련된 녀석인것 같은데.. 살펴볼까요 ?

/// This is a separate method to better communicate to public consumers that
/// an `editingEvent` needs to fire for control property to be updated.
internal func controlPropertyWithDefaultEvents<T>(
    editingEvents: UIControl.Event = [.allEditingEvents, .valueChanged],
    getter: @escaping (Base) -> T,
    setter: @escaping (Base, T) -> Void
    ) -> ControlProperty<T> {
    return controlProperty(
        editingEvents: editingEvents,
        getter: getter,
        setter: setter
    )
}

😱 이녀석입니다 !!

[.allEditingEvents, .valueChanged] 두가지 케이스의 이벤트가 발생할 때 이벤트를 발생 시켜 textfield의 값을 return 하게 됩니다!

allEditingEvents라 하면,

UITextfield의 모든 editing touch라고 정의하고있습니다!

즉, 값을 입력하기 위한 touch, 입력된 값을 수정하기 위한 touch, 입력 완료되어 키보드를 내리는 동작

모두 event를 발생시킵니다!

그렇다면 우리가 원하는것은 !! 값이 변경 될 때만 textfield의 값을 얻고싶은것 !!

좋은 방법이 없을까...? 하다가 찾게된 내용!

바로 RxSwift의 distinctUntilChanged 함수 입니다.

정의를 먼저 살펴볼까요?

/**
 Returns an observable sequence that contains only distinct contiguous elements according to equality operator.
 - seealso: [distinct operator on reactivex.io](http://reactivex.io/documentation/operators/distinct.html)
 - returns: An observable sequence only containing the distinct contiguous elements, based on equality operator, from the source sequence.
*/
public func distinctUntilChanged() -> RxSwift.Observable<Self.Element>

저희가 찾고있던 내용과 딱! 일치합니다.

등호 연산자(=)에 의해 서로 다른 Element인 Observable Sequence를 반환합니다.

즉, 현재 가지고있는 element와 다른 element가 발생했을때, 해당 element를 return해줍니다.

위에서 말씀드린 세가지의 동작은 모두 입력의 변화는 없고, touch에 의한 이벤트이기 때문에 textfield의 값은 모두 변화가 없습니다.

다시말하면, 이전 이벤트의 값과 발생한 이벤트의 값이 같다 라는 것입니다.

그렇다면 distinctUntilChanged()를 적용하여 값의 변화가 있는 이벤트만 추려봅시다!!

textfield.rx.text
	.orEmpty
	.distinctUnitlChanged()
	.subScribe(onNext: { changedText in
		 print("Changed Text ::: \(changedText)")
	})
	.disposed(by: disposeBag())

// focus textfield & typing dvHuni
// Changed Text ::: d
// Changed Text ::: dv
// Changed Text ::: dvH
// Changed Text ::: dvHu
// Changed Text ::: dvHun
// Changed Text ::: dvHuni
// close keyboard

크으 — 만족스럽습니다!

포커스, 키보드를 닫는 이벤트 모두 skip되고, 실제 값이 변할 때만 이벤트가 발생하고 있습니다!

저는 키보드를 닫거나, 포커스가 되었을때 계속 동일한 값으로 API가 호출되는 현상을 겪었습니다 😂

오늘을 기점으로 이런 방식이 있을때 어렵지 않게 해결 할 수 있을것 같습니다!

오늘도 즐겁게 조금 더 배웠다는 마음으로 포스팅을 마칩니다!!

지적이나 질문은 언제나 환영입니다!!

읽어주셔서 감사합니다! 🙇‍♂️

profile
hello, iOS

0개의 댓글