[RxSwift] Scan

2020-04-06

Scan 연산자는 상태를 다루는데 유용한 연산자입니다. 이번 포스트에서는 Scan 연산자의 동작과 활용에 대해 살펴보도록 하겠습니다.

역할

이전에 방출된 아이템과 새로 방출된 아이템을 결합해 현재 아이템을 생성합니다.

scan1

값을 계속 누적시킬 수 있기 때문에 누산기(accumulator)라고 부르기도 합니다. 무슨 뜻인지 이해가 잘 안되신다구요? 괜찮습니다. 예제를 살펴 보면 이해가 되실 겁니다.

scan1

예제

scan이 실행되는 구체적인 과정은 다음과 같습니다.

scan1

[코드]

Observable.of(1,2,3,4,5)
   .scan(0) { (prevValue, newValue) in
      return prevValue + newValue
   }
   .subscribe(onNext: { print($0) })
   .disposed(by: disposeBag)

[실행 결과]

1
3
6
10
15

scan은 2개의 인자를 사용합니다.

  • 초기값 : scan은 새 아이템이 방출될때마다 이전 값을 사용하는데, 최초 실행시에는 이전 값이 없습니다. 그래서 최초에 사용할 이전 값(마지막 값)을 초기값으로 지정합니다.
  • closure(prevValue, newValue) : scan이 실행 될때마다 이 클로저를 실행해 마지막 값과 현재 방출된 값을 인자로 넣어 사용합니다.

대략 감이 오셨나요? 이제부터가 더 흥미로운 내용입니다.

활용

scan을 여러가지 상태처리에 활용할 수 있습니다.

1. 버튼의 선택 상태변경

버튼을 누를때마다 버튼의 상태가 selected <-> deselected로 번갈아가며 전환됩니다.

[코드]

myButton.rx.tap
   .scan(false) { (lastState, newValue) in
      !lastState
   }
   .bind(to: requestMoreButton.rx.isSelected)
   .disposed(by: disposeBag)

2. 카운터

버튼을 누를때마다 값을 누적합니다.

[코드]

myButton.rx.tap
   .scan(0) { (lastCount, newValue) in
      lastCount + 1
   }
   .subscribe(onNext: { print("taps: \($0)") })
   .disposed(by: disposeBag)

[실행 결과]

taps: 1
taps: 2
taps: 3
taps: 4
taps: 5

3. 텍스트 입력값 종류 제한

TextFieldTextView에 특정한 문자형태만 입력할 수 있도록 제한하는데 사용할 수 있습니다. TextField에 숫자만 입력 가능하도록 scan을 사용해 다음과 같이 구현할 수 있습니다.

[코드]

myTextField.rx.text.orEmpty.asObservable()
   .scan("") { (oldValue, newValue) in
      return newValue.isNumber ? newValue : oldValue
   }
   .bind(to: myTextField.rx.text)
   .disposed(by: disposeBag)

extension String {
   var isNumber: Bool {
   do {
      let regex = try NSRegularExpression(pattern: "^[0-9]+$", options: .caseInsensitive)
      if let _ = regex.firstMatch(in: self, options: .reportCompletion, range: NSMakeRange(0, count)) { return true }
      } catch { return false }
      return false
   }
}

TextField의 text가 변경될때마다 이전 text화 변경된 text를 비교하는데, 만약 새로 입력된 문자가 숫자면 새로운 입력을 TextField에 적용하고, 숫자가 아니면 새로운 입력을 무시하고 이전값을 유지하게 됩니다.

TextField12ab3을 차례로 입력해 보겠습니다.

[실행 결과]

Event next(1)
text: 1
Event next(12)
text: 12
Event next(12a)
text: 12
Event next(12b)
text: 12
Event next(123)
text: 123

12ab3을 입력했지만 TextField에는 123만이 입력된 것을 확인할 수 있습니다.

4. 화면상태 처리

화면의 상태를 처리하기 위해 scan을 사용할 수 있습니다. 화면이 A, B, C 세개가 있고 화면은 A -> B -> C 로 이동하는데 뒤로가기도 할 수 있어서 C -> B -> A로도 이동가능하다고 가정해보죠.

만약 B화면이 보여졌는데 A -> B로 이동해서 보여진 경우와 C -> B로 뒤로가기로 이동해 보여진 B화면을 각각 다르게 처리하기 위해 scan을 사용할 수있습니다.

[코드]

enum SceneState {
   case initial
   case sceneA
   case sceneB
   case sceneC
}

let sceneState: BehaviorRelay<SceneState> = BehaviorRelay(value: .initial)
        
sceneState.asObservable()
   scan(.initial) { (prev, current) in
      switch (prev, current) {
      case (.sceneA, .sceneB)
         // 화면이 A -> B로 이동했을 경우 처리
      case (.sceneC, .sceneB)
         // 화면이 C -> B로 이동했을 경우 처리
      }
}

이상으로 scan 연산자에 대해 살펴봤습니다. 상태 관련해 뭔가를 처리해야 한다면 scan을 사용하는 것을 먼저 생각해보세요.

이 포스트에서 제공한 예제 말고도 다양하게 응용해서 활용할 수 있습니다. (👨🏻‍💻지식이 +3늘었다.)

다음 포스트에서 또 만나요~ 🚀😄

[참 고]



[책] 토미의 Git with 소스트리

Git을 제대로 알고 싶으신 분들께 추천드립니다.