Swift/UIKit’te VIPER Mimarisi ile Uygulama Geliştirme

İlyas Hayrat
Team Kraken
Published in
7 min readDec 27, 2023

Yazılım geliştirme süreçlerinde yazılan kodların test edilebilir, düzenlenebilir ve geliştirilebilir olması çok önemlidir. Bu şekilde olması biz yazılımcılar için de çok büyük kolaylık sağlamaktadır. Bu özellikleri barındıran kodları yazabilmemiz için de çeşitli mimarilerden yararlanırız. Bu yazımda da iOS uygulama geliştiriciler için çok yaygın olarak kullanılan VIPER mimarisinden örnek bir uygulama yaparak bahsedeceğim.

VIPER nedir?

VIPER; View, Interactor, Presenter, Entity ve Router kelimelerinin ilk harflerinden adını alan bir mimari yaklaşımıdır.

VIPER mimari modeli, MVC veya MVVM’e bir alternatiftir. VIPER, katmanlar arasındaki bağımlılıkları azaltmayı hedefler.

  • View: Tasarım ve sayfa kısımlarının bulunduğu alan.
  • Interactor: Mantıksal işlemlerin yürütüldüğü katman. Network istekleri vb.
  • Presenter: View ile Interactor arasındaki köprü görevini gören katmandır. Lojik süreçlere göre sayfa yönetimini yapan kısımdır.
  • Entity: Model dosyalarının bulunduğu katmandır.
  • Router: Sayfalarımızın hareketlerini belirleyen ve yönetimini yapan kısımdır.

Örnek uygulamamız ise son depremleri bir API üzerinden çekerek listeleyen ve ilgili liste elemanına tıklandığı zaman harita üzerinde konumunu gösteren bir uygulama olacak. İlk olarak boş bir Swift projesi oluşturarak başlıyoruz.

Ben bu uygulamada interface olarak Storyboard kullanarak ilerleyeceğim için Storyboard seçerek ilerledim. Bu yazımda tasarımla alakalı herhangi bir konudan bahsetmeyeceğim. Projenin GitHub linkini ekleyeceğim. Dilerseniz oradan inceleyebilirsiniz :)

Projemizi oluşturduktan sonra klasörlerimizi mimarimize uygun olarak oluşturuyoruz.

  • EarthquakeDetails: TableView üzerinde herhangi bir cell’e tıklandığı zaman ilgili cell’in harita üzerinden konumunu gösteriyoruz. Buradaki klasörde detayla ilgili kısımları tutuyoruz.
  • Constants: Bu kısımda ise uygulama içerisinde birden çok yerde kullanılan const değerlerimizi tutuyoruz.
  • Models: Bu klasör ise API üzerinden gelen verilerimiz için oluşturacağımız modelleri barındırmakta.
  • Service: API’ye istek atan servisler için oluşturduğumuz kısım.
  • Helpers: Uygulama içerisinde bize yardımcı olacak fonksiyonları tutacağımız klasörümüz.
  • EarthquakeList: Bu yazının en önemli kısmı olan, son depremleri listeleyip TableView üzerinde gösterdiğimiz modülümüz.

Klasörlerimizi oluşturduktan sonra, ilk olarak modellerimizi oluşturarak başlayalım.

Ben burada modelleri oluştururken QuickType adlı siteden faydalandım. QuickType birçok dil için JSON verilerini modele dönüştürme işine yarayan, kullanımı gayet basit olan bir araçtır.

import Foundation

//MARK: - Earthquake
struct Earthquake: Codable {
let result: [Result]
}

//MARK: - Result
struct Result: Codable {
let title: String
let date: String
let mag, depth: Double
let geojson: Geojson
}

//MARK: - Geojson
struct Geojson: Codable {
let type: TypeEnum
let coordinates: [Double]
}

enum TypeEnum: String, Codable {
case point = "Point"
}

Proje içerisinde kullanacağımız modeller için Earthquake adlı bir swift dosyası oluşturuyoruz. Depremin nerede, ne zaman, hangi koordinatlarda olduğu bilgileri bizim için yeterli olduğundan bu bilgileri tutmamız yeterli olacaktır.

İlgili modellerimiz oluşturduktan sonra servis oluşturma kısmına gelelim. Servis kısmında harici bir paket kullanacağız. Network istekleri için yaygın olarak kullanılan Alamofire paketini tercih ettim. SPM (Swift Package Manager) aracılığı ile bu paketi projemize ekliyoruz. Bu paketi kullanmak istemeyenler için URLSession gayet yeterli olacaktır.

Servisimiz için basit iki tane helper eklemekle başlıyoruz. Helpers klasörü içerisine DataResult ve Error adında iki adet swift dosyası oluşturuyoruz.

import Foundation

public enum DataResult<Value> {
case success(Value)
case failure(Error)
}
import Foundation

public enum Error: Swift.Error {
case serializationError(internal: Swift.Error)
case networkError(internal: Swift.Error)
}
  • DataResult enum’u servisimizdeki isteğin başarılı ve başarısız olma durumlarını kontrol etmemizi kolaylaştıracak.
  • Error ise bir hata oluşursa bu hatanın hangi tip bir hata olduğunu ayırt etmemizi sağlayacak. (Şu an için basit bir enum, koşullara göre arttırılabilir).

Servis için helper kısımlarını ekledikten sonra istek atacağımız apiUrl’i Constants klasörü içerisine AppConstants adlı bir swift dosyası altına ekliyoruz.

import Foundation

class AppConstants {
static let apiUrl = "https://api.orhanaydogdu.com.tr/deprem/kandilli/live"
}

API olarak, Kandilli’den verileri çeken ve yaygın olarak kullanılan bir API’yi kullandım.

Servis oluşturmak için bütün adımlar tamamlanmış oldu, şimdi ise servisimizi ekleyelim.

import Foundation
import Alamofire

protocol ApiServiceProtocol {
func fetchEarthquake(completion: @escaping (DataResult<Earthquake>) -> Void)
}

class ApiService: ApiServiceProtocol {

func fetchEarthquake(completion: @escaping (DataResult<Earthquake>) -> Void) {
AF.request(AppConstants.apiUrl).responseData { (response) in
switch response.result {
case .success(let data):
do {
let response = try JSONDecoder().decode(Earthquake.self, from: data)
completion(.success(response))
} catch {
completion(.failure(Error.serializationError(internal: error)))
}
case .failure(let error):
completion(.failure(Error.networkError(internal: error)))
}
}
}
}

Burada Service klasörümüz içerisine ApiService adlı bir swift dosyası oluşturuyoruz. Bu kısımda bir tane ApiServiceProtocol adında bir protokol oluşturarak kullanacağımız servisleri ekliyoruz. Sadece güncel depremleri çektiğimiz için bir tane istek eklememiz yeterli şu an için.

Ardından ise ApiService adlı bir sınıf oluşturarak ApiServiceProtocol’un özelliklerini kullanmasını istiyoruz.

İsteğimizin başarılı olma durumlarını ve başarısız olma durumlarını fetchEarthquake fonksiyonumuz içerisindeki kontrol ederek sonucu bir DataResult olarak dönüyoruz.

Servis kısmını da bitirdikten sonra, en önemli kısım olan VIPER mimarimizi kullanacağımız kısma gelelim.

İlk olarak mimarimize uygun olarak dosyalarımızı oluşturmakla başlayalım.

Models adlı ayrı bir klasör içerisinde modellerimizi tuttuğumuz için Entity kısmı burada bulunmuyor fakat bu modüle özel bir model olsaydı bu kısımda Entity klasörü altında eklememiz gerekiyordu.

Protocols kısmı ile başlayalım; VIPER mimarisindeki katmanlar birbirleriyle protokoller aracılığıyla haberleşmektedir. Bu yüzden dolayı ilk olarak Protocols klasörü altında EarthquakeListProtocols adında bir swift dosyası oluşturuyoruz.

import Foundation

//MARK: - View
protocol PresenterToViewEarthquakeListProtocol {
func sendDataToView(_ earthquake: Earthquake)
func isLoading(_ isLoading: Bool)
}

//MARK: - Presenter
protocol ViewToPresenterEarthquakeListProtocol {
var interactor: PresenterToInteractorEarthquakeListProtocol? {get set}
var view: PresenterToViewEarthquakeListProtocol? {get set}
func load()
}

//MARK: - Interactor
protocol PresenterToInteractorEarthquakeListProtocol {
var presenter: InteractorToPresenterEartquakeListProtocol? {get set}
func load()
}

protocol InteractorToPresenterEartquakeListProtocol {
func sendDataToPresenter(_ earthquake: Earthquake)
func isLoading(_ isLoading: Bool)
}

//MARK: - Router
protocol PresenterToRouterEarthquakeListProtocol {
static func createModule(ref: EarthquakeListViewController)
}
  • PresenterToViewEarthquakeListProtocol: Bu protokolümüz Presenter katmanından aldığı verileri View katmanına ileten protokoldür.
  • ViewToPresenterEarthquakeListProtocol: Bu protokolümüz View kısmından Presenter’a isteklerini ileten protokoldür.
  • PresenterToInteractorEarthquakeListProtocol: Bu protokol ise Presenter katmanının isteklerini Interactor katmanına iletir.
  • InteractorToPresenterEartquakeListProtocol: Bu protokolümüz Interactor katmanından aldığı verileri Presenter katmanına ileten protokoldür.
  • PresenterToRouterEarthquakeListProtocol: Son protokol ve en önemli kısım olan protokolümüz ise katmanlar arası bağlantıyı yapan Router katmanı için gerekli olan protokoldür.

Protokollerimizi de ekledikten sonra, şimdi Interactor kısmını oluşturalım.

import Foundation

class EarthquakeListInteractor: PresenterToInteractorEarthquakeListProtocol {
var presenter: InteractorToPresenterEartquakeListProtocol?

func load() {
presenter?.isLoading(true)
ApiService().fetchEarthquake { result in
self.presenter?.isLoading(false)
switch result {
case .success(let earthquake):
self.presenter?.sendDataToPresenter(earthquake)
case .failure(let error):
print(error.localizedDescription)
}
}
}


}

PresenterToInteractorEarthquakeListProtocol protokolünün özelliklerini kullanan EarthquakeListInteractor adında bir sınıf oluşturuyoruz. Interactor kısmı lojik işlemleri barındıran bir katman olduğu için bu kısımda API üzerinden verileri çekeceğimiz servisimizi tetiklememiz gerekiyor. O yüzden load adında bir fonksiyon oluşturarak ApiService içerisindeki fetchEarthquake’i çağırıyoruz. Ardından buradan dönen sonuçlarımızı Presenter katmanımıza aktarıyoruz.

Interactor katmanımızdan dönen değerleri View’a iletmesi için Presenter katmanına iletmiştik. Şimdi ise Presenter katmanını oluşturuyoruz.

import Foundation

class EarthquakeListPresenter: ViewToPresenterEarthquakeListProtocol {

var interactor: PresenterToInteractorEarthquakeListProtocol?

var view: PresenterToViewEarthquakeListProtocol?

func load() {
interactor?.load()
}


}

extension EarthquakeListPresenter: InteractorToPresenterEartquakeListProtocol {
func sendDataToPresenter(_ earthquake: Earthquake) {
view?.sendDataToView(earthquake)
}

func isLoading(_ isLoading: Bool) {
view?.isLoading(isLoading)
}


}

Presenter katmanımızda ise ViewToPresenterEarthquakeListProtocol ve InteractorToPresenterEarthquakeListProtocol protokollerinin özelliklerini kullanan EarthquakeListPresenter adında bir sınıf oluşturuyoruz. Burada ise load fonksiyonu üzerinden Interactor katmanındaki load fonksiyonunu tetikliyoruz. sendDataToPresenter ve isLoading fonksiyonlarıyla da Interactor katmanından aldığımız sonuçları View katmanına iletiyoruz.

Interactor ve Presenter katmanlarımızı hallettikten sonra şimdi de Router katmanımızı oluşturalım.

import Foundation

class EarthquakeListRouter: PresenterToRouterEarthquakeListProtocol {
static func createModule(ref: EarthquakeListViewController) {
let presenter = EarthquakeListPresenter()
ref.presenter = presenter
ref.presenter?.interactor = EarthquakeListInteractor()
ref.presenter?.view = ref
ref.presenter?.interactor?.presenter = presenter
}
}

Burada ise createModule diyerek bir fonksiyon oluşturuyoruz ve burada Presenter katmanıyla iletişim kurarak, kullandığımız katmanlarımızın tanımlamalarını gerçekleştiriyoruz.

Router katmanımızı da oluşturduktan sonra, son katmanımız olan View katmanımızı oluşturalım.

import UIKit

class EarthquakeListViewController: UIViewController {

@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var loadingIndicator: UIActivityIndicatorView!

var earthquakeList: [Result] = []
var presenter: ViewToPresenterEarthquakeListProtocol?

override func viewDidLoad() {
super.viewDidLoad()
EarthquakeListRouter.createModule(ref: self)
tableView.delegate = self
tableView.dataSource = self

navigationItem.backBarButtonItem = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
}

override func viewWillAppear(_ animated: Bool) {
self.presenter?.load()
}


}

extension EarthquakeListViewController: PresenterToViewEarthquakeListProtocol {
func sendDataToView(_ earthquake: Earthquake) {
DispatchQueue.main.async {
self.earthquakeList = earthquake.result
self.tableView.reloadData()
}
}

func isLoading(_ isLoading: Bool) {
if isLoading {
tableView.isHidden = true
loadingIndicator.startAnimating()
} else {
loadingIndicator.stopAnimating()
tableView.isHidden = false
}
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let indexPath = self.tableView.indexPathForSelectedRow
let earthquake = earthquakeList[indexPath!.row]
if segue.identifier == "toDetail" {
if let earthquakeLocationVC = segue.destination as? EarthquakeLocationViewController {
earthquakeLocationVC.coordinates = earthquake.geojson.coordinates
earthquakeLocationVC.locationName = earthquake.title
}
}
}

}


extension EarthquakeListViewController: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return earthquakeList.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let earthquake = earthquakeList[indexPath.row]
let formattedDate = DateFormatterHelper.formatDate(earthquake.date)
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! EarthquakeListTableViewCell
cell.accessibilityIdentifier = "Cell_\(indexPath.row)"
cell.nameLabel.text = earthquake.title
cell.dateLabel.text = formattedDate
cell.magLabel.text = String(earthquake.mag)

return cell
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 55
}



func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
performSegue(withIdentifier: "toDetail", sender: nil)
}

}

Bu kısımda viewDidLoad fonksiyonun içerisinde Router katmanımızda oluşturduğumuz createModule fonksiyonumuzu çağırarak katmanlarımızın tanımlanma işlemini gerçekleştiriyoruz.

Ardından ise viewWillAppear içerisinde Presenter katmanımızdaki load metotumuzu tetikleyerek Interactor’dan aldığı verileri getirmesini istiyoruz.

Son olarak ise sendDataToView ve isLoading fonksiyonlarında ise Presenter katmanından dönen sonuçlara göre ekranımızdaki görünümleri tetikliyoruz.

Bu şekilde bir modül için VIPER mimarisini uygulamış olduk.

Bu kısımda VIPER mimarisini kullandığım listeleme sayfasının mantığını anlattım. Detay sayfasında harita gösterimini kullandım ve ayrıca örnek olması açısından bazı Unit Test’ler ve UI Test’ler de ekledim, bu kısımlardan yazımda bahsetmedim. Yazımın sonunda bırakacağım GitHub linkinden inceleyebilirsiniz.

Projeye buradan ulaşabilirsiniz.

Son Söz

Zamanınızı ayırıp bu yazıyı okuduğunuz için teşekkür ederim, faydalı olması dileğiyle. Team Kraken olarak düzenli bir şekilde paylaşımlarımıza devam edeceğiz.

Bizi aşağıdaki linklerden takip edebilirsiniz.

--

--