rxSwift 에서 relay에 대해

임혜정·2024년 8월 8일
0
post-custom-banner

rxSwift의 핵심 포인트 : 구독-> 바인딩-> 방출

rxSwift에서 Observable은 시간에 따라 이벤트나 값의 시퀀스를 생성하는 객체다. '관찰 가능'한 연속적으로 전달되는 데이터 흐름을 말한다.

그리고 rxRelay는 rxSwift에 의존하는 라이브러리로, rxCocoa프레임워크에서 제공하는 Observable의 종류이고 주로 UI이벤트 처리나 데이터 바인딩에 사용된다.

RxSwift - relay의 특징

❗️ BehaviorRelay / PublishRelay 두 가지 유형

  • BehaviorRelay 는 BehaviorSubject와 유사하게 초기값을 가지며, 현재 값을 저장한다
  • PublishRelay는 PublishSubject와 유사하나 에러나 완료 이벤트를 방출하지 않음.

accept(_:) 메서드를 통해 새로운 값을 추가할 수 있다.

RxRelay : 데이터 흐름 관리 도구

일반적인 Observable과 다르게 RxRelay는 데이터 스트림이 끝나거나 오류가 발생하지 않는다고 가정한다. 끊임없이 흐르는 물처럼 데이터를 계속해서 전달하는데 UI 상태나 지속적으로 업데이트되는 데이터를 다룰 때 유용하다


두 가지 종류의 Relay 무엇을 쓸 것인가?

1. PublishRelay:

구독 이후에 발생하는 이벤트만 전달한다. 초기값이 없다.
구독 시점에 이미 발생한 이벤트는 전달되지 않는다.


❗️PublishRelay 는 언제?

단방향 데이터 흐름 : 이벤트가 발생하면 그 순간에만 처리하고 싶을 때

이벤트 발생 시점 관리 : 이벤트 발생 시점을 정확히 알고 싶을 때 >
예를 들어 단순히 버튼이 탭된 횟수만 세는 것이라면 behavior가 더 간단할 수 있음. 그러나 탭의 간격, 연속탭의 패턴처럼 시점에 대해 세밀한 제어와 분석이 필요하다면 publish가 더 적합할 수 있음

초기값이 필요 없는 경우 : 초기값이 없어도 문제가 되지 않을 때


import RxSwift
import RxCocoa
import UIKit
import SnapKit

// MARK: - Model
struct TapCountModel {
 var count: Int
}

// MARK: - View
class MainView: UIView {
 let button1: UIButton = {
     let btn = UIButton()
     btn.tintColor = .blue
     btn.setTitle("btn", for: .normal)
     btn.setTitleColor(.white, for: .normal)
     return btn
 }()
 
 let label = UILabel()
 
 override init(frame: CGRect) {
     super.init(frame: frame)
     setupUI()
 }
 
 required init?(coder: NSCoder) {
     fatalError("init(coder:) has not been implemented")
 }
 
 private func setupUI() {
     [button1,label].forEach { addSubview($0)}
     
     button1.snp.makeConstraints {
         $0.width.equalTo(80)
         $0.height.equalTo(40)
         $0.centerX.equalToSuperview()
         $0.centerY.equalToSuperview()
     }
     
     label.snp.makeConstraints {
         $0.top.equalTo(button1.snp.bottom).offset(10)
         $0.centerX.equalToSuperview()
     }
 }
}

// MARK: - ViewModel
class ViewModel {
 private var model: TapCountModel
 let buttonTaps = PublishRelay<Void>()
 let tapCount = PublishRelay<Int>()
 private let disposeBag = DisposeBag()
 
 init() {
     model = TapCountModel(count: 0)
     
     buttonTaps
         .subscribe(onNext: { [weak self] _ in
             self?.incrementCount()
         })
         .disposed(by: disposeBag)
 }
 
 private func incrementCount() {
     model.count += 1
     tapCount.accept(model.count)
 }
}

// MARK: - ViewController
class ViewController: UIViewController {
 private let viewModel = ViewModel()
 private let mainView = MainView()
 private let disposeBag = DisposeBag()
 
 override func viewDidLoad() {
     super.viewDidLoad()
     setupBindings()
 }
 
 override func loadView() {
     view = mainView
 }
 
 private func setupBindings() {
     mainView.button1.rx.tap
         .bind(to: viewModel.buttonTaps)
         .disposed(by: disposeBag)
     
     viewModel.tapCount
         .subscribe(onNext: { [weak self] count in
             self?.mainView.label.text = "버튼을 \(count) 번누름"
             print("업데이트: \(count)")
         })
         .disposed(by: disposeBag)
 }
}

ex) 버튼 클릭 이벤트를 전달하여 ViewModel에서 처리하는 경우 네트워크 요청 결과를 받아 UI를 업데이트하는 경우

2. BehaviorRelay:

구독 시점의 최신 값과 이후 발생하는 모든 이벤트를 전달한다. 반드시 초기값이 필요하다. 현재 값을 항상 유지하고 있기 때문에 UI상태관리에 적합하다

❗️BehaviorRelay 는 언제?

UI 상태 관리 : UI 요소의 상태를 관리하고 실시간으로 반영해야 할 때
초기값이 필요한 경우 : 초기값을 설정하고 이를 기반으로 상태를 관리해야 할 때
값을 캐싱 : 이전 값을 참고하여 다음 값을 계산해야 할 때

import RxSwift
import RxCocoa
import UIKit
import SnapKit


// MARK: - Model
struct UserProfile {
 var name: String
 var email: String
}

// MARK: - View
class ProfileUpdateView: UIView {
 let nameTextField = UITextField()
 let emailTextField = UITextField()
 let saveButton: UIButton = {
     let btn = UIButton()
     btn.setTitle("저장", for: .normal)
     btn.setTitleColor(.white, for: .normal)
     btn.backgroundColor = .systemBlue
     btn.layer.cornerRadius = 10
     btn.clipsToBounds = true
     return btn
 }()
 
 override init(frame: CGRect) {
     super.init(frame: frame)
     setupUI()
 }
 
 required init?(coder: NSCoder) {
     fatalError("init(coder:) has not been implemented")
 }
 
 private func setupUI() {
     [nameTextField,emailTextField,saveButton].forEach { addSubview($0) }
     
     nameTextField.snp.makeConstraints {
         $0.centerY.equalToSuperview()
         $0.centerX.equalToSuperview()
         $0.width.equalTo(200)
         $0.height.equalTo(40)
     }
     emailTextField.snp.makeConstraints {
         $0.top.equalTo(nameTextField.snp.bottom).offset(10)
         $0.centerX.equalToSuperview()
         $0.width.equalTo(200)
         $0.height.equalTo(40)
     }
     saveButton.snp.makeConstraints {
         $0.top.equalTo(emailTextField.snp.bottom).offset(20)
         $0.centerX.equalToSuperview()
         $0.width.equalTo(80)
         $0.height.equalTo(40)
     }
     
     
     nameTextField.placeholder = "Enter name"
     emailTextField.placeholder = "Enter email"
 }
}

// MARK: - ViewModel
class ProfileUpdateViewModel {
 private let userProfile: UserProfile
 let name: BehaviorRelay<String>
 let email: BehaviorRelay<String>
 let isValid = BehaviorRelay<Bool>(value: false)
 
 init(userProfile: UserProfile) {
     self.userProfile = userProfile
     self.name = BehaviorRelay<String>(value: userProfile.name)
     self.email = BehaviorRelay<String>(value: userProfile.email)
     
     Observable.combineLatest(name, email)
         .map { name, email in
             return !name.isEmpty && !email.isEmpty && email.contains("@")
         }
         .bind(to: isValid)
         .disposed(by: disposeBag)
 }
 
 private let disposeBag = DisposeBag()
 
 func updateProfile() -> Observable<UserProfile> {
     return Observable.create { [weak self] observer in
         guard let self = self else {
             observer.onCompleted()
             return Disposables.create()
         }
         
         let updatedProfile = UserProfile(
             name: self.name.value,
             email: self.email.value
         )
         
         // Simulate API call
         DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
             observer.onNext(updatedProfile)
             observer.onCompleted()
         }
         
         return Disposables.create()
     }
 }
}

// MARK: - ViewController
class ViewController: UIViewController {
 private let profileView = ProfileUpdateView()
 private let viewModel: ProfileUpdateViewModel
 private let disposeBag = DisposeBag()
 
 init(viewModel: ProfileUpdateViewModel = ProfileUpdateViewModel(userProfile: UserProfile(name: "Test", email: "test@example.com"))) {
     self.viewModel = viewModel
     super.init(nibName: nil, bundle: nil)
 }
 
 required init?(coder: NSCoder) {
     fatalError("init(coder:) has not been implemented")
 }
 
 override func loadView() {
     view = profileView
 }
 
 override func viewDidLoad() {
     super.viewDidLoad()
     print("나왔다")
     setupBindings()
 }
 
 private func setupBindings() {
     // Bind text fields to view model
     profileView.nameTextField.rx.text.orEmpty
         .bind(to: viewModel.name)
         .disposed(by: disposeBag)
     
     profileView.emailTextField.rx.text.orEmpty
         .bind(to: viewModel.email)
         .disposed(by: disposeBag)
     
     
     //        viewModel.isValid
     //            .debug("isValid")
     //            .bind(to: profileView.saveButton.rx.isEnabled)
     //            .disposed(by: disposeBag)
     
     profileView.saveButton.isEnabled = true
     
     // Handle save button tap
     profileView.saveButton.rx.tap
         .debug("Button Tap")
         .flatMapLatest { [weak self] _ -> Observable<UserProfile> in
             guard let self = self else { return .empty() }
             print("Updating profile")
             return self.viewModel.updateProfile()
         }
         .subscribe(onNext: { [weak self] updatedProfile in
             print("Updated Name: \(updatedProfile.name)")
             print("Updated Email: \(updatedProfile.email)")
             self?.showAlert(message: "Profile updated successfully!")
         })
         .disposed(by: disposeBag)
 }
 
 private func showAlert(message: String) {
     let alert = UIAlertController(title: "Success", message: message, preferredStyle: .alert)
     alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
     present(alert, animated: true, completion: nil)
 }
}

ex) 텍스트 필드의 입력 값을 실시간으로 저장하여 다른 곳에서 활용해야하는 경우,
스위치의 온오프 상태를 관리해야하는 경우

Relay 선택은 상황에 따라 달라질 수 있고
필요에 따라 두 가지 Relay를 함께 사용할 수도 있음


Relay가 주요 사용되는 점

  1. UI 업데이트 - 버튼 탭, 텍스트 필드 변경 등의 UI 이벤트 처리

  2. 데이터 바인딩 - 모델의 변경사항을 UI에 실시간으로 반영

  3. 상태 관리 - 앱의 전역 상태나 화면의 로컬 상태 관리

Relay를 사용할 때는 accept(_:) 메소드로 새 값을 전달 -> asObservable()을 통해 Observable로 변환하여 구독할 수 있다.

profile
오늘 배운걸 까먹었을 미래의 나에게..⭐️
post-custom-banner

0개의 댓글