RxSwift ve RxCocoa’ya Giriş
Bugün sizlere 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 sonraCoffeeShop.xcworkspace
dosyasını Xcode ile açmalısınız.
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.
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 RxCocoaclass 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:
- RxCocoa extension’larından biri olan
text
sayesinde UITextField’in text değişkenine Observable bir değişken olarak ulaşabiliyoruz. - Normalde textfield’in text verisi
String
ya danil
olabiliyor.orEmpty
verinin her zamanString
tipinde gelmesini sağlıyor. - Girdi
validateEmail
metoduyla doğruluğu kontrol ediliyor. Eğer e-mailımız doğru formattaysatrue
, değil isefalse
tipine dönüştürülüyor.emailValid
değişkenin Bool tipinde bir Observable değişken olmasını istediğimizdenmap
ile girdiyi String’den Bool tipine dönüştürmemiz gerekiyor. 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.- 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.
Ş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:
- Ş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.
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:
bind(to:)
metodu aynı tipteki Observable değişkenleri birbirine bağlayabilmemize olanak tanır. (RxCocoa extensionlarından biri olanrx.isEnabled
butonun isEnabled değişkenini Observable bir değişken olarak döndürür.)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.
Ş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:
- Table view’deki her bir satır için çalışacak bu kodu
coffees
değişkeniyle ilişkilendirmek içinbind(to:)
metodunu çağırıyoruz. - RxCocoa extension’larına ulaşabilmek için
rx
metodunu çağırıyoruz. 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.- 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’imizinconfigure(with:_)
metoduna mevcut kahve bilgimizi vererek konfigüre ediyoruz. 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:
- RxCocoa extension’larına ulaşabilmek için
rx
metodunu çağırıyoruz. - 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. - 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. - Kahve detay sayfasına gitmek için daha önceden Storyboard’tan tanımladığımız segue’yi çalıştırıyoruz.
- Table view’de seçilen satırı, seçili durumundan çıkarıyoruz.
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:
- 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.
- 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. accept(:_)
metoduyla BehaviourSubject tipindeki bir değişkenin içersine yeni bir eleman emit edebiliyoruz.- 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.
- Güncellediğimiz dictionary’i
coffees
değişkenine emit ediyoruz. getTotalCost()
metodunu artıkFloat
yerineObservable<Float>
döndüren bir metoda çeviriyoruz.- 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çincoffees
değişkenini map’leyerek içindeki elemanlara ulaşabiliriz ve istediğimiz gibi değiştirebiliriz. getTotalCount()
metodunu artıkInt
yerineObservable<Int>
döndüren bir metoda çeviriyoruz.- Sepette bulunan toplam kahve adetini Observable olarak döndürüyoruz.
getCartItems()
metodunu artık[CartItem]
yerineObservable<[CartItem]>
döndüren bir metoda çeviriyoruz.- 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.
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:
- 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. - Bu Observable değişkeni
subscribe(onNext:)
metodun içindeki closure’a paslarız. Bu closure her cell silindiğinde çalışacaktır. - Table view’den silinen
CartItem
modelimizin barındırdığıcoffee
objesiniShoppingCart
modelimizden de siliyoruz. 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.