Promise, Rx 등 비동기 처리를 하기 위한 방법은 많다. 이를 배워보기 이전에, 왜 그러한 개념이 나왔는지, 어떠한 방식으로 개선해왔는지를 코드를 고쳐보면서 이해해보는 것이 이 포스팅의 목표이다.
import RxSwift
import SwiftyJSON
import UIKit
let MEMBER_LIST_URL = "https://my.api.mockaroo.com/members_with_avatar.json?key=44ce18f0"
class ViewController: UIViewController {
@IBOutlet var timerLabel: UILabel!
@IBOutlet var editView: UITextView!
override func viewDidLoad() {
super.viewDidLoad()
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
self?.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()
})
}
// MARK: SYNC
@IBOutlet var activityIndicator: UIActivityIndicatorView!
@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)
}
}
String(data: encoding:)
의 경우 동기 방식으로 데이터를 가져오기 때문에, UI Update를 할 수 없어, 모든 화면이 멈춘뒤, 데이터를 받은 뒤에 업데이트가 된다.
@IBAction func onLoad() {
self.editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
DispatchQueue.global().async { [weak self] in
let url = URL(string: MEMBER_LIST_URL)!
let data = try! Data(contentsOf: url)
let json = String(data: data, encoding: .utf8)
DispatchQueue.main.async { [weak self] in
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
}
}
}
weak self
를 사용해서 순환참조를 방어해주었다.private 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() {
self.editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
downloadJson(url: MEMBER_LIST_URL) { [weak self] json in
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
}
}
@escaping
키워드를 사용해야 한다.@escaping
이 기본 동작이라고 한다.let json = downloadJson(url)
self.editView.text = json
class 나중에생기는데이터<T> {
// 어떠한 타입을 받아서 Void를 리턴하는 클로저를 인수로 갖는 클로저
// 안쪽에 들어가는 클로저가 후에 데이터를 다 받으면 수행할 completion handler의 역할을 한다.
private let task: (@escaping (T) -> Void) -> Void
init(task: @escaping (@escaping (T) -> Void) -> Void) {
self.task = task
}
func 나중에오면(_ f: @escaping (T) -> Void) {
task(f)
}
}
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() {
self.editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let json: 나중에생기는데이터<String?> = downloadJson(url: MEMBER_LIST_URL)
json.나중에오면 { [weak self] json in
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
}
}
나중에 생기는 데이터
라는 타입 자체의 이름을 어떻게 명명하느냐에 따라 다양한 프레임워크가 발생한다.나중에생기는데이터 = Observable
, 나중에오면 = Subscribe
로 명명한다.리액티브 프로그래밍은 데이터 흐름(data flows)과 변화 전파에 중점을 둔 프로그래밍 패러다임(programming paradigm)이다.
이것은 프로그래밍 언어로 정적 또는 동적인 데이터 흐름을 쉽게 표현할 수 있어야하며, 데이터 흐름을 통해 하부 실행 모델이 자동으로 변화를 전파할 수 있는 것을 의미한다.
Rx = Observable + Observer + Schedulers
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() {
self.editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
let disposable = downloadJson(url: MEMBER_LIST_URL)
.subscribe { [weak self] event in
switch event {
case .next(let json):
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
case .error(let error):
print(error)
case .completed:
break
}
}
// disposable.dispose()
}
dispose
라는 메서드를 가지고 있는데, 이는 위의 정의한 subscribe동작이 다른 스레드에서 끝나지 않았어도 취소할 수 있다.let disposable
라인을 실행시키고 바로 disposable.dispose()
가 실행되어 네트워크 통신을 취소시켜버려 아무런 동작도 하지 않는다.viewWillDisappear
에 추가하여 사용하면 뷰가 변경될 때 취소시키는 효과를 얻을 수 있다.두가지를 배울 것이다.
private func downloadJson(url: String) -> Observable<String?> {
Observable.create { emitter in
emitter.onNext("Hello")
emitter.onNext("world")
emitter.onCompleted()
return Disposables.create()
}
}
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, response, error in
guard error == nil else {
emitter.onError(error!)
return
}
if let data = data, let json = String(data: data, encoding: .utf8) {
emitter.onNext(json)
}
emitter.onCompleted()
}
task.resume()
return Disposables.create() {
task.cancel()
}
}
}
@IBAction func onLoad() {
self.editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
_ = downloadJson(url: MEMBER_LIST_URL)
.subscribe { [weak self] event in
switch event {
case .next(let json):
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
case .error(let error):
print(error)
case .completed:
break
}
}
}
@IBAction func onLoad() {
self.editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
_ = downloadJson(url: MEMBER_LIST_URL)
.subscribe { [weak self] event in
switch event {
case .next(let json):
DispatchQueue.main.async {
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
}
case .error(let error):
print(error)
case .completed:
break
}
}
}
여기서 알아야 하는 점은, 아까도 말했지만, Create 되었다고 동작하는게 아니다. Subscribe가 되었을 때 동작한다. 즉, 구독을 실행할 때 데이터들이 생성되서 전달되는 것. debug()
함수를 추가해서 동작을 확인할 수 있다.
@IBAction func onLoad() {
self.editView.text = ""
self.setVisibleWithAnimation(self.activityIndicator, true)
_ = downloadJson(url: MEMBER_LIST_URL)
.debug()
.subscribe { [weak self] event in
switch event {
case .next(let json):
DispatchQueue.main.async {
self?.editView.text = json
self?.setVisibleWithAnimation(self?.activityIndicator, false)
}
case .error(let error):
print(error)
case .completed:
break
}
}
}
2021-09-22 13:11:23.037: ViewController.swift:109 (onLoad()) -> subscribed
2021-09-22 13:11:24.252: ViewController.swift:109 (onLoad()) -> Event next(Optional("[{\"id\":1,\"name\":\"Gladys Brugden\",\"avatar\":\"https://robohash.org/a ....
2021-09-22 13:11:24.279: ViewController.swift:109 (onLoad()) -> Event completed
2021-09-22 13:11:24.279: ViewController.swift:109 (onLoad()) -> isDisposed
[weak self]
를 명시적으로 적는게 나아보인다.