Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- combine
- RxCocoa
- SWIFT
- tableView
- Refactoring
- Protocol
- MVVM
- map
- Xcode
- Observable
- ios
- ribs
- Clean Code
- 리팩토링
- collectionview
- swiftUI
- 리펙터링
- uitableview
- HIG
- clean architecture
- swift documentation
- UICollectionView
- 애니메이션
- UITextView
- rxswift
- Human interface guide
- 리펙토링
- 스위프트
- 클린 코드
- uiscrollview
Archives
- Today
- Total
김종권의 iOS 앱 개발 알아가기
[iOS - swift] 2. RxSwift의 Map, FlatMap - 사이드 이펙트 처리 방법 (throw와 catch 사용) 본문
RxSwift/RxSwift 응용
[iOS - swift] 2. RxSwift의 Map, FlatMap - 사이드 이펙트 처리 방법 (throw와 catch 사용)
jake-kim 2022. 2. 1. 11:501. RxSwift의 Map, FlatMap - 사용하여 비동기를 순서대로 처리 방법
2. RxSwift의 Map, FlatMap - 사이드 이펙트 처리 방법 (throw와 catch 사용)
map과 flatMap 연산자 개념을 구분
- 개념 구분
- flatMap은 블록 내에서 Observable을 리턴해야하므로, API를 사용할때 응답값이 Observable일때 flatMap사용
- map의 리턴값은 Observable이 아니고, 사이드이펙트 처리 시 throw를 리턴하여 처리
- 주의사항: 스트림안에서 throw가 하나라도 방출되면, 해당 stream은 dispose되므로 사용자가 계속 재시도 할 수 있는 이벤트 처리에는 부적합한것을 주의 (예제 상황도 사용자가 계속 시도할 수 있으므로 부적합하지만 설명을 위해서 구현)
- 사용 아이디어
- flatMap 안에서 API 호출
- map 블록 안에서 값 체크 후, throw를 리턴하여 사이드 이펙트 처리
flatMap, map, throw, catch를 이용한 사이드이펙트 구현
상황)
유저가 로그인하는 상황에서 로그인 결과, 이전에 회원가입 한 유저, block 유저인지, 가입하지 않은 유저인지 사이드 이펙트 처리 필요
- 사이드 이펙트 정의
// UserState.swift
enum UserState: Error {
case block
case noUser
case abuser
}
- API 정의
- 시뮬레이션을 위해 내부는 임시적으로 구현 (편의상 로그인 api가 동작한다고 가정)
// API.swift
enum API {
static func signIn(
email: String?,
password: String?
) -> Observable<Int> {
Observable.just((0...2).randomElement() ?? -1)
}
}
- map블록 내에서 thorw로 반환하고 catch에서 처리될 사이드 이펙트 케이스 정의
- map은 아래 throws키워드를 보면 알 수 있듯이, throw를 리턴 가능 - 처리는 catch(_:) 연산자에서 처리
- map은 아래 throws키워드를 보면 알 수 있듯이, throw를 리턴 가능 - 처리는 catch(_:) 연산자에서 처리
- UI 준비
- UITextField 2개
- UIButton 1개
- 상태값 UILabel 1개
- 사용한 프레임워크
# Rx
pod 'RxSwift'
pod 'RxCocoa'
# UI
pod 'SnapKit'
pod 'Then'
- UI 구현
import UIKit
import SnapKit
import Then
import RxSwift
import RxCocoa
class ViewController: UIViewController {
private let emailTextField = UITextField().then {
$0.borderStyle = .roundedRect
$0.placeholder = "abcd@google.com"
$0.textColor = .black
}
private let passwordTextField = UITextField().then {
$0.borderStyle = .roundedRect
$0.placeholder = "password"
$0.isSecureTextEntry = true
}
private let confirmButton = UIButton().then {
$0.setTitle("확인", for: .normal)
$0.setTitleColor(.white, for: .normal)
$0.setTitleColor(.blue, for: .highlighted)
$0.setBackgroundImage(UIColor.systemBlue.asImage(), for: .normal)
$0.setBackgroundImage(UIColor.gray.asImage(), for: .disabled)
$0.isEnabled = false
}
private let resultLabel = UILabel().then {
$0.textColor = .blue
}
private let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.emailTextField)
self.view.addSubview(self.confirmButton)
self.view.addSubview(self.passwordTextField)
self.view.addSubview(self.resultLabel)
self.emailTextField.snp.makeConstraints {
$0.top.left.equalTo(self.view.safeAreaLayoutGuide)
$0.width.equalTo(self.view.safeAreaLayoutGuide).multipliedBy(2/3.0)
}
self.confirmButton.snp.makeConstraints {
$0.top.right.equalTo(self.view.safeAreaLayoutGuide)
$0.width.equalTo(self.view.safeAreaLayoutGuide).multipliedBy(1/3.0)
}
self.passwordTextField.snp.makeConstraints {
$0.top.equalTo(self.emailTextField.snp.bottom)
$0.left.equalTo(self.view.safeAreaLayoutGuide)
$0.width.equalTo(self.view.safeAreaLayoutGuide).multipliedBy(2/3.0)
}
self.resultLabel.snp.makeConstraints {
$0.top.equalTo(self.passwordTextField.snp.bottom).offset(12)
$0.centerX.equalToSuperview()
}
}
}
extension UIColor {
func asImage(_ width: CGFloat = UIScreen.main.bounds.width, _ height: CGFloat = 1.0) -> UIImage {
let size: CGSize = CGSize(width: width, height: height)
let image: UIImage = UIGraphicsImageRenderer(size: size).image { context in
setFill()
context.fill(CGRect(origin: .zero, size: size))
}
return image
}
}
사이드 이펙트 처리
- 바인딩
- Flatmap 블록 내부에서는 api 호출
- map 블록 내부에서는 사이드 이펙트, throw 던지기
- catch 에서 throw 처리
- 주의사항
- map에서 throw가 한 번 이상 발생하면 해당 스트림은 disposed되므로, 입력 후 버튼을 클릭해도 이벤트가 들어가지 않으므로 1회용임을 주의
- 때문에 예제와 같은, 계속 이메일/패스워드 입력 후 재시도하는 상황에서 비적합하지만, 이해를 돕기위해 구현
- 바인딩 구현
- 버튼이 눌린 경우, email과 password 입력값 획득 - 동시에 얻어야 하므로 zip을 사용하여
self.confirmButton.rx.tap .withLatestFrom( Observable.combineLatest( self.emailTextField.rx.text.asObservable(), self.passwordTextField.rx.text.asObservable() ) )
- map 안에서 어뷰저 유저인지 확인 - 이메일 입력 값에 '?'가 포함되어 있으면 어뷰저 유저라고 판단
.map { guard !($0 ?? "").contains("?") else { throw UserState.abuser } return ($0, $1) }
- flatMap 안에서 Observable타입을 반환하는 API 호출 (Observable 타입을 반환하지 않으면 map 사용)
.flatMap { API.signIn(email: $0, password: $1) }
- map 안에서 해당 api 응답값을 보고 해당 유저가 어떤 유저인지 판단하여 throw 혹은 정상 로그인 판단
.map { intValue in switch intValue { case 0: throw UserState.block case 1: throw UserState.noUser default: break } return Void() }
- catch에서 throws 처리
.catch { [weak self] error -> Observable<Void> in switch error { case UserState.abuser: self?.resultLabel.text = "어뷰져 유저" case UserState.block: self?.resultLabel.text = "block 유저" case UserState.noUser: self?.resultLabel.text = "회원가입이 필요한 유저" default: break } return .empty() }
- throw에서 걸리지 않고 밑에까지 이벤트가 넘어온 경우, 로그인 성공
.do(onNext: { [weak self] in self?.resultLabel.text = "로그인 성공" }) .subscribe() .disposed(by: self.disposeBag)
- 버튼이 눌린 경우, email과 password 입력값 획득 - 동시에 얻어야 하므로 zip을 사용하여
* 바인딩 전체 코드
self.confirmButton.rx.tap
.withLatestFrom(
Observable.combineLatest(
self.emailTextField.rx.text.asObservable(),
self.passwordTextField.rx.text.asObservable()
)
)
.map {
guard !($0 ?? "").contains("?") else { throw UserState.abuser }
return ($0, $1)
}
.flatMap { API.signIn(email: $0, password: $1) }
.map { intValue in
switch intValue {
case 0:
throw UserState.block
case 1:
throw UserState.noUser
default:
break
}
return Void()
}
.catch { [weak self] error -> Observable<Void> in
switch error {
case UserState.abuser:
self?.resultLabel.text = "어뷰져 유저"
case UserState.block:
self?.resultLabel.text = "block 유저"
case UserState.noUser:
self?.resultLabel.text = "회원가입이 필요한 유저"
default:
break
}
return .empty()
}
.do(onNext: { [weak self] in self?.resultLabel.text = "로그인 성공" })
.subscribe()
.disposed(by: self.disposeBag)
'RxSwift > RxSwift 응용' 카테고리의 다른 글
Comments