MEGJEGYZÉS: Ezt a cikket a Swift 2-re és az RxSwift újabb verzióira frissítettük. Kérjük, olvassa el a frissített verziót az újabb (és jobb) szintaxisért és kódért.



Számtalan cikk létezik az MVVM-ről iOS-ben, de kevés szól kifejezetten az RxSwiftről, és kevés foglalkozik azzal, hogy az MVVM hogyan néz ki a gyakorlatban és hogyan kell csinálni.

A ReactiveX egy könyvtár aszinkron és eseményalapú programok összeállítására megfigyelhető sorozatok használatával.reactivex.io

Az RxSwift egy viszonylag fiatal keretrendszer, amely lehetővé teszi a „reaktív” programozást. Ha nem érti, hogy ez mit jelent, akkor meg kell tennie, mert a funkcionális reaktív programozás már egy ideje lendületet vesz, és nem mutatja a leállás jeleit.

Tehát hogyan néz ki az MVVM iOS-ben?

A modell és a ViewController MVC minta használatával történő összekapcsolása gyakran feltörésnek tűnik. Általában valamilyen updateUI() függvényt kell meghívni a vezérlőn, ha úgy gondolja, hogy a modell megváltozhat, ami visszaállítja a ViewController összes kimenetét. Ez következetlenségekhez vezethet a modell és a ViewController között, szükségtelen frissítésekhez és furcsa hibákhoz.

Szükségünk van egy ViewControllerre, amely mindenkor a modell valódi állapotát mutatja. Lényegében a ViewControllernek egy egyszerű proxynak kell lennie, amely az aktuális modelltől függően jeleníti meg az adatokat.

Természetesen a legtöbb alkalmazás mit sem ér, ha csak a modellt jelenítené meg. Tudnunk kell adatokat kinyerni a modellből és előkészíteni a megjelenítésre. Ezért vezetjük be a ViewModel osztályt. A ViewModel előkészíti az összes megjelenítendő adatot.

De itt van a szórakoztató rész: a ViewModel semmit sem tud a ViewControllerről. Soha nem hivatkozik vagy állít be tulajdonságokat közvetlenül benne. Ehelyett a ViewController folyamatosan figyeli a ViewModel-t az esetleges változások miatt, és amint valami megváltozik, megjeleníti azt. Ne feledje, ez ingatlanonkénti alapon történik. Ez azt jelenti, hogy a ViewController a ViewModel-en belül minden tulajdonságot egyenként jelenít meg, azaz ha egy karakterláncot és egy képet szeretne betölteni, akkor betöltésükkor bemutathatja őket, nem kell várnia, amíg mindketten meg vannak töltve, hogy egyszerre mutassák be mindkettőt.

A ViewController azonban nem csak adatokat jelenít meg, hanem felhasználói bemeneteket is fogad. Mivel a ViewControllerünk csak egy proxy, nincs haszna ennek a bemenetnek, így csak át kell adnia a ViewModelnek, és a ViewModel kezeli a többit.

Ez bizonyos értelemben egyirányú kommunikáció a ViewController és a ViewModel között. A ViewController láthatja és beszélhet vele a ViewModel-lel, de a ViewModelnek fogalma sincs, ki a ViewController. Ez azt jelenti, hogy teljesen eltávolíthatja a ViewControllert az alkalmazásból, és minden logikája a rendeltetésszerűen fog működni!

Mindez jól hangzik, de hogyan tegyük?

MVVM RxSwifttel

Készítsünk egy egyszerű időjárás-alkalmazást, amely megjeleníti egy város előrejelzését, amelyet a felhasználó megad.

Ez a cikk az RxSwift alapvető ismereteit feltételezi. Ha nem tud róla semmit, nyugodtan olvasson tovább, de azt javaslom, olvassa elg feljebb a ReactiveX.

Van egy UITextField a városnév beviteléhez, és egy két UILabelünk az aktuális hőmérséklet nevének megjelenítéséhez.

Megjegyzés: ehhez az alkalmazáshoz az OpenWeatherMap alkalmazásból fogom lekérni az időjárási adatokat.

Tehát a modellünk egy Weather osztály lesz „név” és „fok” tulajdonsággal, valamint egy inicializálóval, amely elfogad egy JSON objektumot, amelyet elemzi és beállítja a tulajdonságait.

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
   }
}

Megjegyzés: A „SwiftyJSON” elengedhetetlen a JSON Swiftben történő elemzéséhez.

Most hagynunk kell, hogy a ViewModel manipulálja a modellt egy nyilvános searchText tulajdonságon keresztül, amelyhez a ViewController később hozzáfér.

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?>()

A keresőszövegünk egy BehaviorSubject objektum. Az Subjektumok egyszerre Megfigyelhető és Megfigyelő is. Más szóval, küldhet nekik olyan elemeket, amelyeket újra kiküldhetnek.

A BehaviorSubjects egyediek, mert miután előfizetnek rájuk, mindent kiadnak, amit a múltban kaptak. Erre azért van szükségünk, mert az MVVM-ben, az alkalmazás életciklusától függően, a különböző osztályok megfigyelhető elemei néha megkaphatják az elemeket, mielőtt előfizetne rájuk. Miután a ViewController feliratkozott a ViewModel egy tulajdonságára, látnia kell, hogy mi volt az utolsó elem, hogy megjelenítse azt, és fordítva.

Most deklarálnunk kell egy tulajdonságot a ViewModelben a felhasználói felület minden olyan bitjéhez, amelyet programozottan módosítani szeretne.

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

Állítsunk be egy tulajdonságot a modellünkhöz, és változtassuk meg a fenti tulajdonságokat, amikor a modellünk változik. Ezt úgy fogjuk megtenni, hogy a „régimódi” módszert (Swift tulajdonmegfigyelői) kombináljuk az Rx-szel. Az időjárás objektum tulajdonságait elküldjük a PublishSubjects-nek, hogy kiadhassák a modellben szereplő értékeket.

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")
         }
      }
   }
}

Megjegyzés: Győződjön meg arról, hogy ez a fő sorban történik, mert az onNext() metódus egy másik szálban fut! (az onNext metódus egy elemet küld egy Observernek)

Most kössük össze a modellünket a fent deklarált searchText tulajdonsággal. Ezt úgy fogjuk megtenni, hogy minden alkalommal létrehozunk egy NSURLRequest-et, amikor a keresőszöveg megváltozik, majd feliratkozunk a modellünkre erre a kérésre. Ezt az init() metódusban tesszük, mert tudjuk, hogy minden tulajdonságunk be van állítva, amikor az init() meghívásra kerül.

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)
}

Ily módon, amikor a searchText megváltozik, a jsonRequest a megfelelő NSURLRequest-re változik. Amikor ez módosul, a modellünk az NSURLRequesttől kapott adatokra lesz beállítva.

Megjegyzés: Az rx_JSON() metódus valójában maga egy megfigyelhető sorozat. Tehát a jsonRequest valójában egy megfigyelhető eleme. Ezért használjuk a .switchLatest() függvényt, amely biztosítja, hogy a jsonRequest csak a legújabb sorozatot adja vissza. Ne feledje azt is, hogy a kéréslekérése addig nem indul el, amíg elő nem iratkozik rá.

Most már csak a ViewController csatlakoztatása van hátra a ViewModelhez. Ezt úgy fogjuk megtenni, hogy a ViewModel PublishSubjects elemeit a Controller aljzataihoz kötjük.

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)
   }
}

Ne felejtsük el, hogy meg kell győződnünk arról is, hogy a ViewModelünk tudja, mit írt be a felhasználó a szövegmezőbe! Ezt úgy tehetjük meg, hogy elküldjük a nameTextField értékét, valahányszor megváltozik, a ViewModel searchText tulajdonságába. Tehát egyszerűen hozzáadjuk ezt a viewDidLoad()-hoz:

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

És itt vagyunk! Alkalmazásunk lekéri az időjárási adatokat, miközben a felhasználó gépel, és bármit is lát a felhasználó, az az alkalmazás valódi állapota a színfalak mögött.

Ha érdekli az alkalmazás egy kicsit kibővített verziója, nézze meg a Weather alkalmazás példáját a Github oldalon.

Köszönet Thomas Schusternek, hogy megkeresett PublishSubjects-ben. :)