Signle은 RxSwift의 Traits로 하나의 이벤트만 방출하는 Observable이다.
Single에는 failure, success라는 2가지 이벤트가 존재한다.
- success는 일반 Observable의 next + completed와 같은 역할이고
- failure은 error이벤트와 같은 역할이다.
다음과 같이 초기에 한번만 이루어지는 네트워크 요청에 자주 사용된다.
static func featchAllData(_ url: String) -> Single<Data> {
return Single<Data>.create { single in
URLSession.shared.dataTask(with: URL(string: url)!) { data, res, err in
if let err = err {
single(.failure(err))
return
}
guard let data = data else {
let httpsResponse = res as! HTTPURLResponse
single(.failure(NSError(domain: "no data",
code: httpsResponse.statusCode,
userInfo: nil)))
return
}
single(.success(data))
}.resume()
return Disposables.create()
}
}
Single With Share
Single과 같은 Cold Observable은
Subscriber 수에 따라 스트림이 생성되기 때문에,
중복적으로 네트워크 요청을 하는 경우가 발생한다.
그렇기 때문에, share Operator를 사용하거나,
자체적으로 share가 가능한 Driver가 Signal을 사용한다.
Driver와 Signal의 share에 대한 자세한 애기는 해당 포스팅에서 볼 수 있다.
source.share(replay: 1, scope: .whileConnected) // Driver
source.share(scope: .whileConnected) // Signal
다음과 같은 네트워크 요청이 있다고 가정해보자.
struct Repository {
func fetchUser() -> Single<User> {
return Single.create { emitter in
emitter(.success(User(id: 1, name: "jung", age: 26)))
return Disposables.create()
}.debug("User")
}
}
이를 ViewModel로 가져와서 UI업데이트를 하기 위해 Driver로 가공을 한 후
final class ViewModel {
let dependency = Repository()
let userName: Driver<String>
let userAge: Driver<Int>
init() {
let user = dependency
.fetchUser()
.asDriver(onErrorJustReturn: User(id: 0, name: "", age: 0))
userName = user.map { $0.name }
userAge = user.map { $0.age }
}
}
ViewController에서 다음과 같이 Binding을 한다고 가정하자.
class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
private func bind() {
viewModel.userName
.drive(with: self) { owner, name in
owner.nameLabel.text = name
}
.disposed(by: disposeBag)
viewModel.userAge
.drive(with: self) { owner, age in
owner.ageLabel.text = "\(age)"
}
.disposed(by: disposeBag)
}
}
해당 코드의 동작을 예상하자면,
let user = dependency
.fetchUser()
.asDriver(onErrorJustReturn: User(id: 0, name: "", age: 0))
userName = user.map { $0.name }
userAge = user.map { $0.age }
다음과 같이 user이라는 Driver는 userName과 userAge가 map하는 형태로 구현을 했기 때문에,
user의 하나의 공유 스트림으로 userName과 userAge로 이벤트가 방출될 것이다.
하지만 이를 debug해보면,
하나의 공유 스트림으로 userName과 userAge로 이벤트가 방출될 것이 아닌
다음과 같이 userName과 userAge각각 스트림이 생성되는 것을 볼 수 있다.
/*
User -> subscribed
User -> Event next(User(id: 1, name: "jung", age: 26))
User -> Event completed
User -> isDisposed
User -> subscribed
User -> Event next(User(id: 1, name: "jung", age: 26))
User -> Event completed
User -> isDisposed
*/
이러한 이유는 Single의 경우 이벤트를 방출하자마자 complete이벤트 방출하여 stream이 끊기게 된다.
이는 결국 user의 공유 스트림 역시 끊기게 되며,
map으로 연결된 Driver 역시 disposed되는 결과가 발생한다.
즉, Single과 share 연산자를 사용하게 되면,
결과적으로 공유되지 않고 여러 스트림이 생기게 된다.
특히나, Driver의 경우는 끊기지 않는 UI작업을 위해 사용한 Driver가 소용없게 된다.
해결 방법
Relay 사용해주기
첫번째 방법으로는 중간 스트림을 Relay로 변환해주는 것이다.
Relay는 error뿐만 아닌, completed 이벤트도 무시하기 때문에
Single에서 completed로 스트림이 끊기더라도 Relay는 이를 무시하게 된다.
final class ViewModel {
let dependency = Repository()
let disposeBag = DisposeBag()
let userName: Driver<String>
let userAge: Driver<Int>
private let user = BehaviorRelay<User>(value: User(id: 0, name: "", age: 0))
init() {
userName = user.asDriver().map { $0.name }
userAge = user.asDriver().map { $0.age }
dependency
.fetchUser()
.subscribe(with: self) { owner, userInfo in
owner.user.accept(userInfo)
}
.disposed(by: disposeBag)
}
}
다음과 같이 공유 스트림이 제대로 동작하는 것을 볼 수 있으며,
Driver 역시 Disposed되지 않는다.
/*
User -> subscribed
User -> Event next(User(id: 1, name: "jung", age: 26))
User -> Event completed
User -> isDisposed
*/
하지만 위와 같은 방식은 한가지 문제점이 존재한다.
해당 데이터를 실제로 구독하게 될 구간은 ViewController이지만,
시퀀스가 유지되지 않고 ViewModel에서 한번 끊기고
user라는 Relay를 통해 다시 방출해주게 된다.
Signle 끊기지 않게 변환해주기
해당 방법은 concat 연산자와 never 연산자를 사용한다.
concat연산자는 두 Observable Sequence를 연결해준다.never연산자는 어떠한 항목도 방출하지 않고, 스트림이 종료되지도 않는 Observable을 생성한다.
public extension Single {
func ignoreTerminate() -> Observable<Element> {
return self.asObservable().concat(Observable.never())
}
}
concat 연산자는 앞의 Observable이 completed되어야만
뒤의 Observable 이벤트 방출을 시작한다.
따라서, Single이 completed되면,
아무런 이벤트를 발생하기 않고 스트림이 종료되지 않는 Observable로 전환되기 때문에
스트림이 끊기지 않게 된다.
final class ViewModel {
...
init() {
let user = dependency
.fetchUser()
.ignoreTerminate()
.asDriver(onErrorJustReturn: User(id: 0, name: "", age: 0))
userName = user.map { $0.name }
userAge = user.map { $0.age }
}
}
다음과 같이 공유 스트림이 제대로 동작하는 것을 볼 수 있으며,
Driver 역시 Disposed되지 않는다.
/*
User -> subscribed
User -> Event next(User(id: 1, name: "jung", age: 26))
User -> Event completed
User -> isDisposed
*/
또한, Relay를 사용한 것과 다르게 하나의 Stream으로 ViewController까지 데이터가 전달된다.
Reference
'iOS > RxSwift' 카테고리의 다른 글
[RxSwift] Relay vs. Signal vs. Driver (0) | 2023.07.25 |
---|---|
[RxSwift] Operator(4) - Share (0) | 2023.07.24 |
[RxSwift] Operator(3) - Combining (0) | 2023.01.08 |
[RxSwift] Traits (0) | 2022.12.28 |
[RxSwift] Operator(2) - Transforming (2) | 2022.12.27 |