저번에 예제가 너무 단순한거 같아서 좀 더 복잡한걸 가지고 만들어보자.
1. 예제 링크
www.youtube.com/watch?v=iHKBNYMWd5I&t=103s
2. 동기/ 비동기 예제
코드는 위에는 타이머 라벨이 돌고 있고 button을 누르면 indicator가 돌아가면서 json을 로드하는 예제다.
import UIKit
import RxSwift
import SwiftyJSON
let MEMBER_LIST_URL = "https://my.api.mockaroo.com/members_with_avatar.json?key=44ce18f0"
class AsyncJsonViewController: UIViewController {
@IBOutlet weak var timerLabel: UILabel!
@IBOutlet weak var editView: UITextView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
}
func startTimer() {
if #available(iOS 10.0, *) {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
self?.timerLabel.text = "\(Date().timeIntervalSince1970)"
}
} else {
Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.activeTimer), userInfo: nil, repeats: true)
}
}
@objc func activeTimer() {
timerLabel.text = "\(Date().timeIntervalSince1970)"
}
private func setVisibleWithAnimation(_ v:UIView?, _ s: Bool) {
guard let v = v else {return}
UIView.animate(withDuration: 0.3, animations:{ [weak v] in
v?.isHidden = !s
} ,completion:{ [weak self] _ in
self?.view.layoutIfNeeded()
})
}
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.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)
}
}
이를 단순히 동기로 구성하면 위와 같다.
위 상태에서 버튼을 눌러 json을 load하면 타이머 라벨도 멈추고, indicator도 나타나지 않고 json이 다 로딩되어 editView에 들어갈때까지 화면이 멈춘다.
그럼 의도대로 흘러가지 않기 때문에 아래와 같이 비동기로 바꾸자.
@IBAction func onLoad(_ sender: UIButton) {
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)
}
}
}
시간이 오래걸리는 json을 로딩하는 부분을 비동기로 구현하고, UI를 수정하는 부분에서는 메인스레드에서 동작하게 한다.
그러면 원래 의도대로 잘 동작하게 된다.
3. 함수화
위 예제에서 onLoad는 혼자 너무 많은 일을 하고 있다.
그래서 json받는 곳을 함수로 만들어 보자.
private func downloadJson(_ url:String) -> String? { // 함수 선언
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
return json
}
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
DispatchQueue.global().async {
let json = self.downloadJson(MEMBER_LIST_URL) // 함수처리
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
}
그럼 위와 같이 구성할 수 있다.
그런데 굳이 한줄을 위해서 async로 묶는것은 비효율적이다.
따라서 async처리를 downloadJson안에서 해보자.
func downloadJson(_ url:String) -> String? {
DispatchQueue.global().async {
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
return json
}
}
그럼 json을 반환해야하는데 async코드 안에 있어서 반환을 할 수가 없다.
이럴때는 completion으로 클로저(clousure)를 반환해주자.
// @escaping은 본체함수가 끝나고 나중에 실행되는 것을 명시
// 반환타입이 Optional일경우는 escaping이 default라 안써줘도 됌
func downloadJson(_ url:String, completion:@escaping (String?)->Void) {
DispatchQueue.global().async {
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
completion(json)
}
}
}
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
self.downloadJson(MEMBER_LIST_URL) { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
그럼 이제 onLoad함수에서는 비동기 여부를 신경쓰지 않고 json을 받아서 처리 할 수 있다.
4. completion 나중에 처리하기
json로딩을 하나만 하면 좋겠지만 만약에 비슷한 동작을 4번한다고 생각해보자.
그럼 위의 코드가 아래와 같이 바뀔 것 이다.
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
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)
}
}
}
}
}
이러면 코드분리가 너무 어려워진다.
그래서 비동기 데이터를 return으로 받고 나중에 처리하고 싶을때는 아래와 같이 구성하자.
import UIKit
import RxSwift
import SwiftyJSON
let MEMBER_LIST_URL = "https://my.api.mockaroo.com/members_with_avatar.json?key=44ce18f0"
class 나중에생기는데이터<T> {
private let task:(@escaping (T) -> Void) -> Void
init(task: @escaping (@escaping (T) ->Void) ->Void) {
self.task = task
}
func 나중에오면(_ f:@escaping (T) -> Void) {
task(f)
}
}
class AsyncJsonViewController: UIViewController {
@IBOutlet weak var timerLabel: UILabel!
@IBOutlet weak var editView: UITextView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
startTimer()
}
private func startTimer() {
if #available(iOS 10.0, *) {
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
self?.timerLabel.text = "\(Date().timeIntervalSince1970)"
}
} else {
Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.activeTimer), userInfo: nil, repeats: true)
}
}
@objc private func activeTimer() {
timerLabel.text = "\(Date().timeIntervalSince1970)"
}
private func setVisibleWithAnimation(_ v:UIView?, _ s: Bool) {
guard let v = v else {return}
UIView.animate(withDuration: 0.3, animations:{ [weak v] in
v?.isHidden = !s
} ,completion:{ [weak self] _ in
self?.view.layoutIfNeeded()
})
}
private func downloadJson(_ url:String) -> 나중에생기는데이터<String?> {
return 나중에생기는데이터() { f in
DispatchQueue.global().async {
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f(json)
}
}
}
}
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:나중에생기는데이터<String?> = downloadJson(MEMBER_LIST_URL)
json.나중에오면 { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
}
이렇게 구성을 하면 비동기 결과값을 받아놓고 다른 부분에서 수정할 수 있다.
5. 4번같이 동작하는 Utility
먼저 위의 코드를 다음과 같이 바꿔보자.
private func downloadJson(_ url:String) -> 나중에생기는데이터<String?> {
return 나중에생기는데이터() { f in
DispatchQueue.global().async {
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f(json)
}
}
}
}
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
downloadJson(MEMBER_LIST_URL)
.나중에오면 { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
}
5.1 RxSwift
그럼 우리가 실제로 사용할 RxSwift는 위 코드를 어떻게 바꿀지 살펴보자.
RxSwift에서 "나중에생기는데이터"는 "Observable"이라고 사용한다.
또한 "나중에오면"부분은 "subscribe"라고 사용한다.
위 코드를 그럼 RxSwift로 바꿔보자.
private func downloadJson(_ url:String) -> Observable<String?> {
return Observable.create() { f in
DispatchQueue.global().async {
let url = URL(string: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(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
json.subscribe { event in
switch event {
case .next(let json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
}
위와 같이 사용하고 동작시키면 똑같이 동작한다.
RxSwift를 다시 정의하자면 비동기적으로 생기는 데이터를 completion같은 clousure를 통해서 전달하는게 아니라 return값으로 전달하기 위해서 사용하는 Utility다.
6. Disposable
5번과 같이 코드를 작성하면 워닝이 하나 발생하는데 이는 subscribe에서 return되는 disposable을 사용하지 않아 나는 워닝이다.
아래와 같이 써주자.
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
let disposable = json.subscribe { event in
switch event {
case .next(let json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
}
그럼 이 disposable은 뭐에 쓰느냐하면 subscribe동작을 취소할 때 사용한다.
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
let disposable:Disposable = json.subscribe { event in
switch event {
case .next(let json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
disposable.dispose()
}
위와 같이 사용하면 subscribe를 호출하자 마자 dispose()를 시켰기 때문에 json을 로드하지 않고 뺑뺑이만 돌아가는 상황이 연출된다.
var disposable:Disposable? // disposable 변수 선언
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
disposable = json.subscribe { event in
switch event {
case .next(let json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
}
func cancel() {
disposable?.dispose() // 원하는 곳에서 취소
}
이런 현상을 방지하기 위해 버튼을 하나 추가해서 그 버튼 안에서 dispose()를 호출하면 원하는 타이밍에 작업을 취소시킬 수 있다.
이렇게 사용하는 방법도 있고 disposebag을 사용하는 방법도 있다.
var disposeBag = DisposeBag() // disposeBag변수 선언
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
json.subscribe { event in
switch event {
case .next(let json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}.disposed(by: disposeBag) // disposeBag에 추가
}
func cancel() {
disposeBag = DisposeBag() // 원하는 곳에서 취소
}
7.순환참조 문제
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
json.subscribe { event in
switch event {
case .next(let json):
self.editView.text = json // 클로저에서 self의 reference count를 올림
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
}
위 코드를 보면 clousure에서 self를 사용하고 있다.
arc에서 참조를 하게 되면 reference count가 증가하게 되고, 이는 나중에 self가 메모리에 남아있게 되는 원인이 된다.
그래서 코드를 아래와 같이 쓰자.
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
let disposable = json.subscribe {[weak self] event in // weak self를 받아서 사용
switch event {
case .next(let json):
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
disposable.dispose()
}
하지만 이렇게 할 필요가 없는것이 클로저에서 참조 카운터가 증가했으면 클로저가 끝나면 참조 카운터가 감소할 것이다.
그래서 RxSwift에서는 completed가 발생할때 클로저가 끝난다.
private func downloadJson(_ url:String) -> Observable<String?> {
return Observable.create() { f in
DispatchQueue.global().async {
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f.onNext(json)
}
}
return Disposables.create()
}
}
우리가 만들었던 downloadJson의 경우 onNext만 호출하고 있기 때문에 위와 같은 문제가 나타나는데, onComplete도 호출해주면 reference count문제를 해결할 수 있다.
private func downloadJson(_ url:String) -> Observable<String?> {
return Observable.create() { f in
DispatchQueue.global().async {
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f.onNext(json)
f.onCompleted() // complete호출
}
}
return Disposables.create()
}
}
8. 순환참조문제 확인
말로만 순환참조 문제가 난다고 했는데 이를 눈으로 명확하게 보는 방법이 있을까?
아까 코드를 다시 보자
private func downloadJson(_ url:String) -> Observable<String?> {
return Observable.create() { f in
DispatchQueue.global().async {
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f.onNext(json)
//f.onCompleted() // complete호출
}
}
return Disposables.create()
}
}
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
let disposable = json.subscribe {[weak self] event in // weak self를 받아서 사용
switch event {
case .next(let json):
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
}
이렇게 downloadJson함수에서 f.onCompleted()를 주석처리하고 scheme을 수정해보자.
위와 같이 프로젝트 이름을 클릭하면 Edit Scheme...이 나온다.
여기에서 Malloc Scribble과 Malloc Stack에 체크표시를 해준뒤
오른쪽에 빨간색 버튼을 누르면 왼쪽 화면이 나온다.
왼쪽에 보라색 느낌표들이 뜨는데 위 화면들이 메모리 릭이 난 상황이다.
이제 다시 코드로 들어가 f.onCompleted()를 살리고 보면 메모리 릭이 나지 않는다.
9. RxSwift에서 뭐 하고 싶은거냐?
결국은 2가지로 요약될 수 있다.
1. 비동기로 생기는 데이터를 Observable로 감싸서 return
2. Observable로 오는 데이터를 처리
위에 따라서 우리가 했던걸로 매핑해보면 아래와 같다.
//1. 비동기로 생기는 데이터를 Observable로 감싸서 return
private func downloadJson(_ url:String) -> Observable<String?> {
return Observable.create() { f in
DispatchQueue.global().async {
let url = URL(string:url)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async {
f.onNext(json)
//f.onCompleted() // complete호출
}
}
return Disposables.create()
}
}
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
// 2. Observable로 오는 데이터를 처리
let disposable = json.subscribe {[weak self] event in // weak self를 받아서 사용
switch event {
case .next(let json):
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
}
이제 위 코드에서 1번을 간단하게 코드로 정리해보자.
private func returnObservable() -> Observable<String?> {
return Observable.create() { emitter in //Observable 생성
emitter.onNext("Hello") // Hello 전달
emitter.onNext("World") // World 전달
emitter.onCompleted() // 데이터 전달 끝
return Disposables.create() // disposable 생성 후 전달
}
}
비동기로 생기는 데이터를 Observable로 감싸서 return하는 방법은 위와 같다.
이건 간단한 예제고 만약 completion이 들어가면 어떻게 처리할지 생각해보자.
private func downloadJson(_ url:String) -> Observable<String?> {
return Observable.create() { emitter in
let url = URL(string:url)!
let task = URLSession.shared.dataTask(with: url) { (data, _, err) in
guard err == nil else {
emitter.onError(err!) // 에러 처리
return
}
if let dat = data, let json = String(data: dat, encoding: .utf8) {
emitter.onNext(json) // data전달
}
emitter.onCompleted() // complete
}
task.resume()
return Disposables.create() { // dispose 실행되었을 때
task.cancel()
}
}
}
URLSession을 이용해서 data를 가져오는 코드를 Observable로 반환했다.
각각 상황에 맞게 에러가 발생한 경우 onError로 전달되고, data가 제대로 생성되어 jsonString이 나온 경우 onNext로 전달되고, dispose가 실행되면 task가 cancel된다.
위와 같이 구성할 수 있다.
이제 가져다 쓰는 쪽을 생각해보자.
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
// 2. Observable로 오는 데이터를 처리
let disposable = json.subscribe {event in
switch event {
case .next(let json):
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
case .completed:
break
case .error:
break
}
}
}
현재 onNext:로 전달되는 json은 URLSession이 생성한 thread에서 전달이 된다.
그래서 UI수정하는 부분은 main thread에서 동작하게 수정해주어야한다.
@IBAction func onLoad(_ sender: UIButton) {
editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json:Observable<String?> = downloadJson(MEMBER_LIST_URL)
json.subscribe { event in
switch event {
case .next(let json):
DispatchQueue.main.async { // 메인스레드에서 동작
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
case .completed:
break
case .error:
break
}
}.disposed(by: disposeBag)
}
10. Observable의 생명주기
1. Create
2. Subsribe
3. Next
-------이 아래는 끝날때를 의미-------
4. Completed / Error
5. Disposed
위 이벤트들이 찍히는 걸보고 싶으면 .debug()를 호출하면 된다.
downloadJson(MEMBER_LIST_URL)
.debug() // 디버깅
.subscribe { event in
switch event {
case .next(let json):
DispatchQueue.main.async {
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
}
case .completed:
break
case .error:
break
}
}.disposed(by: disposeBag)
간단하게 적자면 Create를 하면서 옆에 생성한 클로저는 Subscribe가 불리면 호출된다.
Subscribe를 시작한 이후로는 Next가 호출될 수도 있고 Error가 호출될 수도 있고 Completed가 호출될 수도 있는데, 여기서 Next를 뺀 것들은 모두 종료를 의미한다.
위 동작이 다 끝나면 Disposed가 호출된다.
'공부 > swift' 카테고리의 다른 글
[RxSwift] JSON파싱해서 테이블뷰에 뿌리기 (2) | 2020.09.17 |
---|---|
[swift] RxSwift Operator(SugarAPI) (0) | 2020.09.15 |
[swift] RxCocoa (0) | 2020.09.09 |
[swift] RxSwift (0) | 2020.09.06 |