RxSwift ve RxCocoa’ya Giriş

Bugün sizlere 4 yıllık iOS developer hayatımda kullanmaktan en çok keyif aldığım ve beni daha iyi bir developer haline getirdiğine inandığım bir araçtan bahsetmek istiyorum. Bu yazıyı okuyarak günümüzde çok popüler olan reactive programlamayı, RxSwift ve RxCocoa sayesinde siz de kendi Swift projenize entegre edebilecek seviyeye geleceğinize inanıyorum. Lead iOS Developer olarak çalıştığım Bundle News’da da yoğun bir şekilde bu yapıları kullandığımızı ve gayet memnun olduğumuzu belirtmek isterim.

RxSwift ve RxCocoa nedir?

RxSwift kütüphanesi bize Swift’i tamamen farklı bir şekilde kullanmamıza olanak sağlıyor. Bu kütüphaneyle asenkron programlama yapmak hem çok kolay hem de çok daha okunaklı hale geliyor. Daha sağlam mimariler tasarlamanıza ve daha kaliteli uygulamalar yapmanıza olanak sağlıyor.

RxCocoa kütüphanesi ise bize iOS ve OS X’te kullanılan Cocoa API’larını reactive tekniklerle kullanmamıza olanak tanıyor.

Observable ve Observer Yapıları

Bu yazıda bilmeniz gereken konseptlerden biri Observable ve diğeri Observer’dır.

  • Observable (gözlemlenebilir), değişiklikleri yayımlayan yapılardır.
  • Observer (gözlemleyici) yapılar Observable yapılara subscribe olarak, değişlik olduğunda haberleri olan yapılardır.

Dispose Bag

RxSwift ve RxCocoa, ARC ve hafıza yönetimine yardımcı olan DisposeBag denilen bir araç bulundurmaktadır. DisposeBag’i Observer objeleri taşıyan sanal bir çanta gibi düşünebiliriz. Observer’ları tanımladığımız ebeveyn objelerin (parent object) hafızadan uçtuğunda Observer’larından da düzgün bir şekilde kurtulmak için DisposeBag aracını kullanırız.

Biraz kavramlar konusunda kafanız karışmış olabilir ama yazının devamında daha iyi anlayacaksınız.


Hadi Başlayalım

Size RxSwift ve RxCocoa kütüphanelerini daha iyi anlatabilmek için reactive teknikleri kullanılmayan ufak bir uygulama hazırladım. Yazının devamında adım adım uygulamayı reactive bir hale döndüreceğiz. Bu hazırladığım CoffeeShop uygulamasında, kullanıcı girişi sayfası, dükkandaki tüm kahveleri görebileceğiniz menü sayfası, kahveleri sipariş edebileceğiniz detay sayfası ve satın almak istediğiniz kahveleri görebileceğiniz sepet sayfası bulunmaktadır. Projenin ilk halini buradan indirip, yapılacak değişiklikleri benimle birlikte adım adım yapmanızı özellikle öneririm.

Not: Projenin ilk ve son halini terminalde git clone https://github.com/Goktug/RxSwift-RxCocoa-CoffeeShop-Medium.git komutunu kullanarak GitHub’dan clone’layabilirsiniz.
Not: Projeyi indirip, zip dosyasından çıkardıktan sonra RxSwift ve RxCocoa Pod’larını yüklemek için terminalinizden pod install komutunu çalıştırmanız gerekmektedir. Pod yüklemesi bittikten sonra CoffeeShop.xcworkspace dosyasını Xcode ile açmalısınız.
Genel Bakış

Giriş Sayfası

Kullanıcıdan e-mail ve şifre bilgilerini istediğimiz bir giriş sayfası bulunuyor. Burada backend ile iletişim kuran gerçek bir authentication mekanizması kullanmayacağız. Sadece e-mail’ın doğru formatta ve şifrenin de en az 6 karakter olması şartına bakacağız. Eğer e-mail ve şifre istediğimiz şartları karşılıyorsa, kullanıcıyı menü sayfasına yönlendireceğiz.

Giriş Sayfası

LoginViewController.swift dosyasını açtığımızda IBOutlet ile bağlanmış olan E-Mail ve Şifre textfield’lerini ve Giriş butonunu görüyoruz. Bununla birlikte logInButtonPressed() fonksiyonu ise giriş butonuna basıldığında çalışmaktadır. Gördüğünüz gibi şu an texfield’ler için herhangi bir validation mekanizması bulunmuyor. Şimdi bu sayfaya reactive programlama prensiplerini kullanarak validation mekanizması ekleyelim.

İlk olarak dosyanın en üstüne RxSwift ve RxCocoa kütüphanelerini import etmemiz gerekiyor.

import RxSwift
import RxCocoa
class LoginViewController: UIViewController { ... }

RxSwift ve RxCocoa sayesinde textfield’in text input girişlerini anlık bir şekilde alıp, reaksiyon gösterebiliyoruz. Bu sayede kullanıcının her input girişinde e-mail ve şifre alanın doğruluğunu kontrol edeceğiz. Eğer iki girdi de şartlarımızı karşılıyor ise Log In butonunu aktif edeceğiz. Non-reactive programlamada bu işlemi yapmak için UITextFieldDelegate delegate’inin metodlarını sınıfımıza entegre etmemiz ve bir sürü karmaşık if/else koşulları oluşturmamız gerekiyordu. Reactive programlama sayesinde doğrudan bu girdilere erişebileceğiz.

LoginViewController sınıfının en üstüne aşağıdaki kodu ekleyelim.

private let disposeBag = DisposeBag()

Daha önce bahsettiğim gibi DisposeBag, class’ımızın instance’ı hafızadan uçtuğunda (deallocated), Observer’larından da düzgün bir şekilde kurtulmamızı (disposed) sağlıyor.

Sınıfımızın içersine aşağıdaki e-mail doğrulama kodunu ekleyelim. Bunu daha sonra kullanıcıdan aldığımız e-mail girdisini doğrulamak için kullanacağız.

Şimdi e-mail doğrulama kısmına sıra geldi. Aşağıdaki kodu viewDidLoad metodumuzun içersine ekleyelim.

Adım adım bu kodlar ne yapıyor bakalım:

  1. RxCocoa extension’larından biri olan text sayesinde UITextField’in text değişkenine Observable bir değişken olarak ulaşabiliyoruz.
  2. Normalde textfield’in text verisi String ya da nil olabiliyor. orEmpty verinin her zaman String tipinde gelmesini sağlıyor.
  3. Girdi validateEmail metoduyla doğruluğu kontrol ediliyor. Eğer e-mailımız doğru formattaysa true , değil ise false tipine dönüştürülüyor. emailValid değişkenin Bool tipinde bir Observable değişken olmasını istediğimizden map ile girdiyi String’den Bool tipine dönüştürmemiz gerekiyor.
  4. debug metodu konsoldan olayların akışını görebilmemizi sağlıyor. Eklemek zorunda değilsiniz, tercihe bağlıdır. Ben sadece süreci daha iyi gözlemlemeniz açısından başlarda eklemenizi tavsiye ediyorum.
  5. Bu Observable değişkene başka subscribe olan Observer’lar varsa share sayesinde map işlemlerinin tekrarlanmamasını sağlıyor.

Şimdi bu kod kullanıcı her input girdiğinde çalışıyor. Kullanıcıdan her input girdiğinde değil de belli aralıklarla input girildiğinde inputları alarak optimize edebiliriz. RxSwift’in bize sunduğu throttle ya da debounce filtreleme operatörleri bu iş için biçilmiş kaftan. Bizim durumumuz için daha uygun olduğundan dolayı throttle operatörünü kullanacağız.

.throttle(0.1, scheduler: MainScheduler.instance)

Örnek olarak bu kodu ele alalım. Bu kod sayesinde her bir input geldikten sonra 0.1sn boyunca başka bir input gelmesini bekliyor. Bu süre boyunca input gelirse bir sonraki input için bir 0.1sn daha bekliyor. Eğer son input’u aldıktan sonra 0.1sn boyunca başka bir input daha almazsa input’un son halini akışa veriyor. Bu da uygulamamızın hızlı input girişlerinde gereksiz kod çalıştırmasını ve bundan dolayı oluşabilecek kitlenmeleri önlüyor.

throttle akış diagramı

Şimdi kodumuza throttle mekanizmasını entegre edelim. Aşağıdaki kodu sınıfın üstüne ekleyelim.

private let throttleInterval = 0.1

Aşağıdaki kodu da önceki yazdığımız kodla değiştirelim.

Aynı şeyleri şifre girdisini kontrol etmek içinde yapmamız gerekiyor. Aşağıdaki kodu yukarıda yazdığımız kodun altına ekleyelim.

Burada map kodu dışında email için yazdığımız kodlarla birebir aynı olduğundan sadece değişik olan kodun ne yaptığına bakalım:

  1. Şifre girdisinde sadece en az 6 karakter kriterimiz olduğu için map fonksiyonumuz içersine bu koşulu yazıyoruz. String tipinde olan girdiyi Bool tipine dönüştürerek akışa veriyoruz.

Şimdi bu iki girdimizin de aynı anda doğruluğunu kontrol etmemizi sağlayan Observable bir değişken oluşturmamız gerekiyor.

CombineLatest, RxSwift’in birleştirme operatörlerinden biridir. Bu metod sayesinde birden fazla aynı tipte olan Observable değişkenleri tek bir Observable değişken altında toplayabiliyoruz.

combineLatest akış diagramı

emailValid ve passwordValid değişkenlerini && operatörüyle birleştirip, ikisinin de true olduğu durumda everythingValid değişkenimizin true olmasını istiyoruz. Aksi takdirde false tipinde döndüreceğiz. Bu Observable değişkenimizi de logInButton adındaki butonun isEnabled değişkenine bağlayacağız. Bu da bize e-mail ve şifre girdilerinin doğru olduğu durumda giriş butonunumuzun aktif olmasını ve tıklandığında da menü sayfasına geçilmesine olanak tanıyacaktır.

Aşağıdaki kodu viewDidLoad metodunun en altına ekleyelim.

Adım adım bu kodlar ne yapıyor bakalım:

  1. bind(to:) metodu aynı tipteki Observable değişkenleri birbirine bağlayabilmemize olanak tanır. (RxCocoa extensionlarından biri olan rx.isEnabled butonun isEnabled değişkenini Observable bir değişken olarak döndürür.)
  2. bind(to:) metodu bize Disposable tipinde bir değişken döndürür. LoginViewController sınıfı hafızadan uçtuğunda oluşturduğumuz Observer’dan düzgün bir şekilde kurtulmak için bu sınıfındisposeBag‘ine eklememiz gerekmektedir.

Artık giriş sayfamızın tüm fonksiyonelliğini tamamlamış olduk. Bir sonraki sayfamıza geçebiliriz.


Menü Sayfası

Bu sayfada mağazada bulunan kahve çeşitlerini görüntüleyebiliyoruz. Satın almak istenilen kahveye tıklayarak kahvenin detay sayfasına gidebiliyoruz. Ayrıca sağ üst köşedeki kahve bardağı ikonuna tıklayarak Sepet sayfasına gidebiliyoruz. Sepete eklediğiniz kahve sayısını da ikonun üstündeki küçük kırmızı yuvarlakta görebiliyoruz.

Menü Sayfası

Şimdi bu sayfada öncellikle RxCocoa kullanarak UITableView UI komponentimizi reactive yapacağız. RxCocoa, UITableView için bir takım reactive API’lara sahip bu sayede artık UITableViewDataSource ve UITableViewDelegate delegate’ini override etmemize ihtiyaç kalmayacak. RxCocoa bizim için kendisi halledecek.

Öncelikle MenuViewController.swift dosyasını açın ve dosyanın en altında bulunan UITableViewDataSource ve UITableViewDelegate extension kodlarını silin.

Daha sonra configureTableView() fonksiyonunun içersinden aşağıdaki kodları silin. Artık bu kodlara ihtiyacımız olmayacak.

tableView.delegate = self
tableView.dataSource = self

Fakat UITableViewDelegate metodlarını sildiğimiz için configureTableView() fonksiyonun içersine cell’lerin yüksekliğini belirten kodu eklememiz gerekiyor. O yüzden aşağıdaki kodu eklemeniz gerekmektedir.

tableView.rowHeight = 104

Şimdi sıra menüde yer alacak kahve verilerimizi table view’e reactive bir şekilde bağlamaya geldi. Table view’in bu verilere reactive bir şekilde bağlanması için veri yapımızın da reactive olması gerekmektedir.

Bunu sağlamak için coffees değişkenini Observable yapmamız gerekiyor. Kodu aşağıdaki şekilde güncellememiz gerekiyor.

.just(_:) metodu Observable tipinde olan bu değişkenin hiçbir zaman değişmeyeceğini gösteriyor. Fakat değişkenimiz değişmeyecek olsa da Observable bir değişken yaratmış oluyoruz.

Not: Verinin hiç değişmeyecek olduğunu bile bile bazen .just(_:) metodunu kullanmak anlamsız gelebilir fakat hem reactive programlama tekniklerini hem de Rx’in gücünü tam anlamıyla kullanabilmek için buna ihtiyaç duymaktayız.

MenuViewController sınıfının en üstüne aşağıdakini aşağıdaki kodu ekleyin.

private let disposeBag = DisposeBag()

Aşağıdaki kodu viewDidLoad metodunun içine configureTableView() ‘den sonrasına ekleyiniz.

Bu kod bize coffees değişkeninde bir değişiklik olduğunda table view’e cell’lerin eklenmesini sağlamaktadır. Bizim durumuzda coffees değişkenine tanımladığımız 5 adet kahve verisi table view’e cell olarak eklenecektir.

Adım adım bu kodlar ne yapıyor bakalım:

  1. Table view’deki her bir satır için çalışacak bu kodu coffees değişkeniyle ilişkilendirmek için bind(to:) metodunu çağırıyoruz.
  2. RxCocoa extension’larına ulaşabilmek için rx metodunu çağırıyoruz.
  3. items(cellIdentifier:cellType:) metoduna kullanmak istediğimiz cell identifier’ı ve cell’in sınıfını paslamamız gerekmektedir. Bu sayede Rx framework’ü bizim için normalde delegate kullanarak oluşturduğumuz dequeuing metodlarını kendisi çağırır.
  4. Bu kod bloğu her yeni eleman için çalıştırılmaktadır. Row, element ve cell bilgilerine ulaşabilmemize olanak tanır. Bizim durumuzda element değişkeni Coffee tipindedir. CoffeeCell sınıfından oluşan cell’imizin configure(with:_) metoduna mevcut kahve bilgimizi vererek konfigüre ediyoruz.
  5. bind(to:) metodu bize Disposable tipinde bir değişken döndürür. MenuViewController sınıfı hafızadan uçtuğunda oluşturduğumuz Observer’dan düzgün bir şekilde kurtulmak için bu sınıfındisposeBag‘ine eklememiz gerekmektedir.

Bununla birlikte Rx Framework’ü bizim için tableView(_:numberOfRowsInSection:) ve numberOfSections(in:) metodlarının çıktılarını, gözlemlenen değişkene göre kendisi otomatik hesaplayacaktır. Aynı zamanda fark edebileceğiniz üzere tableView(_:cellForRowAt:) metodunu yukarıda yazdığımız closure ile değiştirmiş olduk.

Şimdi uygulamayı çalıştırıp her şey eskisi gibi mi gözüküyor diye bi kontrol edelim. Hiç fena değil neredeyse her şey aynı ama eksik olan bir şey var. tableView(_:didSelectRowAt:) metodunu sildiğimiz için uygulama şu an cell’e tıkladığında ne yapacağını bilmiyor.

Bunu düzeltmek etmek için RxCocoa’nın table view için diğer bir extension’ı olan modelSelected(_:) metodundan yararlanmamız gerekiyor. Bu metod seçilen (tıklanan) cell’in modelini Observable olarak geri döndürür.

Aşağıdaki kodu yukarıda eklediğimiz kodun hemen altına ekleyelim.

Adım adım bu kodlar ne yapıyor bakalım:

  1. RxCocoa extension’larına ulaşabilmek için rx metodunu çağırıyoruz.
  2. Table view’in reactive extension’ı olan modelSelected(_:) metoduna dönecek elemanın tipini paslayıp çağırdığımızda metod bize table view’den seçilen elemanı Observable tipinde döndürür.
  3. Bu Observable değişeni subscribe(onNext:) metodun içindeki closure’a paslarız. Bu closure her cell seçildiğinde (tıklandığında) çalışmaktadır.
  4. Kahve detay sayfasına gitmek için daha önceden Storyboard’tan tanımladığımız segue’yi çalıştırıyoruz.
  5. Table view’de seçilen satırı, seçili durumundan çıkarıyoruz.
  6. subscribe(onNext:) metodu bize Disposable tipinde bir değişken döndürür. MenuViewController sınıfı hafızadan uçtuğunda oluşturduğumuz Observer’dan düzgün bir şekilde kurtulmak için bu sınıfındisposeBag‘ine eklememiz gerekmektedir.

Sonunda table view ile işimiz bitti. Artık table view’imiz tamamen reactive bir şekilde çalışıyor.


Hadi Navbar’daki Sepet Butonunu da Reactive Yapalım!

Butonumuzda yer alan kırmızı yuvarlağın içersindeki sayı sepetteki ürün miktarına göre güncellenmektedir. Mevcut yapımızdaMenuViewController ‘ın viewWillAppear(:_) metodunda ShoppingCart modelimizden toplam sipariş adetini alan ve butondaki badge’i güncelleyen bir kod parçacığı bulunmaktadır. Eğer biz ShoppingCart modelimizdeki coffees değişkenini bir Observable değişkene çevirirsek artık sürekli viewWillAppear(:_) metodunun içersinde sipariş adetinin son halini almak zorunda kalmayız. Reactive yapı sayesinde coffees değişkeni içersine yeni eleman geldiğinde ya da çıktığında otomatik bir şekilde butonumuzdaki badge’in güncellenmesini sağlayabiliriz.

ShoppingCart.swift dosyasını açalım ve aşağıdaki kodla değiştirelim.

Adım adım bu kodlar ne yapıyor bakalım:

BehaviourSubject Diagramı
  1. Burada ilk defa Observable dışında başka bir yapı kullanıyoruz. BehaviorRelay, BehaviourSubject’in bir wrapper’ıdır. Rx konseptinde Subject’ler hem Observable hem de Observer değişken olarak davranabilen proxy ya da köprülerdir diyebiliriz. Başka bir yazımda bu konseptleri daha detaylı açıklayacağım. Fakat şimdilik bilmemiz gereken şey; BehaviourSubject bir değişkene subscribe olduğumuzda bize en son emit ettiği elemanı döndürmesidir.
  2. BehaviourSubject bir değişkenin value parametresine ulaşarak Subject’e en son emit edilen elemanı alabiliyoruz. coffees.value kodu bize [Coffee: Int] tipindeki dictionary’imizi döndürüyor. Bu dictionary’i gecici bir değişkene atayıp, içersinde yeni eklenecek kahvemizle ilgili güncellememizi yapacağız.
  3. accept(:_) metoduyla BehaviourSubject tipindeki bir değişkenin içersine yeni bir eleman emit edebiliyoruz.
  4. Burada da ekleme yaparken kullandığımızla aynı şekilde değişkenin içindeki en son değeri alıp, gecici değişkene atıyoruz. Devamında ise silmek istediğimiz kahveyi dictionary’den çıkartıyoruz.
  5. Güncellediğimiz dictionary’i coffees değişkenine emit ediyoruz.
  6. getTotalCost() metodunu artık Float yerine Observable<Float> döndüren bir metoda çeviriyoruz.
  7. BehaviourRelay’in hem Observable hem de Observer gibi davrandığından bahsetmiştik. coffees değişkeni içersinde bulunan elemanların fiyat bilgilerini alıp, toplamını döndürmemiz gerekiyor. Aynı zamanda Observable bir değişken olarak döndürmemiz lazım. Bunu yapmak için coffees değişkenini map’leyerek içindeki elemanlara ulaşabiliriz ve istediğimiz gibi değiştirebiliriz.
  8. getTotalCount() metodunu artık Int yerine Observable<Int> döndüren bir metoda çeviriyoruz.
  9. Sepette bulunan toplam kahve adetini Observable olarak döndürüyoruz.
  10. getCartItems() metodunu artık [CartItem] yerine Observable<[CartItem]> döndüren bir metoda çeviriyoruz.
  11. Sepet sayfasında bulunan table view’de kullandığımız CartItem modelini Observable bir array olarak döndürüyoruz.

ShoppingCart modelimiz artık reactive bir yapıya çevrildi. Bulunla birlikte şu an projenizi çalıştırmak isterseniz Xcode’un bir çok yerde hata verdiğini göreceksiniz. Projenizi başarılı bir şekilde çalıştırabilmek için ShoppingCart modelinden veri okuduğumuz yerleri de reactive bir hale çevirmemiz gerekiyor.

MenuViewController.swift dosyasına geri dönelim ve viewWillAppear(:_) metodumuzu tamamen silelim.

Aşağıdaki kodu viewDidLoad metodunun en altına yapıştıralım.

Burada ShoppingCart modelimizin getTotalCount() adındaki Observable<Int> tipindeki metoduna subscribe oluyoruz. Artık coffees değişkeninde bir değişiklik olduğunda güncel toplam sipariş edilen kahve adeti bilgisi totalOrderCount değişkeniyle döndürülüyor.


Sepet Sayfası

Bu sayfada menüden seçipte eklediğimiz kahvelerin adetleri ve fiyatları bulunuyor. İstenilen siparişi sepetten silebilme özelliği de bulunuyor. Aynı zamanda sayfanın en alt kısmında toplam ödenecek tutar yer alıyor. Herhangi bir siparişi sildiğinizde toplam ödenecek tutar güncelleniyor.

Sepet Sayfası

ShoppingCartViewController.swift dosyasını açalım. Bu sayfada da aynı şekilde table view’i reactive bir hale getirmemiz gerekiyor. Menu sayfasında öğrendiğimiz şeyleri tekrardan bahsetmeyeceğim. Sadece yeni olarak öğreneceğimiz row/cell silme olayını reactive bir şekilde nasıl gerçekleştirebileceğimizden bahsedeceğim.

Şimdi aşağıdaki kodu mevcut kodla değiştirmekle başlayalım.

Adım adım bu kodlar ne yapıyor bakalım:

  1. Table view’in reactive extension’ı olan modelDeleted(_:) metoduna dönecek elemanın tipini paslayıp çağırdığımızda metod bize table view’den silinen elemanı Observable tipinde döndürür.
  2. Bu Observable değişkeni subscribe(onNext:) metodun içindeki closure’a paslarız. Bu closure her cell silindiğinde çalışacaktır.
  3. Table view’den silinen CartItem modelimizin barındırdığı coffee objesini ShoppingCart modelimizden de siliyoruz.
  4. subscribe(onNext:) metodu bize Disposable tipinde bir değişken döndürür. ShoppingCartViewController sınıfı hafızadan uçtuğunda oluşturduğumuz Observer’dan düzgün bir şekilde kurtulmak için bu sınıfındisposeBag‘ine eklememiz gerekmektedir.

Artık neredeyse tüm projemizi reactive bir yapıya çevirdik. Bir tek OrderCoffeeViewController.swift dosyası kaldı. Öğrendiklerinizi pekiştirmek isterseniz bu sayfayı reactive’e hale getirmeyi size ev ödevi olarak bırakıyorum.

Bu benim Medium’daki ilk makalem. Zaman buldukça iOS geliştirme ile ilgili yazılar yazmayı planlıyorum. Eğer yazıyı sevdiyseniz beğenmeyi ve beni takip etmeyi unutmayın.

RxSwift, RxCocoa ile ilgili sorularınız varsa bana gktggumus@gmail.com adresinden e-mail yolluyla ulaşabilirsiniz.