Swift Protocol Nedir? Her Yönüyle Swift Protocols Kullanımı

Merhaba arkadaşlar, Swift dersleri serimize Swift Protocol (Protokoller) ile devam ediyoruz. Önceki dersimizde anlattığımız protokol yönelimli programlama Swift protokolleri sayesinde gerçeklenmiştir. Aslında o dersimizin içeriğinde protokollerden bir miktar bahsetmiştik, detaylı anlatımını bu ders içeriğinde yapcağız. Protokolü anlaşma veya iletişim kuralı olarak çevirebiliriz. Oluşturduğumuz protokoller içindeki değişken veya metodları, bu protokolü kabul eden (uygulayan, conform eden) struct, enum veya sınıf anlaşma gereği tanımlamak zorundadır. Bu dersin içeriğinde kullanılacak örnekler, anlam bütünlüğünü korumak amacıyla, Swift Protokol ve Nesne Yönelimli Prog. Yaklaşımları dersinin benzerleri olacaktır ve ilk önce Swift Protokol ve Nesne Yönelimli Prog. Yaklaşımları dersini okumanızı tavsiye ederim. Swift Protokollerin nasıl tanımlandığı ile başlayalım o halde:

Swift Protocol Syntax

Not : Kargalar hem karada hem havada yaşar 😀

protocol KaraHayvani {

}

Bir protokolü yukarıdaki şekilde tanımlayabiliriz. Daha sonra struct, class veya enum ile bu protokolü aşağıdaki şekilde kullanabiliriz.

struct Karga: KaraHayvani{

}

Swift dili çoklu miras alma (multiple inheritance) özelliğini desteklememektedir. Swift dokümantasyonunda da class ve inheritance yerine struct, enum ve protokollerle çalışmayı önermekteydi. Detaylarına buradan bakabilirsiniz. Multiple inheritance desteği olmayan Swift dili çoklu protokol eklemeye izin vermektedir.

protocol HavaHayvani {

}
struct Karga: KaraHayvani, HavaHayvani{

}

Yukarıda daha önce oluşturduğumuz Karga structımız için iki adet protokol ekleyebildik. Şimdi aşağıda protokoller içine eklediğimiz metot ve değişkenlere bakalım:

protocol KaraHayvani {
    var karadaHareket: Bool {get set}
    func karadaHareketEt()
}

Yukarıdaki protokolümüze bir değişken ve bir metot ekledik. Eklediğimiz metodun gövdesi boştur ve eklediğimiz struct, sınıf veya enum içerisinde bu metodun içini doldurabiliriz.

struct Karga: KaraHayvani{
   var karadaHareket: Bool

   func karadaHareketEt() {
       print("Karga hareket ediyor")
   }
}
var karga = Karga(karadaHareket: true)

 Swift Protocol { get set}

Swift protocol içine bir değişken eklediğimizde parantez içerisinde yanına getter ve setter yazmamız gerekir. Önceki yazılarımızda get ve set anahtar kelimelerinden bahsetmiştik. Kısaca hatırlatmak gerekirse, değişkenin yanında yer alan get ve set anahtar kelimeleri bu değişkenin hem okunabileceğini hem de değiştirilebileceğini temsil eder. Eğer değiştirilmesini istemiyorsak parantez içerisine sadece get eklemeliyiz. Bu aşamada iki farklı durumdan bahsetmeliyiz.

Birinci durum: Yukarıda Karga structı ile KaraHayvani protokolünü birlikte kullandık. Oluşturduğumuz “karga” nesnesi ile karadaHareket değişkenine erişip hem okuyabilir hem de değiştirebiliriz.

let value = karga.karadaHareket   -> değeri okuma (get)
karga.karadaHareket = false       -> değeri değiştirme (set)

Protokolü şu şekilde değiştirelim:

protocol KaraHayvani {
    var karadaHareket: Bool {get}
    func karadaHareketEt()
}

Yukarıdaki haliyle artık oluşturacağımız nesnenin sadece get işlemi yapmasını bekleriz ancak hangi durumda?

var karga = Karga(karadaHareket: true)

Eğer bu şekilde oluşturursak Karga tipinde bir nesne oluşturmuş oluruz ve karga nesnesinin hala get ve set işlemi yaptığını görürüz. Peki o halde biz değişken yanındaki get ve setleri neden yazıyoruz. İşte cevap:

var karga: KaraHayvani = Karga(karadaHareket: true)

Bu şekilde tanımladığımızda değişkenlerin protokol içindeki getter ve setter değerlerine uyacaktır. Ancak bu durumda da protokolü bir tip olarak kullandığımız için oluşturduğumuz nesne sadece KaraHayvani protokolü içindeki değişken ve metotlara erişebilir, yani Karga structı içine protokolün içindekiler dışında bir değişken eklediğimizde bunu KaraHayvani tipinde oluşturduğumuz karga nesnesi göremez. Ayrıca farklı protokol içeriklerini de göremez. Aşağıda protokol tipi başlığı altında detaylandırılmıştır.

İkinci Durum:

Protokol içine yazdığımız değişkenleri conform (eklediğimiz sınıf, struct veya enum. Yazının devamında bu kelimeyi kullanacağım) ettiğimiz struct içinde computed property  olarak ekleyebiliriz. Bu haliyle değişkenin protocol içinde tanımlanırken kullanılan getter ve setter değerlerine göre get ve set etmemiz gerekmektedir.

struct Karga: KaraHayvani{
    var type: Bool?
    var karadaHareket: Bool{
        get{
            return true
        }
        set (newValue){
            self.type = newValue
        }
    }
    func karadaHareketEt() {
        print("Karga hareket ediyor")
    }
}

Yukarıdaki örnekte karadaHareket değişkenini protocol içinde tanımlanırken {get set} olarak tanımlanmıştır ve computed property olarak yazılırken hem getter hem setter yazılmıştır. Protocol içinde tanımlanırken sadece {get} yazılsa idi örneğimiz şu şekilde olacaktı:

struct Karga: KaraHayvani{
    var type: Bool?
    var karadaHareket: Bool{
        return true
    }
    func karadaHareketEt() {
        print("Karga hareket ediyor")
    }
}

Swift Protocol Initializer

Protocol içinde initializer metodu tanımlanabilir ancak bu metodlar struct ve sınıflar arasında farklılık gösterir. Structlar hiçbir değişikliğe uğramadan bu init metodunu alabilirken sınıflarda init metodunun önüne “required” anahtar kelimesi eklenmelidir. Örnekler üzerinden devam edelim:

protocol KaraHayvani {
    init(name: String, havadaHareket: Bool)
}
struct Penguen: KaraHayvani {
    var name: String
    var havadaHareket: Bool
    
    init(name: String, havadaHareket: Bool) {
        self.name = name
        self.havadaHareket = havadaHareket
    }
}
class PenguenClass: KaraHayvani {
    
    var name: String
    var havadaHareket: Bool
    
    required init(name: String, havadaHareket: Bool) {
        self.name = name
        self.havadaHareket = havadaHareket
    }
}

Init metodlarını protocol ile eklememiz mümkündür. Bu aşamada sadece sınıflar için geçerli olan “convenience” anahtar kelimesinden de bahsetmek isterim. “convenience” anahtar kelimesi ile beraber kullanılan bir initializer sınıf için tanımlamak zorunda olduğumuz “designated initializer” a erişmemize izin verir. PenguenClass sınıfımızı şu şekilde değiştirip örnek üzerinden devam edelim:

class PenguenClass: KaraHayvani {
    var name: String
    var havadaHareket: Bool
    var type: String
    
    init(name: String, havadaHareket: Bool, type: String) {
        self.name = name
        self.havadaHareket = havadaHareket
        self.type = type
    }
    
    required convenience init(name: String, havadaHareket: Bool) {
        let type = "Uçamayan Kuş"
        self.init(name: name, havadaHareket: havadaHareket, type: type)
    }
}

Protokol ile gelen required initializer convenience ile beraber kullanıldı ve designated initializer’a erişmesini sağladık.

Çoklu Protocol Kullanımı

Bir sınıf, struct veya enum ile birden fazla protokol kullanabiliriz.

Not 1: “mutating” anahtar kelimesinden daha önce bahsetmiştik. Struct ve enumların içinde bulunan metotlar, eğer struct veya enumun bir değişkenini değiştiriyorsa (set ediyorsa) bu metodu mutating anahtar kelimesi ile beraber kullanmamız gerekiyor. Eğer protokol içine yazdığımız metotlar, birlikte kullanıldığı struct veya enumun bir değişkenini değiştirmeye çalışıyorsa, protokol içinde tanımlanırken mutating anahtar kelimesi ile beraber tanımlanmalıdır.

protocol HavaHayvani {
    mutating func havadaHareketEt()
}
struct Karga: KaraHayvani, HavaHayvani{
    
    mutating func havadaHareketEt() {
        self.karadaHareket = false
    }
    
    var karadaHareket: Bool
    
    func karadaHareketEt() {
        print("Karga hareket ediyor")
    }
}

Dikkat edersek havadaHareketEt() metodu Karga struct’ı içindeki karadaHareket değişkenini değiştiriyor. Bu durumda bu metodun protokol içinde mutating anahtar kelimesi ile beraber kullanılması gerekmektedir.

Not 2: Protokol içine yazdığımız metodlardan herhangi biri mutating anahtar kelimesi ile beraber kullanılıyorsa, bu protokol class ile beraber kullanıldığında, class içindeki metodlar için mutating anahtar kelimesini yazmayız çünkü bu anahtar kelime struct ve enumlar için kullanılmaktadır.

class Kumru: HavaHayvani{
   
    func havadaHareketEt() {
    }
}

Swift Protocol Extension

Extensions dersinde protokollere extension yazılabileceğinden bahsetmiştik. Protokollere extension yazarak conform ettiğimiz struct vb. protokol extension’ı içinde yazdığımız değişken ve metodları eklemek zorunda olmayız. Örnek vermek gerekirse:

protocol KaraHayvani {
    var karadaHareket: Bool {get }
    func karadaHareketEt()
    func yemekYe(isim: String)
}
extension KaraHayvani{
    func yemekYe(isim: String) {
        print("\(isim) yemek yiyor")
    }
}

Extension içine yazmadığımız metot ve değişkenler conform edilen struct, sınıf veya enum’a eklenmek zorundadır ancak extension içine yazılan metot ve değişkenlere erişilebilir ama conform ettiğimiz struct, sınıf veya enum’a eklemek zorunda olmayız. Karga structımız şu hale gelecektir:

struct Karga: KaraHayvani{
    var type: Bool?
    var karadaHareket: Bool{
        return true
    }
    func karadaHareketEt() {
        print("Karga hareket ediyor")
    }
}
var karga = Karga(karadaHareket: true)
karga.yemekYe(isim: "Siyah Karga")     // -> Siyah Karga yemek yiyor.

Yukarıdan da görüleceği gibi karga nesnesi yemekYe() metodunu struct içine eklemediği halde erişebildi.

Optional Protocol

Optional protokoller, protokolü optional yaparak istediğimiz metot veya değişkeni kullanmama imkanını bize sunuyor.

@objc protocol DenizHayvani{
    var denizdeHareket: Bool {get set}
    @objc optional func denizdeHareketEt()
}

Optional protokoller sadece sınıflar ile kullanılabilir. Struct ve enum ile kullanılamazlar.

class Yunus: DenizHayvani{
    var denizdeHareket: Bool
    
    init(denizdeHareket: Bool) {
        self.denizdeHareket = denizdeHareket
    }
    func denizdeHareketEt() {
        print("Yunus yüzüyor.")
    }
}

Yukarıdaki denizdeHareketEt() metodu optional olduğu için yazmak zorunda değiliz.

class Yunus: DenizHayvani{
    var denizdeHareket: Bool
    
    init(denizdeHareket: Bool) {
        self.denizdeHareket = denizdeHareket
    }
}

Yunus sınıfını bu şekilde de yazabiliriz. Her iki durumda da Yunus sınıfından oluşturulacak nesneler denizdeHareketEt() metoduna erişebilecektir ancak ikinci durumdaki sınıf yapısında denizdeHareketEt() metodunu override etmediğimiz için compile time error alırız. Aslında bu durum proje büyüyüp karmaşıklaştıkça kafa karışıklığına neden olabilir. Bundan dolayı kişisel tavsiye olarak, protokol extensionları kullanmayı öneriyorum. 🙂

Swift Protocol Inheritance

Swift protokelleri inheritance desteklerler. Bir protokol başka bir protokolden inherit edilebilir.

protocol Hayvan {
    func nefesAl()
    func uyu()
}
protocol KaraHayvani: Hayvan {
    var karadaHareket: Bool {get }
    func karadaHareketEt()
}

Yukarıda KaraHayvani protokolü  Hayvan protokolünden inherit edilmiştir. Bu durumda KaraHayvani protokolünü conform edeceğimiz struct, sınıf veya enum’un Hayvan protokolünü de conform etmesi gerekmektedir.

struct Karga: KaraHayvani{

    var karadaHareket: Bool{
        return true
    }
    
    func karadaHareketEt() {
        print("Karga hareket ediyor.")
    }
    
    func nefesAl() {
        print("Karga nefes alıyor.")
    }
    
    func uyu() {
        print("Karga uyuyor.")
    }
}

Protocol Tipi (Protocol as Type)

Yukarıda protokolleri tip olarak kullanabileceğimizden bahsetmiştik. Burada örneği ile beraber göstereceğiz.

protocol KaraHayvani {
    var karadaHareket: Bool {get set}
    func karadaHareketEt()
}
struct Karga: KaraHayvani{

    var isim: String
    var karadaHareket: Bool
    
    func karadaHareketEt() {
        print("Karga yemek yiyor.")
    }
}
let kargaStruct: Karga = Karga(isim: "Rüştü", karadaHareket: true)
let kargaProtocol: KaraHayvani = Karga(isim: "Arda", karadaHareket: true)

İki adet nesne oluşturduk. İlk nesnemizi Karga structından oluşturduk. Karga structımız KaraHayvani protokolünü conform ettiği için “kargaStruct” nesnemiz hem Karga structının değişken ve metodlarına erişebiliyor hem de KaraHayvani protokolünün değişken ve metotlarına erişebiliyor.

İkinci nesnemiz ise “kargaProtocol” nesnesi ve KaraHayvani tipindedir. Kara Hayvani tipinde olduğu için sadece KaraHayvani protokolünün değişken ve metodlarına erişebilir.

Yukarıdaki örneklerde protokolü tip olarak kullanmayı gördük. Protokol tipinden oluşturduğumuz nesne ile sadece protokol içindeki metot ve değişkenlere erişebiliriz. Eğer Karga structımız farklı bir protokolü daha conform etseydi kargaStruct nesnemiz bu protokolün de değişken ve metodlarına erişebilecektir ancak kargaProtocol nesnemiz erişemez.

Swift Protocol Composition (Birleştirme)

Protokolü tip olarak kullandığımızda sadece protokolün metot ve değişkenlerine erişilebildiğini söylemiştik. Farz edelim bir nesne oluşturmamız gerekti ve oluşturduğumuz nesnenin iki veya daha fazla farklı protokole de erişmesini istiyoruz. Peki bu durum mümkün mü? Protokol composition (birleştirme) bize birden fazla protokolü birleştirme imkanı verir. Hemen örneğine bakalım:

protocol KaraHayvani {
    var karadaHareket: Bool {get set}
    func karadaHareketEt()
}
protocol HavaHayvani {
    var havadaHareket: Bool {get set}
    func havadaHareketEt()
}
struct Karga: KaraHayvani, HavaHayvani{

    var havadaHareket: Bool
    var isim: String
    var karadaHareket: Bool
    
    func karadaHareketEt() {
        print("Karga hareket ediyor.")
    }
    
    func havadaHareketEt() {
        print("Karga uçuyor")
    }
}
//
let kargaProtocol: KaraHayvani & HavaHayvani = Karga(havadaHareket: true, isim: "Arda", karadaHareket: true)
//

Composition işlemini kargaProtocol nesnesini tanımlarken “&” işareti vasıtasıyla kullandık. kargaProtocol nesnesi hem KaraHayvani hem de HavaHayvani protokollerine erişebilir.

Not 3: Birleştirmek istediğimiz protokolleri “typealias” kullanarak tek bir yerde tutabilir ve her seferinde “&” işareti kullanarak birleştirme yapmak zorunda kalmayız.

typealias CombinedProtocols = KaraHayvani & HavaHayvani
let kargaProtocol: CombinedProtocols = Karga(havadaHareket: true, isim: "Arda", karadaHareket: true)

Swift Protocol-Delegate Pattern

Bir view controllerdan diğerine geçerken veri aktarmanın farklı yöntemleri vardır ve rahatlıkla yapılabilir. Ancak geçtiğimiz view controllerdan geri dönerken veri aktarmak istediğimizde veya bir metodu tetiklemek istediğimizde ilk duruma göre biraz daha karışık bir yapı ile uğraşmamız gerekir. Bu yapılardan bir tanesi delegate yani temsilcidir. Önce örnek bir senaryo belirleyip daha sonra bu senaryoyu gerçekleyelim. İki Adet view controllerımız olduğunu düşünelim. İlk view controllerımız MainViewController ve ikinci view controllerımız CustomPopUpViewController. CustomPopUpViewController, MainViewController üzerine bir pop up view present ediyor(açıyor) ve bu pop up içindeki butona bastığımızda pop up dismiss(kapanırken) olurken MainViewController içindeki bir metodu tetiklemesini istiyoruz. İşte bunu yapacak olan şey bizim CustomPopUpViewController’a gönderdiğimiz delegateimiz. Şimdi kodlayarak bunu daha iyi anlayacağımızı düşünüyorum:

protocol CustomPopUpDelegate: AnyObject{
    func onDismiss(name: String)
}
class MainViewController: UIViewController, CustomPopUpDelegate {
    override func viewDidLoad() {
          super.viewDidLoad()
      }
    
    @IBAction func goPopUpView(_ sender: Any) {
        let sb = UIStoryboard(name: "Main", bundle: nil)
       DispatchQueue.main.async {
           if let destVC = sb.instantiateViewController(withIdentifier: "CustomPopUpViewController") as? CustomPopUpViewController{
                destVC.delegate = self
                self.present(destVC, animated: false)
           }
       }
    }
    
    func reloadViewData(name: String){
        // istediğimiz herhangi bir şeyi yapabiliriz.
        print("Delegate -> \(name) geldi")
    }
    
    func onDismiss(name: String) {
        self.reloadViewData(name: name)
    }
}
class CustomPopUpViewController: UIViewController{
    
    weak var delegate: CustomPopUpDelegate?
    
    override func viewDidLoad() {
           super.viewDidLoad()
       }
    
    
    @IBAction func btnDismiss(_ sender: Any) {
        self.dismiss(animated: true) { [weak self] in
            self?.delegate?.onDismiss(name: "CustomPopUpViewControllerDelegate")
        }
    }
}

Sonuç:

Kodlarımızı yazdık, adım adım açıklayalım. CustomPopUpDelegate adında bir protokol yazdık ve MainViewControllerımıza bu protokolü conform ettik. Bu protokol içinde bulunan onDismiss() metodu, bizim CustomPopUpDelegate tipinde oluşturduğumuz  delegate’imiz bu metodu çağırdığında tetiklenecektir. Peki ne zaman çağıracak; senaryomuzda bahsettiğimiz gibi pop up view’u dismiss ederken. MainViewController içindeki button ile CustomPopUpViewController’a geçerken, CustomPopUpViewController içindeki CustomPopUpDelegate tipindeki delegatemizi MainViewController olarak ayarladık. Yani artık delegatemiz MainViewControllerı temsil ediyor ve MainViewController içindeki CustomPopUpDelegate protokolünün metodlarına erişebiliyor. CustomPopUpViewController içindeki butona tıkladığımızda pop up viewu kapatırken bu metodu çağırdık ve eriştiğimizi gördük.

Anlattığımız senaryoda olduğu gibi pop up dismiss olurken MainViewControllerdaki onDismiss() metodunu tetikledi. Ayrıca string bir veri aktarımı da yaptık. Örnek olarak sadece string veri aktardık ancak closure, nesne, int, dictionary vb. aktarımı da yapabiliriz.

Not 4: Delegatein çok kullanıldığı bir başka örnek ise tableview cell, collectionview cell vb. içleridir. Bu sınıflar içinden farklı sayfayı açmak için kullandığımız metodlardan biri olan “present” metodunu çağıramayız. Bu işlemi yapabilmek için delegate kullanabiliriz. ViewControllerdan cell’e delegate gönderip daha sonra cell içindeki butona tıklandığında view controlerdaki metot içinde bu işlem yapılabilir. Bunun örneğini yapmayacağım ancak mantık yukarıdaki örneğin aynısı olacaktır. Herhangi bir sorun ile karşılaşırsanız yorum kısmında sormaktan çekinmeyin lütfen.

Not 5: CustomPopUpDelegate protokolünü yazarken yanına AnyObject parametresi ekledik. Bunu yapmamızın sebebi, bu protokolün sadece classlar ile (class only protocols) kullanılabileceğini belirttik. Struct ve enumlar ile bu protokolü kullanamayız. Bir başka sebebi ise bu protokolden oluşturduğumuz yeni instanceları “weak” olarak oluşturmamıza izin vermektedir. Instanceları weak oluşturmanın bize faydası strong reference cycle’a düşmememizdir çünkü delegate weak olarak tanımlandığında referans oluşturmaz ve reference counting artırılmaz, dolayısıyla delegate’in oluşturabileceği bellek açıklarından kurtuluruz. Reference counting anlatırken weak parametresini daha detaylı anlatacağız. Reference counting pek yakında 🙂 …

Özet

Protokolleri anlattığımız bu dersimizde protokollerle alakalı bahsedilebilecek her şeyden bahsetmeye çalıştım ve faydalı olacağını umuyorum. Protokol yönelimli programlama protokoller sayesinde var olmaktadır ve protokolleri kullandıkça daha anlaşılır kodlar yazdığınızı fark edeceksiniz. Bir yazılımcının amaçlarından biri de kolay anlaşılabilir kodlar yazabilmektir çünkü farklı biri projeye dahil olduğunda veya siz bir projeye dahil olduğunuzda spagetti kodla karşılaşmak istemezsiniz. Protokoller de bize daha anlaşılır kodlar yazabilmek için sunulmuştur. Dersi burada bitirirken herkese faydalı olmasını umarak, mutluluklar diliyorum. Soru, görüş ve önerilerinizi yorum kısmından veya soru-cevap kısmından iletebilirsiniz. Sağlıcakla…

Tüm Swift derslerimiz için tıklayınız.

Kaynak: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID270

 

50

Ali Hasanoglu

Yorum Yaz

Haftalık Bülten

Mobilhanem'de yayınlanan dersleri haftalık mail almak ister misiniz?