Merhaba bu yazı MVVM serisinin ilk gönderisi, MVVM patternini tek bir konu ile anlatmak yerine bir kaç küçük parçaya bölerek anlatmayı düşündüm böylelikle baymadan konuya hakim olmanızı istedim.

Seri 1de hazırladığım örnekte, Github public repolarını TableView’da repo adı ve repo url bilgisiyle ile birlikte listeleme yapacağız. örnek proje için linki konu sonunda paylaştım bitmiş halide tam olarak şöyle olacak:

Controller

Controller

ilk olarak listeleme yapabilmekiçin bir tableview controllerdan türemiş bir controllera ihtiyacımız var,

Controller

şimdi yapmamız gereken storyboarda gidip bir tableviewcontroller ekleyip sınıfınıda yeni oluşturduğumuz PublicRepoListVC olarak tanımlıyoruz.

Controller

Şimdi github servisinden alacağımız iki bilgi vardı birisi repo adı ve diğeride repo url bilgisiydi bunun için custom cell tasarımı yapmamız gerekiyor aslında yapmadanda olur biliyorum ama yapalımda havalı olsun.

View

bir sınıf oluşturuyoruz ismi RepoCell olsun ve UITableViewCell sınıfından türesin ayrıca also xib dosyasınıda create xib olarak seçiyoruz ve oluştur diyoruz.

Controller

RepoCell sınıfımızı aşağıdaki gibi düzenlememiz gerekiyor.

import UIKit
class RepoCell: UITableViewCell {

    @IBOutlet weak private var repoNameLabel: UILabel!
    @IBOutlet weak private var repoUrlLabel: UILabel!

}

şimdilik RepoCell sınıfımızın bu şekilde bırakıyoruz daha sonra yine döneceğiz buraya, sırada Web servise istek yapacak olan sınıfı ve istekten dönecek veriden bizim işimizi görecek alanları almamızı sağlayacak modelimizi yazmak var, ilk olarak modelimizden başlıyoruz,

Model

Yeni bir swift dosyası oluşturuyoruz ve Modelimizin adını PublicRepoModel olarak veriyoruz, içeriği tam olarak şu şekilde olmalı:

import Foundation

struct PublicRepoModel:Decodable {
    let full_name:String
    let html_url:String
}

son derecek basit, Decodable protokolüne uygun olması için dikkat edilen nokta web servisten dönen json cevabında anahtar nasıl yazılıyorsa isimlendirmeyide burada öyle yapmamız. devam edelim şimdi sıra Servisi yazmakta.

Service

Service isimli bir class oluşturalım ve aşağıdaki gibi tanımlayalım:

import Foundation

class Service: NSObject {
    static let shared = Service()
    
    func fetchAllPublicRepo(completion: @escaping ([PublicRepoModel]?, Error?) -> ()) {
        let urlString = "https://api.github.com/repositories"
        guard let url = URL(string: urlString) else { return }
        URLSession.shared.dataTask(with: url) { (data, resp, err) in
            if let err = err {
                completion(nil, err)
                print("Failed to fetch courses:", err)
                return
            }
            
            // check response
            
            guard let data = data else { return }
            do {
                let courses = try JSONDecoder().decode([PublicRepoModel].self, from: data)
                DispatchQueue.main.async {
                    completion(courses, nil)
                }
            } catch let jsonErr {
                print("Failed to decode:", jsonErr)
            }
            }.resume()
    }
}

View’a geri dönelim

Servis tanımlamasınıda tamamladığımıza göre artık ViewModel’a geçmeden önce son bir işimiz kalıyor RepoCell sınıfımıza dönelim ve aşağıdaki gibi güncelleyelim kodumuzu:

import UIKit

class RepoCell: UITableViewCell {

    @IBOutlet weak private var repoNameLabel: UILabel!
    @IBOutlet weak private var repoUrlLabel: UILabel!
    
    var repoViewModel:PublicRepoViewModel! {
        didSet {
            repoNameLabel?.text = repoViewModel.name
            repoUrlLabel?.text = repoViewModel.url
        
            self.backgroundColor = repoViewModel.randomColor()
        }
    }
    
}

burada yeni eklediğimiz repoViewModel observer, dışarıdan değer atandığında cellimizin labellarını dolduracak, backgroundColor kısmına şimdi geliyoruz..

sırada ViewModelımız var,

ViewModel

PuclicRepoViewModel sınıfımızı oluşturuyoruz, bu sınıfta view’da kullanıcının görmesini istediğimiz verileri işleyerek gösterime uygun hale getireceğiz hemen başlayalım.

kodumuz şu şekilde olacak:

import UIKit

class PuclicRepoViewModel: NSObject {
    let name:String
    let url:String

    // DI : Depency Injection
    init(publicRepos:PublicRepoModel) {
        self.name = "repo adı: \(publicRepos.full_name)"
        self.url = "repo URL: \(publicRepos.html_url)"
    }
    
    func randomColor()->UIColor {
        return UIColor.rainbowColor()
    }
}

PuclicRepoViewModel init metodunun içerisinde Model layerımızda servisten alınan dataları formatlayarak kendi propertylerine atıyor yani şu şekilde:

        self.name = "repo adı: \(publicRepos.full_name)"

MVVM içerisindeki ViewModel aslında tam olarak bu işleri yapıyor, teorik olarak biraz daha açıklamakta fayda var. Modelimizde yani biznıs lociğimizde datalar hamdır işlenmemiş kullanıcıya gösterilmeye uygun değildir, bu noktada o dataları alıp ViewModel içerisinde işleyerek View’da gösterime uygun hale getiririz. View’ın işleri burada yürütülür, View ViewModelle haberleşir ve Modelden yani biznıs locikten hiçbir mesaj almaz ve ne olduğunuda bilmez. View için bir ihtiyaç meydana geldiğinde örneğimizde olduğu gibi her bir Cell farklı renkte olsun istenildiğinde bu işlemi ViewModelde yapmamız gerekiyor MVVM’e göre yukarıda görüldüğü gibi bu işlemide

	func randomColor()->UIColor 

metoduyla hallediyoruz. Bu bize ne fayda sağladı bi özetleyelim.

  • View için Modelimizdeki data ViewModelde formatlanarak Controller şişmesi engellendi
  • Modelimiz business logic ile ilgilendi sadece
  • View için yeni bir güncelleme geldiğinde ilgileneceğimiz yer ViewModel olacak sadece.

iPucu: her bir Viewın kendine ait bir ViewModeli olması gerekiyor. Yeni bir ekran yeni bir viewmodel demek.

unutmadan ranbowColor() için aşağıdaki kodu kullanabilirsiniz:

extension UIColor {
    static func rainbowColor() -> UIColor {
        let randomRed:CGFloat = CGFloat(drand48())
        let randomGreen:CGFloat = CGFloat(drand48())
        let randomBlue:CGFloat = CGFloat(drand48())
        return UIColor(red: randomRed, green: randomGreen, blue: randomBlue, alpha: 1.0)
    }
}

Controller tekrar..

Şimdi tüm bu hazırlıkların sonucunu görme vakti geldi, Controller sınıfımızıda gondiklediğimizde bizden gıralı yok. hadi başlayalım,

class PublicRepoListVC: UITableViewController {
    let cellID = "repoCell"
    var repoViewModel = [PuclicRepoViewModel]()

yukarıdaki iki property tanımlamasını yapalım, repoViewModeli servis çağrısı yaptığımızda dönen servis datasını saklamak için kullanacağız cellID malum tableview için.

viewDidLoad() metodumuzu aşağıdaki gibi güncelleyelim:

override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.register(UINib(nibName: "RepoCell", bundle: nil), forCellReuseIdentifier: cellID)
        self.tableView.tableFooterView = UIView()
        
        fetchAllPuclicRepo()
    } 

daha önce oluşturduğumuz RepoCelli tableview’ımıza tanıttık daha sonra bir footerview verdik data olmayan cell görünmesin diye. fetchAllPuclicRepo() metoduna çağrıda bulunduk şimdi bu metodumuzu aşağıdaki gibi oluşturalım:

func fetchAllPuclicRepo() {
        Service.shared.fetchAllPublicRepo { (publicRepos, error) in
            if (error != nil) {
                print("repo bilgileri alınırken hata oluştu !")
                return
            }
            
            self.repoViewModel = publicRepos?.map ({ return PuclicRepoViewModel(publicRepos: $0)}) ?? []
            self.tableView.reloadData()
        }
    }

ne yaptık bi bakalım, self.repoViewModel = servisten gelen data bizim Decodable Modelimiz bu veriyi aldık ve mapledik PuclicRepoViewModel ile dönen datayıda repoViewModelimize verdik, tüm atama işlemi sonlandığında tablomuzu yenile dedik.

controllerımızın geri kalan kodu aşağıdaki gibi:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return repoViewModel.count
    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellID, for: indexPath) as! RepoCell
        cell.repoViewModel = repoViewModel[indexPath.row]

        return cell
    }

burada

cell.repoViewModel = repoViewModel[indexPath.row]

yaptığımız atama işlemi şu şekilde yorumlanabilir, RepoCell içerisinde bir observer property tanımlamıştık ona az önce servisten gelen veriyi yüklediğimiz repoViewModel dizimizdeki her bir indexi gezerek dataları aktarıyoruz her bir aktarım işleminde aşağıdaki atama gerçekleşiyor:

var repoViewModel:PuclicRepoViewModel! {
        didSet {
            repoNameLabel?.text = repoViewModel.name
            repoUrlLabel?.text = repoViewModel.url
        
            self.backgroundColor = repoViewModel.randomColor()
        }
    }

yani repo adını ve url bilgisini labellara yaz cellin arkaplanını rastgele bir renk ile doldur. tüm numara bu kadardı.

Özet:

şimdi kısa bir özet geçelim;

  • View’da datalar göstermeden önce istersek test yazabiliriz ViewModel için
  • Daha temiz ve fit bir Controllerımız var
  • Modelimiz son derece basit sadece datayla ilgileniyor
  • ViewModelimiz Viewda data nasıl gösterilecekse tüm angaryayı kendi yönetiyor

Bir sonraki yazıda görüşmek üzere.

MVVM kaynak kodu:

MVVM S1 Github