본문 바로가기

공부/swift

[swift] RxSwift

1. Rx란?

Reactive X.

홈페이지에는 이렇게 적혀있다.

An API for asynchronous programming
with observable streams

Async한 프로그래밍한다 observe가 가능한 stream들로.

뭔소린지 하나도 모르겠으니 예제랑 같이 봐야겠다.

 

2. 동기/비동기 예제 

출처는 www.youtube.com/watch?v=w5Qmie-GbiA&feature=youtu.be

2.1 동기(Sync) 예제

이미지 로딩과 타이머로 숫자 1씩 증가해서 라벨에 text넣는 것을 같이 해볼 예정

func startTimer() {        
   if #available(iOS 10.0, *) {
      Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in                
         self.counter += 1
         self.counterLabel.text = "\(self.counter)"
      }
   } else {            
      Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(self.activeTimer), userInfo: nil, repeats: true)
   }
}


@objc func activeTimer() {        
   counter += 1
   counterLabel.text = "\(counter)"
}


@IBAction func syncAction(_ sender: UIButton) {        
   imageView.image = loadImage(from: IMAGE_URL)
}


private func loadImage(from imageUrl:String) -> UIImage? {        
   guard let url = URL(string: imageUrl) else {            
      return nil
   }
   guard let data = try? Data(contentsOf: url) else {            
      return nil
   }                
   return UIImage(data: data)    
}

앱이 시작하면 타이머를 돌리고 그와 동시에 동기 버튼을 클릭하면 Image를 불러온다.

동기기 때문에 카운터가 올라가다가 이미지 로딩이 불려오면 카운터가 멈추고 이미지 로딩이 다 끝나면 카운터가 다시 동작하기 시작한다.

이렇게 동작하면 UI가 멈춘것처럼 보이기 때문에 이게 앱이 로딩인건지 뻗어버린건지 구분이 안간다.

그래서 이런 작업을 할 때는 비동기를 사용한다.

 

2.2. 비동기(Async) 예제

@IBAction func asyncAction(_ sender: UIButton) {        
   DispatchQueue.global().async {            
      let image = self.loadImage(from: self.IMAGE_URL)            
      DispatchQueue.main.async {                
         self.imageView.image = image            
      }        
   }    
}

아까랑 같은 코드를 이번엔 스텝을 나누었다.

그리고 DispatchQueue를 통해 async로 동작하게 만들었다.

이 때 주의할 점은 UI를 다루는 작업은 mainThread에서 해야한다.

따라서 DispatchQueue.main을 통해 이미지 뷰에 이미지를 넣는 작업을 하고 있다.

 

위와 같이 작업하면 카운터는 카운터대로 올라가고 이미지를 불어와서 넣는 작업은 그 작업대로 동작하여 사용자가 UI가 멈춘 인상을 받지 않는다.

 

3. RxSwift예제

그래서 저 예제를 RxSwift로 돌리면 어떨까?

private func loadImage(from imageUrl:String) -> UIImage? {        
   guard let url = URL(string: imageUrl) else {            
      return nil        
   }        
   guard let data = try? Data(contentsOf: url) else {            
      return nil        
   }                
   return UIImage(data: data)    
}

일단 이 이미지를 로딩하는 부분부터 바꿔보자.

func rxswiftLoadImage(from imageUrl:String) -> Observable<UIImage?> {        
   return Observable.create { seal in            
      asyncLoadImage(from: imageUrl) { image in // async로 이미지 로딩                
         seal.onNext(image)                
         seal.onCompleted()            
      }            
      return Disposables.create()        
   }    
}

그리고 이걸 사용하는 부분도 바꿔보자.

@IBAction func loadImageAction(_ sender: UIButton) {        
   imageView.image = nil
   _ = rxswiftLoadImage(from: LARGER_IMAGE_URL) // 이미지 불러오는 함수 호출
       .observeOn(MainScheduler.instance) // 메인에서 동작(UI작업을 위함)
       .subscribe({ result in // 구독 시작
          switch result {            
          case let .next(image): // stream에서 넘어오는 marble하나하나, 지금은 이미지 하나 호출해서 한번만 불림
             self.imageView.image = image            
          case let .error(err): // 에러가 발생할 때
             print(err.localizedDescription)            
          case .completed: // 동작이 완료되었을 때                
             print("completed")                
             break            
          }        
       })
}      

위와 같이 바꿀 수 있다.

Scheduler는 OperationQueue를 wrapping한 것임.

 

4. 동작을 중간에 취소하는 법

4.1. Disposable을 이용

var disposable:Disposable? // 디스포즈 가능한 변수 선언


@IBAction func loadImageAction(_ sender: UIButton) {        
   imageView.image = nil                
   disposable = rxswiftLoadImage(from: LARGER_IMAGE_URL) // 이미지 불러오는 함수 호출
      .observeOn(MainScheduler.instance) // 메인스레드에서 동작
      .subscribe({ result in // 구독 시작            
      switch result {            
      case let .next(image): // stream에서 넘어오는 marble하나하나, 지금은 이미지 하나 호출해서 한번만 불림
         self.imageView.image = image            
      case let .error(err): // 에러가 발생할 때                
         print(err.localizedDescription)            
      case .completed: // 동작이 완료되었을 때                
         print("completed")                
         break            
      }        
   })
}

수행 결과를 disposable로 받음

@IBAction func cancelAction(_ sender: UIButton) {        
   disposable?.dispose()    
}

해당 변수에 dispose()를 실행

 

4.2. DisposableBag을 이용

var disposeBag = DisposeBag()


@IBAction func loadImageAction(_ sender: UIButton) {        
   imageView.image = nil                
   rxswiftLoadImage(from: LARGER_IMAGE_URL)            
      .observeOn(MainScheduler.instance)            
      .subscribe({ result in            
      switch result {            
      case let .next(image):                
         self.imageView.image = image            
      case let .error(err):                
         print(err.localizedDescription)            
      case .completed:                
         print("completed")                
         break            
      }            
   }).disposed(by: disposeBag) // 여기서 디스포즈 백에 담음    
}

dispose 변수로 받던것을 .dispose(by : bag)으로 수정

@IBAction func cancelAction(_ sender: UIButton) {        
   disposeBag = DisposeBag() // 디스포즈백 안에 있는 모든 것을 초기화, 취소된다.    
}

그리고 disposeBag을 초기화해주면 해당 동작이 취소 된다.

 

5. Just

just는 데이터를 전달하면 그게 바로 다음줄로 보낸다.

홈페이지에는 이렇게 써있다.

"객체 하나 또는 객채집합을 Observable로 변환한다. 변환된 Observable은 원본 객체들을 발행한다."

create어쩌구 하기 귀찮아서 없앨 수 있음.

   Observable.just("Hello World") // just는 데이터를 넣어주면 바로 전달이 된다.
        .subscribe(onNext: { str in // "Hello World"가 들어옴
            print(str) // "Hello World"를 출력
        })
        .disposed(by: disposeBag)
   Observable.just(["Hello", "World"]) // ["Hello", "World"]이 들어감
        .subscribe(onNext: { arr in // ["Hello", "World"]를 arr로 받음
            print(arr) // ["Hello", "World"] 출력
        })
        .disposed(by: disposeBag)

 

6. From

데이터에 있는 것을 하나씩 꺼내온다.

데이터를 스트림으로 보는것이다.

홈페이지에는 이렇게 써있다.

"다른 객체나 자료 구조를 Observable로 변환한다."

Observable.from(["RxSwift", "In", "4", "Hours"]) // Observable "RxSwift", "In", "4", "Hours"가 만들어진다.
        .subscribe(onNext: { str in // 하나씩 가져와서
            print(str) // 출력한다.
        })
        .disposed(by: disposeBag)
    }

만약에 중간에 자료형 다른게 들어가면 어떻게 될까?

Observable.from(["RxSwift", "In", 4, "Hours"]) // Observable "RxSwift", "In", 4, "Hours"가 만들어진다.
        .subscribe(onNext: { str in // 하나씩 가져와서
            print(str) // 출력한다.
        })
        .disposed(by: disposeBag)
    }

동일하게 출력된다.

swift arr에서 위가 가능하기 때문에 이렇게 쓸 수 있다.

 

여기에서 from으로 전달하는 데이터들을 stream이라고 한다.

이 쯤오면 RxSwift의미 중에서 observe 가능한 stream이 이해가 될것이다.

observe가 가능한 stream들로 Async한 프로그래밍한다.

이런걸 하기 위해 RxSwift사용한다고 보면 된다.

 

7. Map

홈페이지에는 이렇게 적혀있다.

"Observable이 배출한 항목에 함수를 적용한다"

swift고차함수 map 생각하면 된다.

   Observable.just("Hello") // "Hello"가 내려감
        .map { str in "\(str) RxSwift" } // str에 Hello가 들어가서 "Hello RxSwift"가 됌
        .subscribe(onNext: { str in // "Hello RxSwift"받음
            print(str) // 출력
        })
        .disposed(by: disposeBag)

너무 간단하니 from을 사용해서 여러개를 넣어보자.

   Observable.from(["with", "곰튀김","RxSwift"]) // Observable 3개가 생김, 하나씩 내림, 이걸 stream이라고 한다.
        .map { $0.count } // 카운트 연산
        .subscribe(onNext: { str in // 카운트를 str로 받아서
            print(str) // 출력
        })
        .disposed(by: disposeBag)

From을 통해서 각각 "with", "곰튀김", "RxSwift"가 Observable로 내려갈거고

map을 통해서 각각 카운트인 4,3,7이 내려갈거고

subscribe의 OnNext에 4, 3, 7이 하나씩 전달되어 print한다.

그래서 결과는

4

3

7

이 나온다.

 

8. Filter

홈페이지에는 아래와 같이 써있다.

"테스트 조건을 만족하는 항목들만 배출한다"

   Observable.from([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) // Observable한 1~10 생성
        .filter { $0 % 2 == 0 } // 짝수만 넘김
        .subscribe(onNext: { n in // 짝수를 받아서
            print(n) // 출력
        })
        .disposed(by: disposeBag)

from을 통해서 1~10을 받은뒤

filter로 짝수만 걸러 받은 뒤에

하나씩 onNext에서 받아서 출력한다.

2

4

6

8

10

이 출력된다.

 

9. 에러처리

의도적으로 에러가 나는 상황을 만들어 보자.

   Observable.from(["RxSwift","In",4,"Hours"])
            .single()
            .subscribe { event in
                switch event {
                case .next(let str):
                    print("next : \(str)")
                case .error(let err):
                    print("error : \(err.localizedDescription)")
                case .completed:
                    print("completed")
                }
        }.disposed(by: disposeBag)

여기서 Single()은 "배출된 항목이 단지 하나이고 이것을 조회해야 한다면 사용"한다고 한다.

근데 Observable이 여러개기 때문에 에러가 난다.

next : RxSwift

error : The operation couldn’t be completed. (RxSwift.RxError error 5.)\

그래서 맨 첫번째인 RxSwift를 하나 처리하고 바로 에러가 난다.

에러가 난 경우에는 error쪽을 타고 complete는 안탄다.

complete가 안불리기 때문에 여기서 어떤 작업을 하면 이슈의 원인이 된다.

 

위에서 switch로 처리하기 귀찮으면 아래 subscribe로 하면 된다.

   Observable.from(["RxSwift","In",4,"Hours"])
            .single()
            .subscribe(onNext: { (<#Any#>) in
                <#code#>
            }, onError: { (<#Error#>) in
                <#code#>
            }, onCompleted: {
                <#code#>
            }, onDisposed: {
                <#code#>
            }).disposed(by: disposeBag)

위 방법이 좋은 점은 처리 안할거면 아래와 같이 빼면 된다.

   Observable.from(["RxSwift","In",4,"Hours"])
            .single()
            .subscribe(onNext: { (<#Any#>) in
                <#code#>
            }).disposed(by: disposeBag)

그래서 이걸로 다시 위의 에러가 나는 상황을 만든다면 아래와 같다.

   Observable.from(["RxSwift","In",4,"Hours"])
            .single()
            .subscribe(onNext: { str in
                print(str)
            }, onError: { err in
                print(err.localizedDescription)
            }, onCompleted: {
                print("completed")
            }, onDisposed: {
                print("disposed")
            }).disposed(by: disposeBag)

그리고 실행을 해보면 아래와 같이 disposed가 추가로 불린다.

RxSwift

The operation couldn’t be completed. (RxSwift.RxError error 5.)

disposed

그리고 .single()을 빼서 성공하는 상황을 만들어보면 결과가 아래와 같다.

RxSwift

In

4

Hours

completed

disposed

위 결과를 보면 disposed는 항상 출력되는 것을 확인할 수 있다.

그래서 에러가 나건 안나건 무조건 처리하고 싶을때는 disposed에서 작업을 처리해주면 된다.

 

10. onNext에 전달되는 것

onNext에서 지금은 단순히 print만하지만 만약 처리가 길어질 경우 함수로 따로 빼야한다.

이럴때는 아래와 같이 사용하자.

   Observable.from(["RxSwift","In",4,"Hours"])
            .subscribe(onNext: output, onError: { err in
                print(err.localizedDescription)
            }, onCompleted: {
                print("completed")
            }, onDisposed: {
                print("disposed")
            }).disposed(by: disposeBag)
    }
    
    
    func output(_ s :Any) -> Void {
        print(s)
    }

onNext:다음에 함수를 output이라고 적어놨다.

그래서 output에서 s를 받아 작업하면 된다.

 

11. Scheduler

thread처리하고 싶을 때 아까 이미지뷰에 이미지 넣는 예제로 작업해보자.

   Observable.just("800x600")// "800x600"
            .observeOn(ConcurrentDispatchQueueScheduler(qos: .default)) // thread처리
            .map { $0.replacingOccurrences(of: "x", with: "/") } // "800/600"
            .map { "https://picsum.photos/\($0)/?random" } // "https://picsum.photos/800/600/?random"
            .map { URL(string: $0) } // URL생성
            .filter { $0 != nil } // URL생성이 실패하지 않은 경우만 전달
            .map { $0! } // 옵셔날 추출 URL!
            .map { try Data(contentsOf: $0) } // URL을 데이터로 생성
            .map { UIImage(data: $0) } // 데이터로 UIImage생성
            .observeOn(MainScheduler.instance) // main thread처리
            .subscribe(onNext: { image in // 그 UIImage를 받아서
                self.imageView.image = image // 이미지 뷰에 넣기
        })
        .disposed(by: disposeBag)

사실 데이터를 가져오는부분만 오래걸리므로 그 전줄에만 넣어도 제대로 동작한다.

   Observable.just("800x600")// "800x600"
            .map { $0.replacingOccurrences(of: "x", with: "/") } // "800/600"
            .map { "https://picsum.photos/\($0)/?random" } // "https://picsum.photos/800/600/?random"
            .map { URL(string: $0) } // URL생성
            .filter { $0 != nil } // URL생성이 실패하지 않은 경우만 전달
            .map { $0! } // 옵셔날 추출 URL!
            .observeOn(ConcurrentDispatchQueueScheduler(qos: .default)) // thread처리
            .map { try Data(contentsOf: $0) } // URL을 데이터로 생성
            .map { UIImage(data: $0) } // 데이터로 UIImage생성
            .observeOn(MainScheduler.instance) // main thread처리
            .subscribe(onNext: { image in // 그 UIImage를 받아서
                self.imageView.image = image // 이미지 뷰에 넣기
        })
        .disposed(by: disposeBag)

근데 만약에 첫 줄에 just부터 데이터를 가져오는 코드를 쓴다면?

특정위치에 넣는 observeOn을 사용하지 못한다.

그럴때는 subscribeOn을 사용하면 된다.

   Observable.just("800x600")// "800x600"
            .map { $0.replacingOccurrences(of: "x", with: "/") } // "800/600"
            .map { "https://picsum.photos/\($0)/?random" } // "https://picsum.photos/800/600/?random"
            .map { URL(string: $0) } // URL생성
            .filter { $0 != nil } // URL생성이 실패하지 않은 경우만 전달
            .map { $0! } // 옵셔날 추출 URL!
            .map { try Data(contentsOf: $0) } // URL을 데이터로 생성
            .map { UIImage(data: $0) } // 데이터로 UIImage생성
            .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .default)) // 어디있든 thread동작이 가능하다.
            .observeOn(MainScheduler.instance) // main에서 동작하게
            .subscribe(onNext: { image in // 그 UIImage를 받아서
                self.imageView.image = image // 이미지 뷰에 넣기
        })
        .disposed(by: disposeBag)

밑에 observeOn을 넣은 이유는 imageView에 넣는 건 메인스레드에서 동작해야 하기 때문이다.

 

12. Side effect

방금 예제에서 외부에 영향을 주는 부분은 한 군데 밖에 없다.

self.imageView.image = image; 이 부분이다.

이렇게 사이드 이펙트를 허용하는 곳은 .subscribe와 .do 두 군데다.

do도  onNext, onError, onCompleted, onSubscribed, onDispose가 존재한다.

'공부 > swift' 카테고리의 다른 글

[RxSwift] JSON파싱해서 테이블뷰에 뿌리기  (2) 2020.09.17
[swift] RxSwift Operator(SugarAPI)  (0) 2020.09.15
[swift] RxSwift 기본적인 사용법  (0) 2020.09.14
[swift] RxCocoa  (0) 2020.09.09