Swift Closures Nedir? Swift Closures Kullanımı Part 2

Merhaba arkadaşlar, Swift dersleri serimize  Swift Closures Part 2 ile devam edeceğiz. Closures ile ilgili temel bilgiye Swift Closure Kullanımı dersinden ulaşabilirsiniz. Bu konuların üzerine ekleyerek devam edeceğimiz bu dersimizde, Swift dilinin esnekliğine ciddi bir katkı yapan Swift Closures konusunu enine boyuna inceleyeceğiz.

İlk dersin kısa bir tekrarını yapıp ardından da yeni konularımızı anlatacağız. Kısaca nasıl tanımlandıklarından başlayalım:

   Swift Closures Syntax (Sözdizimi)

Closures, fonksiyonları kısaltarak yazabilme imkanı sağlamaktadır ve az kod yaz, çok iş yap (write less, do more) prensibini benimsemişlerdir. Bir değişken ya da sabite atanabilirler ve bir fonksiyona parametre olarak da gönderilebilirler. Aşağıda temel bir örnek ile başlayıp, oluşabilecek closure kombinasyonlarına değineceğiz.

let closureAdi:(1) -> 2 = { (3:4) in

}
  • 1 -> Parametre tipleri, Closure için gönderdiğimiz parametre tiplerini parantez içerisinde yazıyoruz. Birden fazla parametre tipi yazacaksak eğer aralarına virgül koyarak ekleyebiliriz.
  • 2 -> Dönüş parametre tipi, Dönüş parametresi tipini yazıyoruz. (Örn: String)
  • 3 -> Closure için gönderilen parametre
  • 4 -> Closure için gönderilen parametrenin tipi

   Closure tanımlaması yukarıdaki gibidir. Farklı tanımlama şekilleri de bulunmaktadır, bunlara aşağıda değineceğiz. Şimdi normal bir örnek yapalım:

let toplamaIslemi: (Int, Int) -> Int = { (x: Int, y: Int) in        
  return x + y
}

Yukarıda toplama işlemi yapan bir closure yazdık. Bu closure’ı daha da kısaltabiliriz. Adım adım bu closure’ı kısaltacağız.

let toplamaIslemiKisa: (Int, Int) -> Int = { (x, y) in
    return x + y
}

Bu fonksiyonda yakaladığımız değerlerin tipini yazmamıza gerek yoktur. toplamaIslemiKisa closure’unun içine aldığı parametreler Int literal olduğu için buradaki x ve y’nin tipini belirtmediğimiz durumda Int yapacaktır. Int literal değerler kod içinde kullandığımız int değerlerdir (Örn: 1 , 34, 45 vb.). Yukarıdaki closure içinde gönderdiğimiz parametreler int literal olacağı için x ve y’nin tipini belirtmesek bile bu değerler int olacaktır. Bu duruma “type inferring” denmektedir.

let toplamaIslemiKisaV2 = { (x: Int, y:Int) in
    return x + y
}

Closurea göndereceğimiz tipleri de belirtmeyebiliriz ancak bu durumda yakalayacağımız x ve y değerlerinin tipini yazmamız gerekmektedir.

let toplamaIslemiDahaKisa: (Int, Int) -> Int = {return $0 + $1}

Bu closure’da ise x ve y değerlerini de yazmadık. Bu iki değeri de yazmamıza gerek yoktur. Buradaki $0 x’i, $1 ise y’i temsil etmektedir. toplamaIslemiDahaKisa() closure’u iki adet int değer almaktadır. $0 ve $1 bu değerlerdir. $1 yerine $2 yazdığımızda hata alırız. Çünkü bu durumda closure içerisinde $0, $1 ve $2 kullanacağımızı belirtmiş oluruz ancak closure iki adet parametre almaktadır, dolayısıyla üçüncü parametre olmadığı için hata ile karşılaşırız.

let toplamaIslemiEnKisa: (Int, Int) -> Int = {$0 + $1}

$0 ve $1’in closure ile gönderilen 1 ve 2. parametreler olduğunu söylemiştik. Eğer bu parametreleri yazıyorsak “return” yazmamıza da gerek kalmayacaktır.

  Trailing Closure

Eğer bir metodun içine aldığı parametrelerin en sonuncusu closure ise bunun için kısa bir yol olan trailing closure kullanılabilir. Örnekle açıklayalım:

func write(_ str: String, closure: (String) -> Void){
 // metot gövdesi
}

Metodumuzu yukarıda oluşturduk. Bu metodu çağırmak istediğimizde bizden iki adet parametre bekleyecek. Birincisi “str” parametresi, ikincisi ise closure olacaktır. Metot son parametre olarak closure aldığı için burada trailing closure’u gerçekleyebiliriz. Aşağıda normal ve trailing closure halini göreceğiz.

write("", closure: { (str: String) -> Void in

})

Yukarıda metodun normal çağrılma şekli var. Aynı metodu trailing closure vasıtasıyla aşağıdaki gibi de çağırabiliriz.

write("") { (str) in

}

Burada değişen şey son parametre olan closure’un parantezlerin “()” içerisinden dışarıda yazılmasıdır. Trailing closure bize metot gövdesini kısaltarak daha rahat anlamaya imkan veren bir yapı sunuyor.

  Escaping Closure

Bir metot parametre olarak closure alıyorsa ve bu closure metodun dışındaki bir parametrede tutulmak istenirse bu closure’un @escaping parametresi alması gerekmektedir. Örnekte daha açıklayıcı olacaktır:

struct Book{
  var readclosure: (() -> Void)     
  mutating func sampleMethod(closure: @escaping () -> Void){
     self.readclosure = closure
  }
}

Yukarıdaki örnekte sampleMethod() isminde bir metot oluşturduk ve bu metot parametre olarak bir closure almaktadır. Metodun dışındaki readclosure closure’una, metot parametresi olan closure’u atayabilmek için @escaping anahtar kelimesini kullanmamız gerekmektedir. Kullanılmadığı durumda “Assigning non-escaping parameter ‘closure’ to an @escaping closure” hatasını derleyici tarafından alacağız ve derleyici bu hatayı otomatik olarak @escaping parametresini ekleyerek düzeltecektir.

   Autoclosures

Autoclosure, yazdığımız clousure’u çağırırken kodu daha da basitleştirmemizi sağlar. Aşağıda normal ve autoclosure örneklerinden bunu görelim:

func sayMyName(closure: () -> String){
    print("My name is \(closure())")
}
    

func sayMyNameAuto(closure: @autoclosure () -> String){
    print("My name is \(closure())")    
}

Yukarıdaki her iki metot da bir stringi closure vasıtasıyla print etmektedir. Bu kısım her ikisi için de ortaktır ancak bu metotlar çağırılırken @autoclosure olan metot çağırımı daha sade olmaktadır. Aşağıda her ikisi de görülebilir:

sayMyName { () -> String in
   return "Ali"
}

sayMyNameAuto(closure: "Ali")

@autoclosure parametresi ile beraber @escaping parametresini de kullanarak metot parametresi olarak gelen closure metot dışına aktarılabilir. Bu haliyle yeni metodumuz aşağıdaki gibi olacaktır:

var nameClosure: (() -> String)?
    
func sayMyNameAutoV2(closure: @autoclosure @escaping () -> String){
    print("My name is \(closure())")
    self.nameClosure = closure
}

   Swift Closures ve Asenkron İşlemler

Swift Closures, metotlardan farklı olarak asenkron işlemlere izin verirler. Metot gövdeleri senkron olarak işleme alınırken, closures’da asenkron işlemler yapılabilir. Bu sebepten ötürü network requesti attığımız yapılar closures’dan meydana gelmektedir.

let session = URLSession.shared
let url = URL(string: "www.mobilhanem.com")!
let task = session.dataTask(with: url, completionHandler: { data, response, error in

})

Metot gövdeleri işlemleri sırasıyla yaparken closures observing(gözlemleme) işlemine izin verirler. Network requesti attığımızda, request devam ederken sırasıyla diğer işlemlerden devam edilir. Request cevabı alındığında ise, yukarıdaki örnekten devam edecek olursak, dataTask() gövdesi işleme alınır.

   View Controllerlar Arası Observing (Gözlemleme)

Bir view controllerdan diğerine geçerken veri aktarımı yapacağımız farklı yapılar mevcuttur, ancak gittiğimiz view controllerdan alttaki(dismiss) veya gerideki(pop) view controllera dönerken veri aktarımı yapmak biraz daha karmaşıktır. Bunun için kullanacağımız yapılardan bir tanesi de closures’dır. Aynı zamanda gittiğimiz view controllerdaki herhangi bir işlemi gözlemleyerek, bu işlem meydana geldiğinde ilk view controllerda bir metot tetiklenebilir. Şimdi bu işlemleri örnek vasıtasıyla açıklayalım ve örnek bir senaryo oluşturalım:

Üç adet view controllerımız var. AVC, BVC ve CVC. İlk önce observing işlemine bakalım. BVC içine bir closure tanımladık ve AVC’dan BVC’a geçerken bu observe işlemini destVC.successListener vasıtasıyla tanımladık. BVC içine tanımladığımız closure hiç bir veri dönmediği için sadece haber vermek için kullanacağız. AVC’den BVC’ye geçerken firstButtonClick içindeki “destVC.successListener” BVC’den haber beklemeye başlayacaktır. BVC içindeki secondButtonClick vasıtasıyla bu haber gönderimini yapacağız. Haber göndermeden önce BVC içinde istediğimiz işlemi yapabiliriz, bu sırada AVC içindeki successListener haber göndermemizi bekleyecektir. BVC içinde işimiz bittikten sonra ihtiyacımız olan şey BVC’yi kapatırken başka işleme ihtiyaç duymadan CVC’ye geçmek. AVC içinde bunu successListener vasıtasıyla gerçekledik. Bu işlemin farklı kombinasyonları yapılabilir.

    class AVC: UIViewController{
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        @IBAction func firstButtonClick(_ sender: Any) {
            let sb = UIStoryboard(name: "Main", bundle: nil)
            if let destVC = sb.instantiateViewController(withIdentifier: "BVC") as? BVC{
                
                destVC.successListener = {
                    let sb = UIStoryboard(name: "Main", bundle: nil)
                    if let destVC = sb.instantiateViewController(withIdentifier: "CVC") as? CVC{
                        self.navigationController?.pushViewController(destVC, animated: true)
                    }
                }
                self.present(destVC, animated: false, completion: nil)
            }
        }
    }
    class BVC: UIViewController{


        typealias SuccessListener = () -> Void
        var successListener: SuccessListener?

        override func viewDidLoad() {
            super.viewDidLoad()
        }

        @IBAction func secondButtonClick(_ sender: Any) {
            self.dismiss(animated: false, completion: {
                self.successListener?()
            })
        }
    }

Not: typealias anahtar kelimesi var olan tipleri adlandırmamıza olanak verir. Yukarıda void dönen ve parametre almayan closure’u SuccessListener olarak adlandırdık.

class CVC: UIViewController{

        override func viewDidLoad() {
            super.viewDidLoad()
        }

    }

Şimdi aynı işlemi CVC’ye BVC’den data aktarımı için kullanalım. Burada BVC ve CVC arasında direk bir bağlantı olmadığı için AVC’yi aracı olarak kullanacağız. O halde yukarıdaki kodlarımızda bir kaç küçük değişiklik yapalım.

    class AVC: UIViewController{
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        @IBAction func firstButtonClick(_ sender: Any) {
            let sb = UIStoryboard(name: "Main", bundle: nil)
            if let destVC = sb.instantiateViewController(withIdentifier: "BVC") as? BVC{
                destVC.successListener = { hello in
                    let sb = UIStoryboard(name: "Main", bundle: nil)
                    if let destVC = sb.instantiateViewController(withIdentifier: "CVC") as? CVC{
                        destVC.helloStr = hello
                        self.navigationController?.pushViewController(destVC, animated: true)
                    }
                }
                self.present(destVC, animated: false, completion: nil)
            }
        }
    }
    class BVC: UIViewController{
        
        
        typealias SuccessListener = (_ str: String) -> ()
        var successListener: SuccessListener?
        
        override func viewDidLoad() {
            super.viewDidLoad()
        }
        
        @IBAction func secondButtonClick(_ sender: Any) {
            self.dismiss(animated: false, completion: {
                self.successListener?("Hello World")
            })
        }
    }
class CVC: UIViewController{
        
        var helloStr: String = ""
        
        override func viewDidLoad() {
            super.viewDidLoad()
            
            print(helloStr)
        }
    }

BVC içinde tanımladığımız closure’a geri döndürmesi için bir string parametresi ekledik. secondButtonClick içinde döndüreceğimiz string parametresinin “Hello World” olacağını belirledik. AVC içindeki successListener artık bizim göndereceğimiz parametreyi bekliyor. AVC içindeki successListenerda hello parametresi ile bu string parametreyi yakaladık ve CVC’ye geçerken CVC içindeki helloStr değişkenine atadık. CVC içinde print ettiğimizde “Hello World” yazdığını görürüz.

   Swift Closures “weak self” ve “unowned self”

  Closure kullanırken bellek yönetimini doğru yapmamız gerekmektedir. Swift Class ve Struct Yapısı dersinde class, fonksiyon ve closureların referans tipi olduğunu söylemiştik. Bu referansları bizim yerimize ARC(automatic reference counting) yönetmektedir. Detaylı bilgiyi ARC konusunda anlatacağız ancak kısaca açıklamak gerekirse, bir class ile işimiz bittiğinde bellekten bu classın temizlenebilmesi için bu class içinde hiç referans kalmaması gerekmektedir. Bu sebeple tanımladığımız closures’ a  weak self ve unowned self parametrelerini vererek bellekte referans oluşturmamalarını sağlarız. Bu durumda classtan oluşan nesneler de ARC tarafından temizlenir ve doğru bir bellek yönetimi sayesinde retain cycle’dan (bellek açıkları) kurtulmuş oluruz.

  destVC.failListener = { [weak self] in
      self?.error(errorMessage:"Bir hata oluştu.")
  }

Yukarıdaki closure içinde view controllera ait olan error metodunu kullanmak için self kullandık. Burada bir adet strong referans oluşturduk. Eğer closure’da “weak self” kullanıp, closure’ın strong referans oluşturmasını önlemez isek bu view controllerdaki referans sayısı 0’a düşmez ve bu view controller bellekten silinmez. Bu durumun önüne geçmek için [weak self] kullandık. weak self ve unowned self arasında bir fark bulunmaktadır. Eğer oluşturduğumuz closure’un nil olmaması gerekiyorsa unowned, nil olabiliyorsa da weak parametresi kullanılır.

Özet

Bir dersin daha sonuna geldik. Closures, Swift dilinde önemli bir yere sahiptir. Doğru kullanıldığı takdirde kod karmaşıklığı azalacak ve yazılan kodun anlaşılırlığı artacaktır. Bu dersi anlatırken özellikle asenkron işlemler, observing ve bellek yönetimi kısmının Closure konusundaki en önemli noktalar olduğunu düşünüyorum. Doğru bellek yönetimi ile daha uzun ömürlü uygulamalar geliştirirken, gözlemleme ve veri aktarımı kullanarak da efektif çözümler üretebiliriz.

Faydalı bir ders olmasını umarak herkese mutluluklar diliyorum. Soru, görüş ve önerilerinizi y0rum kısmından veya soru-cevap kısmından iletebilirsiniz. Sağlıcakla…

Kaynak: https://docs.swift.org/swift-book/LanguageGuide/Closures.html

8

Ali Hasanoglu

1 Yorum

Haftalık Bülten

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