Skip to content

Latest commit

 

History

History
183 lines (135 loc) · 11.6 KB

File metadata and controls

183 lines (135 loc) · 11.6 KB

이글은 Marin Benčević 의 "Implementing MVVM in iOS with RxSwift"를 번역한 글입니다. ( https://medium.cobeisfresh.com/implementing-mvvm-in-ios-with-rxswift-458a2d47c33d )

(번역 허락을 받은 것은 아니지만, 도움이 필요하신 분들을 위해서 올려봅니다. 문제가 될 경우 바로 삭제하겠습니다. ㅡㅜ)

RxSwift를 가지고 iOS에서 MVVM 구현하기

주의: 이글은 Swift2 과 RxSwift 새버전으로 업그레이드 되었습니다. 업데이트된 버전의 문법과 코드를 읽어 주세요. (Swift3 에대한 업데이트는 아직인 것 같습니다. Swift2라서 Swift3와는 좀 다르지만, 개념을 잡고 참고하기에 좋을 것 같아서 번역해봅니다....)

iOS에서의 MVVM에 대한 매우 많은 글들이 있지만, 특히 RxSwift에 대한 글은 거의 없고, 실제로 MVVM 처리 보이는 것과 그걸 어떻게 해야 하는지에 대해 초점을 맞춘 글들도 거의 없습니다.

ReactiveX는 observable sequence들을 이용해서 비동기와 event-based programming를 결합시키는 라이브러리 입니다. - ReactiveX.io

RxSwift는 reactive하게 프로그램할 수 있도록 해주는 비교적 신생 framework입니다. 만약 여러분이 그것이 의미하는 바를 이해할 수 없다면, 요즘 한동안 Functional reactive programming이 탄력받고 있고, 멈출 기미를 보이지 않기 때문에 이해할 수 있도록 (공부)해야 합니다.

그래서 iOS에서 MVVM은 어떤 모습인가요?

여러분이 MVC 패턴을 이용해서 model을 ViewController와 연결시키는 방법은 종종 지겨운 일처럼 느껴집니다. 여러분은 보통 model이 변경되었다고 생각될 때 controller에서 일종의 updateUI()를 호출할 것이고, ViewController의 모든 outlet들이 reset 될 겁니다. 이런 방법은 model 과 ViewController간에 모순과 불필요한 갱신, 이상한 버그들을 유발하게 됩니다.

우리가 원하는 것은 항상 model의 진짜 상태를 보여줄 ViewController입니다. 본질적으로, 우리는 ViewController가 어떤 모델이던지간에 상관없이 data를 바로 보여줄 간단한 proxy이기를 원합니다.

물론, 단순히 model을 표시만 해주기는 하는 앱이라면 쓸모가 없을 겁니다. 우리는 model로 부터 data를 추출할 수 있기를 원하고 표시를 위해 그것을 준비할 수 있기를 원합니다. 이것이 ViewModel class를 도입하는 이유입니다. ViewModel은 우리가 표시하기를 원하는 모든 데이터를 준비합니다.

하지만, 재미있는 부분이 여기 있습니다. ViewModel은 ViewController를 전혀 알지 못한다는 것입니다. ViewModel은 결코 ViewController를 참조하거나 ViewController의 property를 내부에서 직접적으로 설정하지 않습니다.

대신에, ViewController는 지속적으로 ViewModel에 변경사항이 있는지를 감시합니다. 변경이 발생하자마자 표시됩니다. 기억하세요. property 단위로 이런 동작이 일어납니다. 이것은 ViewController가 ViewModel 내부의 각각의 property를 개별적으로 표시하고 있다는 것을 의미합니다. 예를들어 여러분이 문자열과 이미지를 load하기를 원한다면, 한번에 둘다 표시하기 위해서 문자열과 이미지가 모두 load될 때까지 기다릴 필요가 없이, load되자 마자 바로 표시할 수 있습니다.

하지만, ViewController는 단지 데이터를 표시만 하지 않습니다. 사용자 입력도 또한 받습니다. 우리의 ViewController는 단지 Proxy이기 때문에 그러한 입력에 대해서는 사용할 수 없습니다. 사용자 입력을 처리하기 위해서는 받은 입력을 ViewModel로 넘겨줘야 합니다. ViewModel은 나머지를 처리할 겁니다.

어떤 관점에서 이것은 ViewController 와 ViewModel 간에 단방향 통신입니다. ViewController는 볼 수 있고, ViewModel에게는 말할 수 있습니다. 하지만 ViewModel은 ViewController를 전혀 알지 못합니다. 이것은 여러분의 앱에서 ViewController를 완전히 제거해도 모든 로직 부분은 의도한대로 잘 동작한다는 것을 의미합니다.

멋지게 들립니다. 어떻게 하면 될까요?

RxSwift를 이용한 MVVM

사용자 입력가 입력하는 도시의 날씨 예보를 보여주는 간단한 날씨 앱을 만들어 봅시다.

이글은 여러분이 RxSwift의 기본적으로 알고 있다고 가정합니다. 만약 여러분이 RxSwift에 대해서 전혀 모른다고 하더라도 맘편히 계속 읽어도 됩니다. 하지만 ReactiveX( http://reactivex.io )에 대해서 한번 읽어 볼 것을 권합니다.

도시의 이름 입력을 위한 UITextField 하나와 현재 온도의 이름을 표시할 두개의 UILabel이 있습니다.

주의:이 앱을 위해 OpenWeatherMap에서 날씨 data를 가져올 겁니다.

그래서, 우리의 model은 'name', 'degree' property 그리고 initializer를 가지는 Weather class가 될 겁니다. initializer는 받은 JSON object를 파싱하여 property들을 설정합니다.

class Weather {
   var name:String?
   var degrees:Double?

   init(json: AnyObject) {
      let data = JSON(json)
      self.name = data["name"].stringValue
      self.degrees = data["main"]["temp"].doubleValue
   }
}

주의: SwiftyJSON( https://github.com/SwiftyJSON/SwiftyJSON )은 Swift에서 JSON을 파싱하는데 필수입니다.

이제 우리는 ViewModel이 public searchText property를 통해서 model을 다루도록 할 필요가 있습니다. public searchText property는 나중에 ViewController가 access하게 됩니다.

class ViewModel {

  private struct Constants {
      static let URLPrefix = "http://api.openweathermap.org/data/2.5/weather?q="
      static let URLSuffix = "/* my openweathermap APPID */"
    }

   let disposeBag = DisposeBag()

   var searchText = BehaviorSubject<String?>()

우리의 searchText는 BehaviorSubject object입니다. Subject들은 동시에 Observable이면서 Observer입니다. 다른 말로, 여러분은 Subject들에게 item들을 보낼 수 있고, Subject들은 그 item들을 다시 발생시킬 수 있습니다.(re-emit)

BehaviorSubject들은 한번 subscribe 되면, 과거에 수신했던 모든 것들을 발생시키(emit)하기 때문에 독특합니다.(*주석:원 작성자가 잘못 알고 있는 것으로 보인다. BehaviorSubject는 구독하면 이전 마지막 이벤트를 다시 발생시켜준다. 모든 이벤트가 아닌 마지막 하나를 발생시킨다. 그리고 추가로 발생하는 이벤트도 발생시킨다.이벤트를 모두 다시 발생시킬려면 share를 써야한다.) MVVM에서는 app의 lifecycle에 의존하고 있기때문에 우리는 그런 것이 필요합니다. 때때로 다른 class들의 Observable들은 여러분이 그것들을 subscribe하기 전에 element들을 받을 수 있습니다. ViewController가 ViewModel의 property를 subscribe를 하면, ViewController는 표시할 순서상 마지막 item이 무엇인지 그리고 거꾸로는 어떻게 되는지 알아야 할 필요가 있습니다.

이제 우리는 ViewModel에 프로그램적으로 변경하기를 원하는 UI의 모든 요소에 대해서 property를 선언할 필요가 있습니다.

var cityName = BehaviorSubject<String>()
var degrees = BehaviorSubject<String>()

또한 model에 대한 property를 설정하고, model이 변경될 때마다 위의 property들을 변경합시다. 우리는 Rx를 가지고 옛날 방식(Swift의 property observers)으로 연결해서 할 겁니다. 우리는 우리의 PublishSubject들에 Weather object의 property들을 보낼 겁니다. 그래야 PublishSubject들이 model에서 값들을 발생시킬(emit) 수 있게됩니다.

var weather:Weather? {
   didSet {
      if let name = weather?.name {
         dispatch_async(dispatch_get_main_queue()) {
            self.cityName.onNext(name)
         }
      }
      if let temp = weather?.degrees {
         dispatch_async(dispatch_get_main_queue()) {
            self.degrees.onNext("\(temp)°F")
         }
      }
   }
}

주의: onNext() 메소드가 다른 thread! 에서 돌기 때문에, main queue에서 이것들이 수행된다는 것을 확실히 할 필요가 있습니다.(onNext 메소드는 element를 Observer로 보냅니다.)

이제 우리 model을 위에서 우리가 선언한 searchText property에 연결합시다. searchText가 변경될 때 마다 NSURLRequest를 생성함으로써 이걸 할 겁니다. 그리고 그러고 나서 우리의 model을 그 request에 subscribe 시킵니다. 우리는 init() 이 호출될 때 모든 property가 설정된다는 것을 알기 때문에 init()에서 이것을 할 겁니다.

init() {
   let jsonRequest = searchText
      .map { text in
         return NSURLSession.sharedSession().rx_JSON(self.getURLForString(text)!)
      }
      .switchLatest()

   jsonRequest
      .subscribeNext { json in
         self.weather = Weather(json: json)
       }
      .addDisposableTo(disposeBag)
}

이렇게 searchText이 변경될 때 마다, jsonRequest는 searchText 변경사항 자체를 관련된 NSURLRequest로 변경시킵니다. 그 jsonRequest가 변경될 때 마다, 우리의 모델은 NSURLRequest로 부터 받은 것으로 설정됩니다.

주의:rx_JSON() 메소드는 실제로 observable sequence 그자체 입니다. 그래서 jsonRequest 는 실제로 Observable 의 Observable 입니다. 이것이 우리가 .switchLatest 를 사용하는 이유입니다. .switchLatest는 jsonRequest가 가장 최신 sequence만을 리턴한다는 것을 보장합니다. 또한 여러분이 request에 subscribe할때까지 request는 fetching을 시작하지 않는 다는 것을 기억하세요.

이제 ViewController를 ViewModel에 연결하는 것만 남았습니다. 우리는 viewModel의 PublishSubject들을 Controller의 outlet들에 연결함으로써 이것을 할 겁니다.

class ViewController: UIViewController {

   let viewModel = ViewModel()
   let disposeBag = DisposeBag()


   @IBOutlet weak var nameTextField: UITextField!

   @IBOutlet weak var degreesLabel: UILabel!
   @IBOutlet weak var cityNameLabel: UILabel!
   override func viewDidLoad() {
      super.viewDidLoad()

      //Binding the UI
      viewModel.cityName.bindTo(cityNameLabel.rx_text)
         .addDisposableTo(disposeBag)

      viewModel.degrees.bindTo(degreesLabel.rx_text)
         .addDisposableTo(disposeBag)
   }
}

사용자가 textField!에 입력한 것을 우리의 ViewModel이 알게 할 필요가 있다는 것을 잊지 마세요. nameTextField가 변경될 때마다 nameTextField의 값을 searchText property에 보냄으로써 우리는 이것을 할 수 있습니다. 그래서 우리는 이것을 간단히 우리의 viewDidLoad에 넣을 겁니다.

nameTextField.rx_text.subscribeNext { text in
   self.viewModel.searchText.onNext(text)
   }
   .addDisposableTo(disposeBag)

이제 다 왔습니다! 우리의 앱은 사용자가 입력할 때 마다 날씨 데이터를 fetching합니다. 그리고 사용자가 보는 것이 무엇이던지 간에 장면뒤에 있는 앱의 진짜 상태입니다.

만약 이 버전의 앱을 확정하는 것에 관심이 있다면, 깃헙에서 저의 날씨 앱 예제를 확인해 보세요.( https://github.com/marinbenc/ReactiveWeatherExample )

PublishSubject들에 대해 보정해준 Thomas Schuste( https://medium.com/@schuster_thomas )에게 감사드립니다.