apple/UIKit & ReactiveX
[iOS] RxDelegateProxy + WebSocket (Starscream)
lgvv
2022. 1. 17. 01:20
[iOS] RxDelegateProxy + WebSocket (Starscream)
인턴을 하고 있는데, 기존 사이드 프로젝트에서 Starscream을 많이 다루어서 Rx로 변환하는 과정을 담당.
RxStascream의 Rx 버전 문제로 인하여 직접 proxy로 만들어주어야 하는 상황.
개발환경
- Xcode 13.0
- Swift 5
- CocoaPod
- Differentiator (3.1.0)
- RxCocoa (4.5.0):
- RxSwift (>= 4.4.2, ~> 4.4)
- RxDataSources (3.1.0):
- Differentiator (~> 3.0)
- RxCocoa (~> 4.0)
- RxSwift (~> 4.0)
- RxSwift (4.5.0)
- SnapKit (5.0.1)
- Starscream (4.0.4)
- Then (2.7.0)
Starscream 반드시 4.0.0이상 버전으로 사용. 3.x 버전과 아주 큰 차이로 proxy를 사용할 수 없을 수도 있으니 주의할 것.
🟠 아래는 예제코드
기존에 methodInvoked를 이용한 proxy 만드는 방법은 다들 보았고, 크게 어렵지 않을탠데, Websocket은 그럴 수 없어서 약간의 트릭을 이용했는데, 한번 보도록 하자
✅ WebSocketDelegateProxy.Swift
import Foundation
import RxSwift
import RxCocoa
import Starscream
public class RxWebSocketDelegateProxy: DelegateProxy<WebSocket, WebSocketDelegate>, DelegateProxyType, WebSocketDelegate {
var subject = PublishSubject<WebSocketEvent>()
public func didReceive(event: Starscream.WebSocketEvent, client: WebSocket) {
subject.onNext(event)
print(event)
}
public static func currentDelegate(for object: WebSocket) -> WebSocketDelegate? {
return object.delegate
}
public static func setCurrentDelegate(_ delegate: WebSocketDelegate?, to object: WebSocket) {
object.delegate = delegate
}
public static func registerKnownImplementations() {
self.register { inputViewController -> RxWebSocketDelegateProxy in
RxWebSocketDelegateProxy(parentObject: inputViewController, delegateProxy: self)
}
}
}
extension WebSocket: ReactiveCompatible { } // WebSocket을 Rx로 사용할 수 있게 바꿔줌
extension Reactive where Base: WebSocket {
public var response: Observable<WebSocketEvent> {
return RxWebSocketDelegateProxy.proxy(for: base).subject
}
public var text: Observable<String> {
return self.response
.filter {
switch $0 {
case .text:
return true
default:
return false
}
}
.map {
switch $0 {
case .text(let message):
return message
default:
return String()
}
}
}
public var connected: Observable<Bool> {
return response
.filter {
switch $0 {
case .connected, .disconnected:
return true
default:
return false
}
}
.map {
switch $0 {
case .connected:
return true
default:
return false
}
}
}
public func write(data: Data) -> Observable<Void> {
return Observable.create { sub in
self.base.write(data: data) {
sub.onNext(())
sub.onCompleted()
}
return Disposables.create()
}
}
public func write(ping: Data) -> Observable<Void> {
return Observable.create { sub in
self.base.write(ping: ping) {
sub.onNext(())
sub.onCompleted()
}
return Disposables.create()
}
}
public func write(string: String) -> Observable<Void> {
return Observable.create { sub in
self.base.write(string: string) {
sub.onNext(())
sub.onCompleted()
}
return Disposables.create()
}
}
}
여기 부분의 코드는 WebSocket을 Proxy로 감싸는 부분이야.
🟠 WebSocketManager.swift
import Foundation
import RxSwift
import RxCocoa
import Starscream
class WebSocketManager {
static let shared = WebSocketManager()
var webSocket: WebSocket!
init() {
print("WebSocketManager init")
setupWebSocket()
}
func setupWebSocket() {
print("setupWebSocket function")
let url = "{Your Server}"
var request = URLRequest(url: URL(string: url)!)
request.addValue("Your Server", forHTTPHeaderField: "Origin")
request.timeoutInterval = 10
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
webSocket = WebSocket(request: request)
}
func connect() {
print("connect")
webSocket.connect()
}
func disconnect() {
print("disconnect")
webSocket.disconnect()
}
func write(_ text: String) {
print("write")
webSocket.write(string: text)
}
}
Decoding 부분을 따로 두고 있고, 최근에 MVVM + Rx 좋은 코드들에 비하면 잘 작성한 코드는 아니지만 학습하면서 이렇게 작성.
🟠 ViewModel
// ViewModel
self.responseDriver = WebSocketManager.shared
.webSocket.rx.response
.asDriver(onErrorJustReturn: .error(nil))
위 코드처럼 사용.
🟠 ViewController
// ViewController
viewModel.responseDriver
//.debug("outputDriver VC")
.drive ( onNext: {
switch $0 {
case .connected:
print("커넥트 성공한다면 나는 정말정말 좋을거 같아요.~_~")
// TODO
case .disconnected(let text, _):
print("디스커넥트도 과연 성공... 했습니다.\(text)")
case .text(let text):
print("메시지가 날아왔을까요? \(text)")
// 디코더로 보내자.
self.viewModel.decodeSubject.onNext(text)
case .cancelled:
print("취소")
default: break
}
})
.disposed(by: bag)
ViewController에서는 이렇게 사용하면 데이터를 받을 수 있음.