RxSwift를 사용한 비동기 프로그래밍 #1
[1교시] 개념잡기 - RxSwift를 사용한 비동기 프로그래밍
textview 에 json 데이터를 띄우는 함수
버튼을 누르면 animation이 실행된다.
@IBAction func onLoad() {
editView.text = ""
setVisibleWithAnimation(activityIndicator, true)
let url = URL(string: MEMBER_LIST_URL)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
해당 코드로 실행을 하면 시간도 멈추고 Animation도 적용이 안된체 데이터만 들고오게 된다. (동기화가 되어있다.)
--> 데이터를 다운로드 받을때 Time 도 멈추고
Animation 도 나오고 Time 도 멈추지 않으면서 json을 다운받고 UI 업데이트 해주려면 어떻게 해야 할까. (비동기로 만들어 준다.)
--> 다른 쓰레드를 만들어 원하는 작업을 동시에 수행한다.
동시에 비동기로 진행하는 DispatchQueue.global.async 로 감싸주고 UI 업데이트 부분은 main 쓰레드에서 처리되도록 감싸준다.
@IBAction func onLoad() {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
DispatchQueue.global().async {
let url = URL(string: MEMBER_LIST_URL)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
}
만약 DispatchQueue를 깔끔하게 비동기 처리 부분에 구현하고 싶다면?
Json 다운받는 부분을 함수로 빼주고 completion handler를 통해 값을 return 해준다.
*@escaping은 써줘야 한다. (optional 일 경우는 default 이므로 명시안해도 된다.
func downloadJson(_ url : String, _ completion: @escaping (String?) -> Void) {
DispatchQueue.global().async {
let url = URL(string: MEMBER_LIST_URL)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
completion(json)
}
}
}
@IBAction func onLoad() {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
Swift에서 이렇게 비동기 적으로 쓰면 된다.
하지만 계속 이런 방식으로 비동기 부분을 개발 할 경우 아래와 같이 여러번의 중첩된 비동기 처리에서 어떤게 먼저 리턴될지
순서를 보장할 수 없게된다. --> 계속 뎁스가 생기면서 관리가 힘들어 진다.
@IBAction func onLoad() {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
self.downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
self.downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
self.downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
self.downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
}
Completion handler 말고 return 값으로 처리하고 싶다!
쉽게 만들어 주는 PromiseKit, Bolt, RxSwift 유틸리티를 사용하면 된다.
class 나중에생기는데이터<T> {
private let task: (@escaping (T) -> Void) -> Void
init(task: @escaping (@escaping (T) -> Void) -> Void) {
self.task = task
}
func 나중에오면(_ f: @escaping (T) -> Void){
tast(f)
}
}
func downloadJson(_ url : String -> 나중에생기는데이터<String?>)
return 나중에 생기는 데이터(){ f in
f(json)
}
let json:나중에생기는데이터<String?> = downloadJson(MEMBER_LIST_URL)
json.나중에오면 { json in
UI뷰 업데이트
}
- Observable : 나중에 생기는 데이터 class
- Observable.create() : 나중에 생기는 데이터 만들때
- Subscribe : 나중에 오면
- onNext : f에 바로 전달하는게 아닌 onNext로 전달
- event : subscribe 하면 나중에 event 가 온다.
- Disposable 로 작업 취소(create 했으면 Disposable 를 리턴 해야함)
disposable.dispose() 로 작업을 바로 취소할 수 있다.
RxSwift를 적용한 코드
func downloadJson(_ url : String)-> Observable<String?> {
return Observable.create() { f in
DispatchQueue.global().async {
let url = URL(string: MEMBER_LIST_URL)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f.onNext(json)
}
}
return Disposables.create()
}
}
@IBAction func onLoad() {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
downloadJson(MEMBER_LIST_URL)
.subscribe { event in
switch event{
case let .next(json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
break
case .completed:
break
case .error(_):
break
}
}
}
- 데이터가 전달 될때는 .next
- 데이터가 다 전달 됬을 때는 .completed
RxSwift 란 비동기적으로 생기는 데이터를 completion 같은 closure 로 전달하는게 아니라 return 값으로 전달하기 위해서 만들어진 유틸리티이다.
RxSwift의 Observable 을 class로 표현하면
class Observable<T>{
private let task : (@escaping (T) -> Void) -> Void
init(task: @escaping (@escaping (T) -> Void) -> Void){
self.task = task
}
func subscribe(_ f: @escaping (T) -> Void){
task(f)
}
}
비동기적으로 발생하는 데이터를 리턴값으로 사용하고 subscribe 메소드로 사용한다.
* 클로져 내에있는 self들이 순환참조 될 수 있다.
{ event in
switch event{
case let .next(json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
break
case .completed:
break
case .error(_):
break
}
}
이땐 f.onComplete 를 통해 클로져를 벗어나면서 해결할 수 있다.
RxSwift 사용방법 2개
- 비동기로 생기는 데이터를 Observable로 감싸서 리턴하는 방법
--> downloadJson 함수 - Observable로 오는 데이터를 받아서 처리하는 방법
--> downloadJson.subscribe 클로져
1. 비동기로 생기는 데이터를 Observable로 감싸서 리턴하기
- Observable.create() 한다.
- onNext로 데이터를 받는다.
- 데이터 다 받으면 onComplete()
- 마지막에 Disposables.create()
func downloadJson(_ url : String)-> Observable<String?> {
//1. 비동기로 생기는 데이터를 Observable 로 감싸서 리턴하는 방법
return Observable.create(){emitter in
emitter.onNext("Hello")
emitter.onNext("World")
emitter.onCompleted()
return Disposables.create()
}
}
그럼 함수의 event 에 Hello 와 World 가 오니까 최종적으로 World 만 찍혀있다.
URLSession task로 처리하면
func downloadJson(_ url : String)-> Observable<String?> {
//1. 비동기로 생기는 데이터를 Observable 로 감싸서 리턴하는 방법
return Observable.create(){emitter in
let url = URL(string: MEMBER_LIST_URL)!
let task = URLSession.shared.dataTask(with: url){(data,_,err) in
//err는 nil 이여야 하닌데 아닐땐 onError
guard err == nil else{
emitter.onError(err!)
return
}
//데이터가 제대로 왔다면 onNext로 Json 접근
if let data = data , let json = String(data: data, encoding: .utf8){
emitter.onNext(json)
}
//다 받았을 경우 끝
emitter.onCompleted()
}
task.resume()
//중간에 cancel 하면 task를 Cancel 한다.
return Disposables.create() {
task.cancel()
}
}
}
URLSession 자체가 Main 쓰레드가 아닌 다른 쓰레드에서 처리되기 때문에 함수 호출 부분인 downloadJson 의 subscribe 부분 또한 URLSession과 같은 쓰레드에서 동작한다. 그래서 Main 쓰레드에서 UI 를 업데이트 하지 않기 때문에 에러가 발생한다.
- Observable을 create 한다.
- 적절한 시점에 emitter의 err를 호출하거나 onNext로 데이터를 받거나 onComplete 으로 종료시킨다.
- 취소되었을때 수행해야되는 코드가 있다면 Dispsables.create(){ 코드구현 } 한다.
Observable 의 생명주기
- Create
- Subscribe
- onNext
-----------끝---------- - onCompleted / onError
- Disposed
Observable은 subscribe 부분에서 실행이 된다.
downloadJson() 만으로 실행되지 않고 downloadJson().subscribe() 해야 실행이 된다.
동작이 끝난 Observable 은 재사용하지 못한다.
let ob = downloadJson(MEMBER_LIST_URL)
let disp = ob.subscribe ...
disp.dispose() //동작이 다 끝나고 없어짐
ob.동작 해도 수행안됨
ob.subscribe을 다시 해야함
debug()로 생명주기 확인
downloadJson(MEMBER_LIST_URL)
.debug()
.subscribe
디버그 찍어주면 subscribe에서 어떤 데이터가 왔는지 확인할 수 있다.
2020-05-27 23:45:07.460: ViewController.swift:93 (onLoad()) -> subscribed
2020-05-27 23:45:08.890: ViewController.swift:93 (onLoad()) -> Event next(Optional("데이터들"))
2020-05-27 23:45:08.949: ViewController.swift:93 (onLoad()) -> Event completed
2020-05-27 23:45:08.949: ViewController.swift:93 (onLoad()) -> isDisposed
2. Observable로 오는 데이터를 받아서 처리하는 방법
- observable 을 만든다.
- subscribe로 이벤트를 받는다.
- 이벤트의 next err complete 을 처리한다
- 필요에 따라 취소시킨다.
let observable = downloadJson(MEMBER_LIST_URL)
let disposable = observable.subscribe{ event in
switch event{
case .next(let json):
break
case .error(let err):
break
case .completed:
break
}
}
disposable.dispose() // 로 필요에 따라 취소시킬 수 있다.
Sugar API - RxSwift를 더욱 쉽게 쓸 수 있는 API
굉장히 많다.
- Observable.just
- 어차피 데이터 하나 보낼 꺼면 간단하게 하자
- dispose 되었을 때 따로 처리가 없을 때
func downloadJson(_ url : String)-> Observable<String?> {
return Observable.just("Hello World")
// return Observable.create { emitter in
// emitter.onNext("Hello world")
// emitter.onCompleted()
// return Disposables.create()
// }
}
* 근데 데이터 두개 보내고 싶을 땐 : 배열을 써라
func downloadJson(_ url : String)-> Observable<[String?]> {
return Observable.just(["Hello", "World"])
// return Observable.create { emitter in
// emitter.onNext("Hello world")
// emitter.onCompleted()
// return Disposables.create()
// }
}
* 근데 배열 한꺼번에 말고 하나씩 보내고 싶을 때 : from
func downloadJson(_ url : String)-> Observable<String?> {
return Observable.from(["Hello", "World"])
// return Observable.create { emitter in
// emitter.onNext("Hello world")
// emitter.onCompleted()
// return Disposables.create()
// }
}
- subscribe 도 한줄로 onNext만 받아서 처리할 수 있다.
_ = downloadJson(MEMBER_LIST_URL)
.subscribe(onNext : {print($0)})
_ = downloadJson(MEMBER_LIST_URL)
.subscribe(
onNext : {print($0)},
onError: {print("error")}
onCompleted: {print("complete")}
)
- Dispatch.main 으로 감싸주기 귀찮을 때
downloadJson(MEMBER_LIST_URL)
.observeOn(MainScheduler.instance)
.subscribe ( onNext: {json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
})
- just from 생성을 할때 제공하는 suger
- next, onError 처리할 때 제공하는 suger
- observe 에서 subscribe로 데이터가 전달되는 중간에 데이터를 바꿔치기하는 operator suger
- map, filter, 등등 겁나많다.
downloadJson(MEMBER_LIST_URL)
.map{json in json?.count ?? 0} // json이 count로 바껴서 내려오고
.filter{cnt in cnt > 0} // 필터에서 0보다 큰 값만 걸러지고
.map{"\($0)"} // String 으로 바뀐 다음
.observeOn(MainScheduler.instance) // Main Thread 로 전환된다.
.subscribe ( onNext: {json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
})
문서 보는법: 그림을 통해 데이터의 변환이나 operator 의 동작을 이해해라.
- 빨간 동그라미는 데이터 이다.
- 데이터를 Just 에 넣으면
- Observable이 생기고 (직선) 빨간데이터가 전달되고 complete 된다(세로 작대기)
- array가 from 에 전달되면
- observable 이 생성되고 순서대로 전달이 끝나면 complete 된다.
- observable 과 observable 사이에서 쓸 수 있다.
- 변환 공식에 따라 바뀐 후 전달된다.
- 직선의 색은 Thread 의 종류를 나타낸다.
- ObserveOn하면 Thread 가 바뀐다.
- 파랑 쓰레드에서 observeOn(주황색) 하면 주황 쓰레드로 바뀐다.
- 주황으로 바뀐후 map 을 해도 쓰레드의 종류는 바뀌지 않는다.
- subscribeOn(파란색) 은 맨 처음 쓰레드에 영향을 준다. (첫째줄)
- observeOn(핑크)로 다시 핑크 쓰레드로 바뀐다.
downloadJson(MEMBER_LIST_URL)
.map{json in json?.count ?? 0}
.filter{cnt in cnt > 0}
.map{"\($0)"}
.observeOn(MainScheduler.instance) // 메인쓰레드로 바뀌고
.subscribeOn(ConcurrentDispatchQueueScheduler(qos:.default)) //default
.subscribe ( onNext: {json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
})
- default qos를 가지는 Thread에서 처음부터 실행이 된다.
- observeOn에서 Main Thread 가 바뀌고 나서 subscribe가 Main Thread 에서 실행된다.
- subscribeOn의 위치는 어디든 상관없다. (처음 동작의 쓰레드를 지정하기 때문에)